blob: 541220dbd7e5b94a56e730a5f684bfa54ccf8e9d [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS 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 "gestures/include/immediate_interpreter.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <limits>
#include <tuple>
#include "gestures/include/gestures.h"
#include "gestures/include/logging.h"
#include "gestures/include/util.h"
using std::bind;
using std::for_each;
using std::make_pair;
using std::make_tuple;
using std::max;
using std::min;
using std::tuple;
namespace gestures {
namespace {
float MaxMag(float a, float b) {
if (fabsf(a) > fabsf(b))
return a;
return b;
}
float MinMag(float a, float b) {
if (fabsf(a) < fabsf(b))
return a;
return b;
}
// A comparator class for use with STL algorithms that sorts FingerStates
// by their origin timestamp.
class FingerOriginCompare {
public:
explicit FingerOriginCompare(const ImmediateInterpreter* interpreter)
: interpreter_(interpreter) {
}
bool operator() (const FingerState* a, const FingerState* b) const {
return interpreter_->finger_origin_timestamp(a->tracking_id) <
interpreter_->finger_origin_timestamp(b->tracking_id);
}
private:
const ImmediateInterpreter* interpreter_;
};
} // namespace {}
void TapRecord::NoteTouch(short the_id, const FingerState& fs) {
// New finger must be close enough to an existing finger
if (!touched_.empty()) {
bool reject_new_finger = true;
for (map<short, FingerState, kMaxTapFingers>::const_iterator it =
touched_.begin(), e = touched_.end(); it != e; ++it) {
const FingerState& existing_fs = (*it).second;
if (immediate_interpreter_->metrics_->CloseEnoughToGesture(
Vector2(existing_fs),
Vector2(fs))) {
reject_new_finger = false;
break;
}
}
if (reject_new_finger)
return;
}
touched_[the_id] = fs;
}
void TapRecord::NoteRelease(short the_id) {
if (touched_.find(the_id) != touched_.end())
released_.insert(the_id);
}
void TapRecord::Remove(short the_id) {
min_tap_pressure_met_.erase(the_id);
min_cotap_pressure_met_.erase(the_id);
touched_.erase(the_id);
released_.erase(the_id);
}
float TapRecord::CotapMinPressure() const {
return immediate_interpreter_->tap_min_pressure() * 0.5;
}
void TapRecord::Update(const HardwareState& hwstate,
const HardwareState& prev_hwstate,
const set<short, kMaxTapFingers>& added,
const set<short, kMaxTapFingers>& removed,
const set<short, kMaxFingers>& dead) {
if (!t5r2_ && (hwstate.finger_cnt != hwstate.touch_cnt ||
prev_hwstate.finger_cnt != prev_hwstate.touch_cnt)) {
// switch to T5R2 mode
t5r2_ = true;
t5r2_touched_size_ = touched_.size();
t5r2_released_size_ = released_.size();
}
if (t5r2_) {
short diff = static_cast<short>(hwstate.touch_cnt) -
static_cast<short>(prev_hwstate.touch_cnt);
if (diff > 0)
t5r2_touched_size_ += diff;
else if (diff < 0)
t5r2_released_size_ += -diff;
}
for (set<short, kMaxTapFingers>::const_iterator it = added.begin(),
e = added.end(); it != e; ++it)
Log("TapRecord::Update: Added: %d", *it);
for (set<short, kMaxTapFingers>::const_iterator it = removed.begin(),
e = removed.end(); it != e; ++it)
Log("TapRecord::Update: Removed: %d", *it);
for (set<short, kMaxFingers>::const_iterator it = dead.begin(),
e = dead.end(); it != e; ++it)
Log("TapRecord::Update: Dead: %d", *it);
for_each(dead.begin(), dead.end(),
bind(&TapRecord::Remove, this, std::placeholders::_1));
for (set<short, kMaxTapFingers>::const_iterator it = added.begin(),
e = added.end(); it != e; ++it)
NoteTouch(*it, *hwstate.GetFingerState(*it));
for_each(removed.begin(), removed.end(),
bind(&TapRecord::NoteRelease, this, std::placeholders::_1));
// Check if min tap/cotap pressure met yet
const float cotap_min_pressure = CotapMinPressure();
for (map<short, FingerState, kMaxTapFingers>::iterator it =
touched_.begin(), e = touched_.end();
it != e; ++it) {
const FingerState* fs = hwstate.GetFingerState((*it).first);
if (fs) {
if (fs->pressure >= immediate_interpreter_->tap_min_pressure())
min_tap_pressure_met_.insert(fs->tracking_id);
if (fs->pressure >= cotap_min_pressure) {
min_cotap_pressure_met_.insert(fs->tracking_id);
if ((*it).second.pressure < cotap_min_pressure) {
// Update existing record, since the old one hadn't met the cotap
// pressure
(*it).second = *fs;
}
}
stime_t finger_age = hwstate.timestamp -
immediate_interpreter_->finger_origin_timestamp(fs->tracking_id);
if (finger_age > immediate_interpreter_->tap_max_finger_age())
fingers_below_max_age_ = false;
}
}
}
void TapRecord::Clear() {
min_tap_pressure_met_.clear();
min_cotap_pressure_met_.clear();
t5r2_ = false;
t5r2_touched_size_ = 0;
t5r2_released_size_ = 0;
fingers_below_max_age_ = true;
touched_.clear();
released_.clear();
}
bool TapRecord::Moving(const HardwareState& hwstate,
const float dist_max) const {
const float cotap_min_pressure = CotapMinPressure();
for (map<short, FingerState, kMaxTapFingers>::const_iterator it =
touched_.begin(), e = touched_.end(); it != e; ++it) {
const FingerState* fs = hwstate.GetFingerState((*it).first);
if (!fs)
continue;
// Only look for moving when current frame meets cotap pressure and
// our history contains a contact that's met cotap pressure.
if (fs->pressure < cotap_min_pressure ||
(*it).second.pressure < cotap_min_pressure)
continue;
// Compute distance moved
float dist_x = fs->position_x - (*it).second.position_x;
float dist_y = fs->position_y - (*it).second.position_y;
// Respect WARP flags
if (fs->flags & GESTURES_FINGER_WARP_X_TAP_MOVE)
dist_x = 0.0;
if (fs->flags & GESTURES_FINGER_WARP_X_TAP_MOVE)
dist_y = 0.0;
bool moving =
dist_x * dist_x + dist_y * dist_y > dist_max * dist_max;
if (moving)
return true;
}
return false;
}
bool TapRecord::Motionless(const HardwareState& hwstate, const HardwareState&
prev_hwstate, const float max_speed) const {
const float cotap_min_pressure = CotapMinPressure();
for (map<short, FingerState, kMaxTapFingers>::const_iterator it =
touched_.begin(), e = touched_.end(); it != e; ++it) {
const FingerState* fs = hwstate.GetFingerState((*it).first);
const FingerState* prev_fs = prev_hwstate.GetFingerState((*it).first);
if (!fs || !prev_fs)
continue;
// Only look for moving when current frame meets cotap pressure and
// our history contains a contact that's met cotap pressure.
if (fs->pressure < cotap_min_pressure ||
prev_fs->pressure < cotap_min_pressure)
continue;
// Compute distance moved
if (DistSq(*fs, *prev_fs) > max_speed * max_speed)
return false;
}
return true;
}
bool TapRecord::TapBegan() const {
if (t5r2_)
return t5r2_touched_size_ > 0;
return !touched_.empty();
}
bool TapRecord::TapComplete() const {
bool ret = false;
if (t5r2_)
ret = t5r2_touched_size_ && t5r2_touched_size_ == t5r2_released_size_;
else
ret = !touched_.empty() && (touched_.size() == released_.size());
for (map<short, FingerState, kMaxTapFingers>::const_iterator
it = touched_.begin(), e = touched_.end(); it != e; ++it)
Log("TapRecord::TapComplete: touched_: %d", (*it).first);
for (set<short, kMaxTapFingers>::const_iterator it = released_.begin(),
e = released_.end(); it != e; ++it)
Log("TapRecord::TapComplete: released_: %d", *it);
return ret;
}
bool TapRecord::MinTapPressureMet() const {
// True if any touching finger met minimum pressure
return t5r2_ || !min_tap_pressure_met_.empty();
}
bool TapRecord::FingersBelowMaxAge() const {
return fingers_below_max_age_;
}
int TapRecord::TapType() const {
size_t touched_size =
t5r2_ ? t5r2_touched_size_ : min_cotap_pressure_met_.size();
int ret = GESTURES_BUTTON_LEFT;
if (touched_size > 1)
ret = GESTURES_BUTTON_RIGHT;
if (touched_size == 3 &&
immediate_interpreter_->three_finger_click_enable_.val_ &&
(!t5r2_ || immediate_interpreter_->t5r2_three_finger_click_enable_.val_))
ret = GESTURES_BUTTON_MIDDLE;
return ret;
}
// static
ScrollEvent ScrollEvent::Add(const ScrollEvent& evt_a,
const ScrollEvent& evt_b) {
ScrollEvent ret = { evt_a.dx + evt_b.dx,
evt_a.dy + evt_b.dy,
evt_a.dt + evt_b.dt };
return ret;
}
void ScrollEventBuffer::Insert(float dx, float dy, float dt) {
head_ = (head_ + max_size_ - 1) % max_size_;
buf_[head_].dx = dx;
buf_[head_].dy = dy;
buf_[head_].dt = dt;
size_ = std::min(size_ + 1, max_size_);
}
void ScrollEventBuffer::Clear() {
size_ = 0;
}
const ScrollEvent& ScrollEventBuffer::Get(size_t offset) const {
if (offset >= size_) {
Err("Out of bounds access!");
// avoid returning null pointer
static ScrollEvent dummy_event = { 0.0, 0.0, 0.0 };
return dummy_event;
}
return buf_[(head_ + offset) % max_size_];
}
void ScrollEventBuffer::GetSpeedSq(size_t num_events, float* dist_sq,
float* dt) const {
float dx = 0.0;
float dy = 0.0;
*dt = 0.0;
for (size_t i = 0; i < Size() && i < num_events; i++) {
const ScrollEvent& evt = Get(i);
dx += evt.dx;
dy += evt.dy;
*dt += evt.dt;
}
*dist_sq = dx * dx + dy * dy;
}
HardwareStateBuffer::HardwareStateBuffer(size_t size)
: states_(new HardwareState[size]),
newest_index_(0), size_(size), max_finger_cnt_(0) {
for (size_t i = 0; i < size_; i++) {
memset(&states_[i], 0, sizeof(HardwareState));
}
}
HardwareStateBuffer::~HardwareStateBuffer() {
for (size_t i = 0; i < size_; i++) {
delete[] states_[i].fingers;
}
}
void HardwareStateBuffer::Reset(size_t max_finger_cnt) {
max_finger_cnt_ = max_finger_cnt;
for (size_t i = 0; i < size_; i++) {
delete[] states_[i].fingers;
}
if (max_finger_cnt_) {
for (size_t i = 0; i < size_; i++) {
states_[i].fingers = new FingerState[max_finger_cnt_];
memset(states_[i].fingers, 0, sizeof(FingerState) * max_finger_cnt_);
}
} else {
for (size_t i = 0; i < size_; i++) {
states_[i].fingers = NULL;
}
}
}
void HardwareStateBuffer::PushState(const HardwareState& state) {
newest_index_ = (newest_index_ + size_ - 1) % size_;
Get(0)->DeepCopy(state, max_finger_cnt_);
}
void HardwareStateBuffer::PopState() {
newest_index_ = (newest_index_ + 1) % size_;
}
ScrollManager::ScrollManager(PropRegistry* prop_reg)
: prev_result_suppress_finger_movement_(false),
did_generate_scroll_(false),
max_stationary_move_speed_(prop_reg, "Max Stationary Move Speed", 0.0),
max_stationary_move_speed_hysteresis_(
prop_reg, "Max Stationary Move Speed Hysteresis", 0.0),
max_stationary_move_suppress_distance_(
prop_reg, "Max Stationary Move Suppress Distance", 1.0),
max_pressure_change_(prop_reg, "Max Allowed Pressure Change Per Sec",
800.0),
max_pressure_change_hysteresis_(prop_reg,
"Max Hysteresis Pressure Per Sec",
600.0),
max_pressure_change_duration_(prop_reg,
"Max Pressure Change Duration",
0.016),
max_stationary_speed_(prop_reg, "Max Finger Stationary Speed", 0.0),
vertical_scroll_snap_slope_(prop_reg, "Vertical Scroll Snap Slope",
tanf(DegToRad(50.0))), // 50 deg. from horiz.
horizontal_scroll_snap_slope_(prop_reg, "Horizontal Scroll Snap Slope",
tanf(DegToRad(30.0))),
fling_buffer_depth_(prop_reg, "Fling Buffer Depth", 10),
fling_buffer_suppress_zero_length_scrolls_(
prop_reg, "Fling Buffer Suppress Zero Length Scrolls", 1),
fling_buffer_min_avg_speed_(prop_reg,
"Fling Buffer Min Avg Speed",
10.0) {
}
bool ScrollManager::StationaryFingerPressureChangingSignificantly(
const HardwareStateBuffer& state_buffer,
const FingerState& current) const {
bool pressure_is_increasing = false;
bool pressure_direction_established = false;
const FingerState* prev = &current;
stime_t now = state_buffer.Get(0)->timestamp;
stime_t duration = 0.0;
if (max_pressure_change_duration_.val_ > 0.0) {
for (size_t i = 1; i < state_buffer.Size(); i++) {
const HardwareState& state = *state_buffer.Get(i);
stime_t local_duration = now - state.timestamp;
if (local_duration > max_pressure_change_duration_.val_)
break;
duration = local_duration;
const FingerState* fs = state.GetFingerState(current.tracking_id);
// If the finger just appeared, skip to check pressure change then
if (!fs)
break;
float pressure_difference = prev->pressure - fs->pressure;
if (pressure_difference) {
bool is_currently_increasing = pressure_difference > 0.0;
if (!pressure_direction_established) {
pressure_is_increasing = is_currently_increasing;
pressure_direction_established = true;
}
// If pressure changes are unstable, it's likely just noise.
if (is_currently_increasing != pressure_is_increasing)
return false;
}
prev = fs;
}
} else {
// To disable this feature, max_pressure_change_duration_ can be set to a
// negative number. When this occurs it reverts to just checking the last
// event, not looking through the backlog as well.
prev = state_buffer.Get(1)->GetFingerState(current.tracking_id);
duration = now - state_buffer.Get(1)->timestamp;
}
if (max_stationary_speed_.val_ != 0.0) {
// If finger moves too fast, we don't consider it stationary.
float dist_sq = (current.position_x - prev->position_x) *
(current.position_x - prev->position_x) +
(current.position_y - prev->position_y) *
(current.position_y - prev->position_y);
float dist_sq_thresh = duration * duration *
max_stationary_speed_.val_ * max_stationary_speed_.val_;
if (dist_sq > dist_sq_thresh)
return false;
}
float dp_thresh = duration *
(prev_result_suppress_finger_movement_ ?
max_pressure_change_hysteresis_.val_ :
max_pressure_change_.val_);
float dp = fabsf(current.pressure - prev->pressure);
return dp > dp_thresh;
}
bool ScrollManager::FillResultScroll(
const HardwareStateBuffer& state_buffer,
const FingerMap& prev_gs_fingers,
const FingerMap& gs_fingers,
GestureType prev_gesture_type,
const Gesture& prev_result,
Gesture* result,
ScrollEventBuffer* scroll_buffer) {
// For now, we take the movement of the biggest moving finger.
float max_mag_sq = 0.0; // square of max mag
float dx = 0.0;
float dy = 0.0;
bool stationary = true;
bool pressure_changing = false;
for (FingerMap::const_iterator it =
gs_fingers.begin(), e = gs_fingers.end(); it != e; ++it) {
const FingerState* fs = state_buffer.Get(0)->GetFingerState(*it);
const FingerState* prev = state_buffer.Get(1)->GetFingerState(*it);
if (!prev)
return false;
const stime_t dt =
state_buffer.Get(0)->timestamp - state_buffer.Get(1)->timestamp;
pressure_changing =
pressure_changing ||
StationaryFingerPressureChangingSignificantly(state_buffer, *fs);
// Call SuppressStationaryFingerMovement even if stationary is already true,
// because it records updates.
stationary =
SuppressStationaryFingerMovement(*fs, *prev, dt) &&
stationary;
float local_dx = fs->position_x - prev->position_x;
if (fs->flags & GESTURES_FINGER_WARP_X_NON_MOVE)
local_dx = 0.0;
float local_dy = fs->position_y - prev->position_y;
if (fs->flags & GESTURES_FINGER_WARP_Y_NON_MOVE)
local_dy = 0.0;
float local_max_mag_sq = local_dx * local_dx + local_dy * local_dy;
if (local_max_mag_sq > max_mag_sq) {
max_mag_sq = local_max_mag_sq;
dx = local_dx;
dy = local_dy;
}
}
// See if we should snap to vertical/horizontal
if (fabsf(dy) < horizontal_scroll_snap_slope_.val_ * fabsf(dx))
dy = 0.0; // snap to horizontal
else if (fabsf(dy) > vertical_scroll_snap_slope_.val_ * fabsf(dx))
dx = 0.0; // snap to vertical
prev_result_suppress_finger_movement_ = pressure_changing || stationary;
if (pressure_changing) {
// If we get here, it means that the pressure of the finger causing
// the scroll is changing a lot, so we don't trust it. It's likely
// leaving the touchpad. Normally we might just do nothing, but having
// a frame or two of 0 length scroll before a fling looks janky. We
// could also just start the fling now, but we don't want to do that
// because the fingers may not actually be leaving. What seems to work
// well is sort of dead-reckoning approach where we just repeat the
// scroll event from the previous input frame.
// Since this isn't a "real" scroll event, we don't put it into
// scroll_buffer_.
// Also, only use previous gesture if it's in the same direction.
if (prev_result.type == kGestureTypeScroll &&
prev_result.details.scroll.dy * dy >= 0 &&
prev_result.details.scroll.dx * dx >= 0) {
did_generate_scroll_ = true;
*result = prev_result;
}
return false;
}
if (stationary) {
scroll_buffer->Clear();
return false;
}
if (max_mag_sq > 0) {
did_generate_scroll_ = true;
*result = Gesture(kGestureScroll,
state_buffer.Get(1)->timestamp,
state_buffer.Get(0)->timestamp,
dx, dy);
}
if (prev_gesture_type != kGestureTypeScroll || prev_gs_fingers != gs_fingers)
scroll_buffer->Clear();
if (!fling_buffer_suppress_zero_length_scrolls_.val_ ||
!FloatEq(dx, 0.0) || !FloatEq(dy, 0.0))
scroll_buffer->Insert(
dx, dy,
state_buffer.Get(0)->timestamp - state_buffer.Get(1)->timestamp);
return true;
}
void ScrollManager::UpdateScrollEventBuffer(
GestureType gesture_type, ScrollEventBuffer* scroll_buffer) const {
if (gesture_type != kGestureTypeScroll)
scroll_buffer->Clear();
}
size_t ScrollManager::ScrollEventsForFlingCount(
const ScrollEventBuffer& scroll_buffer) const {
if (scroll_buffer.Size() <= 1)
return scroll_buffer.Size();
enum Direction { kNone, kUp, kDown, kLeft, kRight };
size_t i = 0;
Direction prev_direction = kNone;
size_t fling_buffer_depth = static_cast<size_t>(fling_buffer_depth_.val_);
for (; i < scroll_buffer.Size() && i < fling_buffer_depth; i++) {
const ScrollEvent& event = scroll_buffer.Get(i);
if (FloatEq(event.dx, 0.0) && FloatEq(event.dy, 0.0))
break;
Direction direction;
if (fabsf(event.dx) > fabsf(event.dy))
direction = event.dx > 0 ? kRight : kLeft;
else
direction = event.dy > 0 ? kDown : kUp;
if (i > 0 && direction != prev_direction)
break;
prev_direction = direction;
}
return i;
}
void ScrollManager::RegressScrollVelocity(
const ScrollEventBuffer& scroll_buffer, int count, ScrollEvent* out) const {
struct RegressionSums {
float tt_; // Cumulative sum of t^2.
float t_; // Cumulative sum of t.
float tx_; // Cumulative sum of t * x.
float ty_; // Cumulative sum of t * y.
float x_; // Cumulative sum of x.
float y_; // Cumulative sum of y.
};
out->dt = 1;
if (count <= 1) {
out->dx = 0;
out->dy = 0;
return;
}
RegressionSums sums = {0, 0, 0, 0, 0, 0};
float time = 0;
float x_coord = 0;
float y_coord = 0;
for (int i = count - 1; i >= 0; --i) {
const ScrollEvent& event = scroll_buffer.Get(i);
time += event.dt;
x_coord += event.dx;
y_coord += event.dy;
sums.tt_ += time * time;
sums.t_ += time;
sums.tx_ += time * x_coord;
sums.ty_ += time * y_coord;
sums.x_ += x_coord;
sums.y_ += y_coord;
}
// Note the regression determinant only depends on the values of t, and should
// never be zero so long as (1) count > 1, and (2) dt values are all non-zero.
float det = count * sums.tt_ - sums.t_ * sums.t_;
if (det) {
float det_inv = 1.0 / det;
out->dx = (count * sums.tx_ - sums.t_ * sums.x_) * det_inv;
out->dy = (count * sums.ty_ - sums.t_ * sums.y_) * det_inv;
} else {
out->dx = 0;
out->dy = 0;
}
}
bool ScrollManager::SuppressStationaryFingerMovement(const FingerState& fs,
const FingerState& prev,
stime_t dt) {
if (max_stationary_move_speed_.val_ <= 0.0 ||
max_stationary_move_suppress_distance_.val_ <= 0.0)
return false;
float dist_sq = DistSq(fs, prev);
// If speed exceeded, allow free movement and discard history
if (dist_sq > dt * dt *
max_stationary_move_speed_.val_ * max_stationary_move_speed_.val_) {
stationary_start_positions_.erase(fs.tracking_id);
return false;
}
if (dist_sq <= dt * dt *
max_stationary_move_speed_hysteresis_.val_ *
max_stationary_move_speed_hysteresis_.val_ &&
!MapContainsKey(stationary_start_positions_, fs.tracking_id)) {
// We assume that the first nearly-stationay event won't exceed the
// distance threshold and return from here.
Point point(fs.position_x, fs.position_y);
stationary_start_positions_[fs.tracking_id] = point;
return true;
}
if (!MapContainsKey(stationary_start_positions_, fs.tracking_id)) {
return false;
}
// Check if distance exceeded. If so, erase history and allow motion
float dx = fs.position_x - stationary_start_positions_[fs.tracking_id].x_;
float dy = fs.position_y - stationary_start_positions_[fs.tracking_id].y_;
if (dx * dx + dy * dy > max_stationary_move_suppress_distance_.val_ *
max_stationary_move_suppress_distance_.val_) {
stationary_start_positions_.erase(fs.tracking_id);
return false;
}
return true;
}
void ScrollManager::FillResultFling(const HardwareStateBuffer& state_buffer,
const ScrollEventBuffer& scroll_buffer,
Gesture* result) {
if (!did_generate_scroll_)
return;
ScrollEvent out = { 0.0, 0.0, 0.0 };
ScrollEvent zero = { 0.0, 0.0, 0.0 };
size_t count = 0;
// Make sure fling buffer met the minimum average speed for a fling.
float buf_dist_sq = 0.0;
float buf_dt = 0.0;
scroll_buffer.GetSpeedSq(fling_buffer_depth_.val_, &buf_dist_sq, &buf_dt);
if (fling_buffer_min_avg_speed_.val_ * fling_buffer_min_avg_speed_.val_ *
buf_dt * buf_dt > buf_dist_sq) {
out = zero;
goto done;
}
count = ScrollEventsForFlingCount(scroll_buffer);
if (count > scroll_buffer.Size()) {
Err("Too few events in scroll buffer");
out = zero;
goto done;
}
if (count < 2) {
if (count == 0)
out = zero;
else if (count == 1)
out = scroll_buffer.Get(0);
goto done;
}
// If we get here, count == 3 && scroll_buffer.Size() >= 3
RegressScrollVelocity(scroll_buffer, count, &out);
done:
float vx = out.dt ? (out.dx / out.dt) : 0.0;
float vy = out.dt ? (out.dy / out.dt) : 0.0;
*result = Gesture(kGestureFling,
state_buffer.Get(1)->timestamp,
state_buffer.Get(0)->timestamp,
vx,
vy,
GESTURES_FLING_START);
did_generate_scroll_ = false;
}
FingerButtonClick::FingerButtonClick(const ImmediateInterpreter* interpreter)
: interpreter_(interpreter),
fingers_(),
fingers_status_(),
num_fingers_(0),
num_recent_(0),
num_cold_(0),
num_hot_(0) {
}
bool FingerButtonClick::Update(const HardwareState& hwstate,
stime_t button_down_time) {
const float kMoveDistSq = interpreter_->button_move_dist_.val_ *
interpreter_->button_move_dist_.val_;
// Copy all fingers to an array, but leave out palms
num_fingers_ = 0;
for (int i = 0; i < hwstate.touch_cnt; ++i) {
const FingerState& fs = hwstate.fingers[i];
if (fs.flags & (GESTURES_FINGER_PALM | GESTURES_FINGER_POSSIBLE_PALM))
continue;
// we don't support more than 4 fingers
if (num_fingers_ >= 4)
return false;
fingers_[num_fingers_++] = &fs;
}
// Single finger is trivial
if (num_fingers_ <= 1)
return false;
// Sort fingers array by origin timestamp
FingerOriginCompare comparator(interpreter_);
std::sort(fingers_, fingers_ + num_fingers_, comparator);
// The status describes if a finger is recent (touched down recently),
// cold (touched down a while ago, but did not move) or hot (has moved).
// However thumbs are always forced to be "cold".
for (int i = 0; i < num_fingers_; ++i) {
stime_t finger_age =
button_down_time -
interpreter_->finger_origin_timestamp(fingers_[i]->tracking_id);
bool moving_finger =
SetContainsValue(interpreter_->moving_, fingers_[i]->tracking_id) ||
(interpreter_->DistanceTravelledSq(*fingers_[i], true) > kMoveDistSq);
if (!SetContainsValue(interpreter_->pointing_, fingers_[i]->tracking_id))
fingers_status_[i] = STATUS_COLD;
else if (moving_finger)
fingers_status_[i] = STATUS_HOT;
else if (finger_age < interpreter_->right_click_second_finger_age_.val_)
fingers_status_[i] = STATUS_RECENT;
else
fingers_status_[i] = STATUS_COLD;
}
num_recent_ = 0;
for (int i = 0; i < num_fingers_; ++i)
num_recent_ += (fingers_status_[i] == STATUS_RECENT);
num_cold_ = 0;
for (int i = 0; i < num_fingers_; ++i)
num_cold_ += (fingers_status_[i] == STATUS_COLD);
num_hot_ = num_fingers_ - num_recent_ - num_cold_;
return true;
}
int FingerButtonClick::GetButtonTypeForTouchCount(int touch_count) const {
if (touch_count == 2)
return GESTURES_BUTTON_RIGHT;
if (touch_count == 3 && interpreter_->three_finger_click_enable_.val_)
return GESTURES_BUTTON_MIDDLE;
return GESTURES_BUTTON_LEFT;
}
int FingerButtonClick::EvaluateTwoFingerButtonType() {
// Only one finger hot -> moving -> left click
if (num_hot_ == 1)
return GESTURES_BUTTON_LEFT;
float start_delta =
fabs(interpreter_->finger_origin_timestamp(fingers_[0]->tracking_id) -
interpreter_->finger_origin_timestamp(fingers_[1]->tracking_id));
// check if fingers are too close for a right click
const float kMin2fDistThreshSq =
interpreter_->tapping_finger_min_separation_.val_ *
interpreter_->tapping_finger_min_separation_.val_;
float dist_sq = DistSq(*fingers_[0], *fingers_[1]);
if (dist_sq < kMin2fDistThreshSq)
return GESTURES_BUTTON_LEFT;
// fingers touched down at approx the same time
if (start_delta < interpreter_->right_click_start_time_diff_.val_) {
// If two fingers are both very recent, it could either be a right-click
// or the left-click of one click-and-drag gesture. Our heuristic is that
// for real right-clicks, two finger's pressure should be roughly the same
// and they tend not be vertically aligned.
const FingerState* min_fs = NULL;
const FingerState* fs = NULL;
if (fingers_[0]->pressure < fingers_[1]->pressure)
min_fs = fingers_[0], fs = fingers_[1];
else
min_fs = fingers_[1], fs = fingers_[0];
float min_pressure = min_fs->pressure;
// It takes higher pressure for the bottom finger to trigger the physical
// click and people tend to place fingers more vertically so that they have
// enough space to drag the content with ease.
bool likely_click_drag =
(fs->pressure >
min_pressure +
interpreter_->click_drag_pressure_diff_thresh_.val_ &&
fs->pressure >
min_pressure *
interpreter_->click_drag_pressure_diff_factor_.val_ &&
fs->position_y > min_fs->position_y);
float xdist = fabsf(fs->position_x - min_fs->position_x);
float ydist = fabsf(fs->position_y - min_fs->position_y);
if (likely_click_drag &&
ydist >= xdist * interpreter_->click_drag_min_slope_.val_)
return GESTURES_BUTTON_LEFT;
return GESTURES_BUTTON_RIGHT;
}
// 1 finger is cold and in the dampened zone? Probably a thumb!
if (num_cold_ == 1 && interpreter_->FingerInDampenedZone(*fingers_[0]))
return GESTURES_BUTTON_LEFT;
// Close fingers -> same hand -> right click
// Fingers apart -> second hand finger or thumb -> left click
if (interpreter_->TwoFingersGesturing(*fingers_[0], *fingers_[1], true))
return GESTURES_BUTTON_RIGHT;
else
return GESTURES_BUTTON_LEFT;
}
int FingerButtonClick::EvaluateThreeOrMoreFingerButtonType() {
// Treat recent, ambiguous fingers as thumbs if they are in the dampened
// zone.
int num_dampened_recent = 0;
for (int i = num_fingers_ - num_recent_; i < num_fingers_; ++i)
num_dampened_recent += interpreter_->FingerInDampenedZone(*fingers_[i]);
// Re-use the 2f button type logic in case that all recent fingers are
// presumed thumbs because the recent fingers could be from thumb splits
// due to the increased pressure when doing a physical click and should be
// ignored.
if ((num_fingers_ - num_recent_ == 2) &&
(num_recent_ == num_dampened_recent))
return EvaluateTwoFingerButtonType();
// Only one finger hot with all others cold -> moving -> left click
if (num_hot_ == 1 && num_cold_ == num_fingers_ - 1)
return GESTURES_BUTTON_LEFT;
// A single recent touch, or a single cold touch (with all others being hot)
// could be a thumb or a second hand finger.
if (num_recent_ == 1 || (num_cold_ == 1 && num_hot_ == num_fingers_ - 1)) {
// The ambiguous finger is either the most recent one, or the only cold one.
const FingerState* ambiguous_finger = fingers_[num_fingers_ - 1];
if (num_recent_ != 1) {
for (int i = 0; i < num_fingers_; ++i) {
if (fingers_status_[i] == STATUS_COLD) {
ambiguous_finger = fingers_[i];
break;
}
}
}
// If it's in the dampened zone we will expect it to be a thumb.
// Otherwise it's a second hand finger
if (interpreter_->FingerInDampenedZone(*ambiguous_finger))
return GetButtonTypeForTouchCount(num_fingers_ - 1);
else
return GESTURES_BUTTON_LEFT;
}
// If all fingers are recent we can be sure they are from the same hand.
if (num_recent_ == num_fingers_) {
// Only if all fingers are in the same zone, we can be sure that none
// of them is a thumb.
Log("EvaluateThreeOrMoreFingerButtonType: Dampened: %d",
num_dampened_recent);
if (num_dampened_recent == 0 || num_dampened_recent == num_recent_)
return GetButtonTypeForTouchCount(num_recent_);
}
// To make a decision after this point we need to figure out if and how
// many of the fingers are grouped together. We do so by finding the pair
// of closest fingers, and then calculate where we expect the remaining
// fingers to be found.
// If they are not in the expected place, they will be called separate.
Log("EvaluateThreeOrMoreFingerButtonType: Falling back to location based "
"detection");
return EvaluateButtonTypeUsingFigureLocation();
}
int FingerButtonClick::EvaluateButtonTypeUsingFigureLocation() {
const float kMaxDistSq = interpreter_->button_max_dist_from_expected_.val_ *
interpreter_->button_max_dist_from_expected_.val_;
// Find pair with the closest distance
const FingerState* pair_a = NULL;
const FingerState* pair_b = NULL;
float pair_dist_sq = std::numeric_limits<float>::infinity();
for (int i = 0; i < num_fingers_; ++i) {
for (int j = 0; j < i; ++j) {
float dist_sq = DistSq(*fingers_[i], *fingers_[j]);
if (dist_sq < pair_dist_sq) {
pair_a = fingers_[i];
pair_b = fingers_[j];
pair_dist_sq = dist_sq;
}
}
}
int num_separate = 0;
const FingerState* last_separate = NULL;
if (interpreter_->metrics_->CloseEnoughToGesture(Vector2(*pair_a),
Vector2(*pair_b))) {
// Expect the remaining fingers to be next to the pair, all with the same
// distance from each other.
float dx = pair_b->position_x - pair_a->position_x;
float dy = pair_b->position_y - pair_a->position_y;
float expected1_x = pair_a->position_x + 2 * dx;
float expected1_y = pair_a->position_y + 2 * dy;
float expected2_x = pair_b->position_x - 2 * dx;
float expected2_y = pair_b->position_y - 2 * dy;
// Check if remaining fingers are close to the expected positions
for (int i = 0; i < num_fingers_; ++i) {
if (fingers_[i] == pair_a || fingers_[i] == pair_b)
continue;
float dist1_sq = DistSqXY(*fingers_[i], expected1_x, expected1_y);
float dist2_sq = DistSqXY(*fingers_[i], expected2_x, expected2_y);
if (dist1_sq > kMaxDistSq && dist2_sq > kMaxDistSq) {
num_separate++;
last_separate = fingers_[i];
}
}
} else {
// In case the pair is not close we have to fall back to using the
// dampened zone
Log("EvaluateButtonTypeUsingFigureLocation: Falling back to dampened zone "
"separation");
for (int i = 0; i < num_fingers_; ++i) {
if (interpreter_->FingerInDampenedZone(*fingers_[i])) {
num_separate++;
last_separate = fingers_[i];
}
}
}
// All fingers next to each other
if (num_separate == 0)
return GetButtonTypeForTouchCount(num_fingers_);
// The group with the last finger counts!
// Exception: If the separates have only one finger and it's a thumb
// count the other group
int num_pressing;
if (fingers_[num_fingers_ - 1] == last_separate &&
!(num_separate == 1 &&
interpreter_->FingerInDampenedZone(*last_separate))) {
num_pressing = num_separate;
} else {
num_pressing = num_fingers_ - num_separate;
}
Log("EvaluateButtonTypeUsingFigureLocation: Pressing: %d", num_pressing);
return GetButtonTypeForTouchCount(num_pressing);
}
ImmediateInterpreter::ImmediateInterpreter(PropRegistry* prop_reg,
Tracer* tracer)
: Interpreter(NULL, tracer, false),
button_type_(0),
finger_button_click_(this),
sent_button_down_(false),
button_down_timeout_(0.0),
started_moving_time_(-1.0),
gs_changed_time_(-1.0),
finger_leave_time_(-1.0),
moving_finger_id_(-1),
tap_to_click_state_(kTtcIdle),
tap_to_click_state_entered_(-1.0),
tap_record_(this),
last_movement_timestamp_(-1.0),
last_swipe_timestamp_(-1.0),
swipe_is_vertical_(false),
current_gesture_type_(kGestureTypeNull),
state_buffer_(8),
scroll_buffer_(20),
pinch_guess_start_(-1.0),
pinch_locked_(false),
pinch_status_(GESTURES_ZOOM_START),
pinch_prev_direction_(0),
pinch_prev_time_(-1.0),
finger_seen_shortly_after_button_down_(false),
scroll_manager_(prop_reg),
tap_enable_(prop_reg, "Tap Enable", true),
tap_paused_(prop_reg, "Tap Paused", false),
tap_timeout_(prop_reg, "Tap Timeout", 0.2),
inter_tap_timeout_(prop_reg, "Inter-Tap Timeout", 0.15),
tap_drag_delay_(prop_reg, "Tap Drag Delay", 0.05),
tap_drag_timeout_(prop_reg, "Tap Drag Timeout", 0.3),
tap_drag_enable_(prop_reg, "Tap Drag Enable", 0),
drag_lock_enable_(prop_reg, "Tap Drag Lock Enable", 0),
tap_drag_stationary_time_(prop_reg, "Tap Drag Stationary Time", 0),
tap_move_dist_(prop_reg, "Tap Move Distance", 2.0),
tap_min_pressure_(prop_reg, "Tap Minimum Pressure", 25.0),
tap_max_movement_(prop_reg, "Tap Maximum Movement", 0.0001),
tap_max_finger_age_(prop_reg, "Tap Maximum Finger Age", 1.2),
three_finger_click_enable_(prop_reg, "Three Finger Click Enable", 1),
zero_finger_click_enable_(prop_reg, "Zero Finger Click Enable", 1),
t5r2_three_finger_click_enable_(prop_reg,
"T5R2 Three Finger Click Enable",
0),
change_move_distance_(prop_reg, "Change Min Move Distance", 3.0),
move_lock_speed_(prop_reg, "Move Lock Speed", 10.0),
move_report_distance_(prop_reg, "Move Report Distance", 0.35),
change_timeout_(prop_reg, "Change Timeout", 0.2),
evaluation_timeout_(prop_reg, "Evaluation Timeout", 0.15),
pinch_evaluation_timeout_(prop_reg, "Pinch Evaluation Timeout", 0.1),
thumb_pinch_evaluation_timeout_(prop_reg,
"Thumb Pinch Evaluation Timeout", 0.25),
thumb_pinch_min_movement_(prop_reg,
"Thumb Pinch Minimum Movement", 0.8),
thumb_pinch_movement_ratio_(prop_reg, "Thumb Pinch Movement Ratio", 20),
thumb_slow_pinch_similarity_ratio_(prop_reg,
"Thumb Slow Pinch Similarity Ratio",
5),
thumb_pinch_delay_factor_(prop_reg, "Thumb Pinch Delay Factor", 9.0),
minimum_movement_direction_detection_(prop_reg,
"Minimum Movement Direction Detection", 0.003),
damp_scroll_min_movement_factor_(prop_reg,
"Damp Scroll Min Move Factor",
0.2),
two_finger_pressure_diff_thresh_(prop_reg,
"Two Finger Pressure Diff Thresh",
32.0),
two_finger_pressure_diff_factor_(prop_reg,
"Two Finger Pressure Diff Factor",
1.65),
click_drag_pressure_diff_thresh_(prop_reg,
"Click Drag Pressure Diff Thresh",
10.0),
click_drag_pressure_diff_factor_(prop_reg,
"Click Drag Pressure Diff Factor",
1.20),
click_drag_min_slope_(prop_reg, "Click Drag Min Slope", 2.22),
thumb_movement_factor_(prop_reg, "Thumb Movement Factor", 0.5),
thumb_speed_factor_(prop_reg, "Thumb Speed Factor", 0.5),
thumb_eval_timeout_(prop_reg, "Thumb Evaluation Timeout", 0.06),
thumb_pinch_threshold_ratio_(prop_reg,
"Thumb Pinch Threshold Ratio", 0.25),
thumb_click_prevention_timeout_(prop_reg,
"Thumb Click Prevention Timeout", 0.15),
two_finger_scroll_distance_thresh_(prop_reg,
"Two Finger Scroll Distance Thresh",
1.5),
two_finger_move_distance_thresh_(prop_reg,
"Two Finger Move Distance Thresh",
7.0),
three_finger_swipe_distance_thresh_(prop_reg,
"Three Finger Swipe Distance Thresh",
1.5),
four_finger_swipe_distance_thresh_(prop_reg,
"Four Finger Swipe Distance Thresh",
1.5),
three_finger_swipe_distance_ratio_(prop_reg,
"Three Finger Swipe Distance Ratio",
0.2),
four_finger_swipe_distance_ratio_(prop_reg,
"Four Finger Swipe Distance Ratio",
0.2),
three_finger_swipe_enable_(prop_reg, "Three Finger Swipe EnableX", 1),
bottom_zone_size_(prop_reg, "Bottom Zone Size", 10.0),
button_evaluation_timeout_(prop_reg, "Button Evaluation Timeout", 0.05),
button_finger_timeout_(prop_reg, "Button Finger Timeout", 0.03),
button_move_dist_(prop_reg, "Button Move Distance", 10.0),
button_max_dist_from_expected_(prop_reg,
"Button Max Distance From Expected", 20.0),
button_right_click_zone_enable_(prop_reg,
"Button Right Click Zone Enable", 0),
button_right_click_zone_size_(prop_reg,
"Button Right Click Zone Size", 20.0),
keyboard_touched_timeval_high_(prop_reg, "Keyboard Touched Timeval High",
0),
keyboard_touched_timeval_low_(prop_reg, "Keyboard Touched Timeval Low",
0, this),
keyboard_touched_(0.0),
keyboard_palm_prevent_timeout_(prop_reg, "Keyboard Palm Prevent Timeout",
0.5),
motion_tap_prevent_timeout_(prop_reg, "Motion Tap Prevent Timeout",
0.05),
tapping_finger_min_separation_(prop_reg, "Tap Min Separation", 10.0),
pinch_noise_level_sq_(prop_reg, "Pinch Noise Level Squared", 2.0),
pinch_guess_min_movement_(prop_reg, "Pinch Guess Minimum Movement", 2.0),
pinch_thumb_min_movement_(prop_reg,
"Pinch Thumb Minimum Movement", 1.41),
pinch_certain_min_movement_(prop_reg,
"Pinch Certain Minimum Movement", 8.0),
inward_pinch_min_angle_(prop_reg, "Inward Pinch Minimum Angle", 0.3),
pinch_zoom_max_angle_(prop_reg, "Pinch Zoom Maximum Angle", -0.4),
scroll_min_angle_(prop_reg, "Scroll Minimum Angle", -0.2),
pinch_guess_consistent_mov_ratio_(prop_reg,
"Pinch Guess Consistent Movement Ratio", 0.4),
pinch_zoom_min_events_(prop_reg, "Pinch Zoom Minimum Events", 3),
pinch_initial_scale_time_inv_(prop_reg,
"Pinch Initial Scale Time Inverse",
3.33),
pinch_res_(prop_reg, "Minimum Pinch Scale Resolution Squared", 1.005),
pinch_stationary_res_(prop_reg,
"Stationary Pinch Scale Resolution Squared",
1.05),
pinch_stationary_time_(prop_reg,
"Stationary Pinch Time",
0.10),
pinch_hysteresis_res_(prop_reg,
"Hysteresis Pinch Scale Resolution Squared",
1.05),
pinch_enable_(prop_reg, "Pinch Enable", 1),
right_click_start_time_diff_(prop_reg,
"Right Click Start Time Diff Thresh",
0.1),
right_click_second_finger_age_(prop_reg,
"Right Click Second Finger Age Thresh",
0.5),
quick_acceleration_factor_(prop_reg, "Quick Acceleration Factor", 0.0) {
InitName();
requires_metrics_ = true;
}
void ImmediateInterpreter::SyncInterpretImpl(HardwareState* hwstate,
stime_t* timeout) {
if (!state_buffer_.Get(0)->fingers) {
Err("Must call SetHardwareProperties() before Push().");
return;
}
state_buffer_.PushState(*hwstate);
FillOriginInfo(*hwstate);
result_.type = kGestureTypeNull;
const bool same_fingers = state_buffer_.Get(1)->SameFingersAs(*hwstate) &&
(hwstate->buttons_down == state_buffer_.Get(1)->buttons_down);
if (!same_fingers) {
// Fingers changed, do nothing this time
FingerMap new_gs_fingers =
SetSubtract(GetGesturingFingers(*hwstate), non_gs_fingers_);
ResetSameFingersState(*hwstate);
FillStartPositions(*hwstate);
if (pinch_enable_.val_ &&
(hwstate->finger_cnt <= 2 || new_gs_fingers.size() != 2)) {
// Release the zoom lock
UpdatePinchState(*hwstate, true, new_gs_fingers);
}
moving_finger_id_ = -1;
}
if (hwstate->finger_cnt < state_buffer_.Get(1)->finger_cnt &&
AnyGesturingFingerLeft(*hwstate, prev_active_gs_fingers_)) {
finger_leave_time_ = hwstate->timestamp;
}
// Check if clock changed backwards
if (hwstate->timestamp < state_buffer_.Get(1)->timestamp)
ResetTime();
UpdatePointingFingers(*hwstate);
UpdateThumbState(*hwstate);
FingerMap newly_moving_fingers = UpdateMovingFingers(*hwstate);
UpdateNonGsFingers(*hwstate);
FingerMap gs_fingers =
SetSubtract(GetGesturingFingers(*hwstate), non_gs_fingers_);
if (gs_fingers != prev_gs_fingers_)
gs_changed_time_ = hwstate->timestamp;
UpdateStartedMovingTime(hwstate->timestamp, gs_fingers, newly_moving_fingers);
UpdateButtons(*hwstate, timeout);
UpdateTapGesture(hwstate,
gs_fingers,
same_fingers,
hwstate->timestamp,
timeout);
FingerMap active_gs_fingers;
UpdateCurrentGestureType(*hwstate, gs_fingers, &active_gs_fingers);
GenerateFingerLiftGesture();
if (result_.type == kGestureTypeNull)
FillResultGesture(*hwstate, active_gs_fingers);
// Prevent moves while in a tap
if ((tap_to_click_state_ == kTtcFirstTapBegan ||
tap_to_click_state_ == kTtcSubsequentTapBegan) &&
result_.type == kGestureTypeMove)
result_.type = kGestureTypeNull;
prev_active_gs_fingers_ = active_gs_fingers;
prev_gs_fingers_ = gs_fingers;
prev_result_ = result_;
prev_gesture_type_ = current_gesture_type_;
if (result_.type != kGestureTypeNull) {
non_gs_fingers_ = SetSubtract(gs_fingers, active_gs_fingers);
ProduceGesture(result_);
}
}
void ImmediateInterpreter::HandleTimerImpl(stime_t now, stime_t* timeout) {
result_.type = kGestureTypeNull;
// Tap-to-click always aborts when real button(s) are being used, so we
// don't need to worry about conflicts with these two types of callback.
UpdateButtonsTimeout(now);
UpdateTapGesture(NULL,
FingerMap(),
false,
now,
timeout);
if (result_.type != kGestureTypeNull)
ProduceGesture(result_);
}
void ImmediateInterpreter::FillOriginInfo(
const HardwareState& hwstate) {
RemoveMissingIdsFromMap(&origin_timestamps_, hwstate);
RemoveMissingIdsFromMap(&distance_walked_, hwstate);
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState& fs = hwstate.fingers[i];
if (MapContainsKey(origin_timestamps_, fs.tracking_id) &&
state_buffer_.Size() > 1 &&
state_buffer_.Get(1)->GetFingerState(fs.tracking_id)) {
float delta_x = hwstate.GetFingerState(fs.tracking_id)->position_x -
state_buffer_.Get(1)->GetFingerState(fs.tracking_id)->position_x;
float delta_y = hwstate.GetFingerState(fs.tracking_id)->position_y -
state_buffer_.Get(1)->GetFingerState(fs.tracking_id)->position_y;
distance_walked_[fs.tracking_id] += sqrtf(delta_x * delta_x +
delta_y * delta_y);
continue;
}
origin_timestamps_[fs.tracking_id] = hwstate.timestamp;
distance_walked_[fs.tracking_id] = 0.0;
}
}
void ImmediateInterpreter::ResetSameFingersState(const HardwareState& hwstate) {
pointing_.clear();
fingers_.clear();
start_positions_.clear();
three_finger_swipe_start_positions_.clear();
four_finger_swipe_start_positions_.clear();
scroll_manager_.ResetSameFingerState();
RemoveMissingIdsFromSet(&moving_, hwstate);
changed_time_ = hwstate.timestamp;
}
void ImmediateInterpreter::ResetTime() {
started_moving_time_ = -1.0;
gs_changed_time_ = -1.0;
finger_leave_time_ = -1.0;
tap_to_click_state_entered_ = -1.0;
last_movement_timestamp_ = -1.0;
last_swipe_timestamp_ = -1.0;
pinch_guess_start_ = -1.0;
pinch_prev_time_ = -1.0;
}
void ImmediateInterpreter::UpdatePointingFingers(const HardwareState& hwstate) {
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
if (hwstate.fingers[i].flags & GESTURES_FINGER_PALM)
pointing_.erase(hwstate.fingers[i].tracking_id);
else
pointing_.insert(hwstate.fingers[i].tracking_id);
}
fingers_ = pointing_;
}
float ImmediateInterpreter::DistanceTravelledSq(const FingerState& fs,
bool origin,
bool permit_warp) const {
Point delta = FingerTraveledVector(fs, origin, permit_warp);
return delta.x_ * delta.x_ + delta.y_ * delta.y_;
}
Point ImmediateInterpreter::FingerTraveledVector(
const FingerState& fs, bool origin, bool permit_warp) const {
const map<short, Point, kMaxFingers>* positions;
if (origin)
positions = &origin_positions_;
else
positions = &start_positions_;
if (!MapContainsKey(*positions, fs.tracking_id))
return Point(0.0f, 0.0f);
const Point& start = (*positions)[fs.tracking_id];
float dx = fs.position_x - start.x_;
float dy = fs.position_y - start.y_;
bool suppress_move =
(!permit_warp || (fs.flags & GESTURES_FINGER_WARP_TELEPORTATION));
if ((fs.flags & GESTURES_FINGER_WARP_X) && suppress_move)
dx = 0;
if ((fs.flags & GESTURES_FINGER_WARP_Y) && suppress_move)
dy = 0;
return Point(dx, dy);
}
bool ImmediateInterpreter::EarlyZoomPotential(const HardwareState& hwstate)
const {
if (fingers_.size() != 2)
return false;
int id1 = *(fingers_.begin());
int id2 = *(fingers_.begin() + 1);
const FingerState* finger1 = hwstate.GetFingerState(id1);
const FingerState* finger2 = hwstate.GetFingerState(id2);
float pinch_eval_timeout = pinch_evaluation_timeout_.val_;
if (finger1 == NULL || finger2 == NULL)
return false;
// Wait for a longer time if fingers arrived together
if (fabs(origin_timestamps_[id1] - origin_timestamps_[id2]) <
evaluation_timeout_.val_ &&
hwstate.timestamp -
max(origin_timestamps_[id1], origin_timestamps_[id2]) <
thumb_pinch_evaluation_timeout_.val_ * thumb_pinch_delay_factor_.val_)
pinch_eval_timeout *= thumb_pinch_delay_factor_.val_;
bool early_decision = hwstate.timestamp -
min(origin_timestamps_[finger1->tracking_id],
origin_timestamps_[finger2->tracking_id]) <
pinch_eval_timeout;
// Avoid extra computation if it's too late for a pinch zoom
if (!early_decision &&
hwstate.timestamp - origin_timestamps_[finger1->tracking_id] >
thumb_pinch_evaluation_timeout_.val_)
return false;
float walked_distance1 = distance_walked_[finger1->tracking_id];
float walked_distance2 = distance_walked_[finger2->tracking_id];
if (walked_distance1 > walked_distance2)
std::swap(walked_distance1,walked_distance2);
if ((walked_distance1 > thumb_pinch_min_movement_.val_ ||
hwstate.timestamp - origin_timestamps_[finger1->tracking_id] >
thumb_pinch_evaluation_timeout_.val_) &&
walked_distance1 > 0 &&
walked_distance2 / walked_distance1 > thumb_pinch_movement_ratio_.val_)
return false;
bool motionless_cycles = false;
for (int i = 1;
i < min<int>(state_buffer_.Size(), pinch_zoom_min_events_.val_); i++) {
const FingerState* curr1 = state_buffer_.Get(i - 1)->GetFingerState(id1);
const FingerState* curr2 = state_buffer_.Get(i - 1)->GetFingerState(id2);
const FingerState* prev1 = state_buffer_.Get(i)->GetFingerState(id1);
const FingerState* prev2 = state_buffer_.Get(i)->GetFingerState(id2);
if (!curr1 || !curr2 || !prev1 || !prev2) {
motionless_cycles = true;
break;
}
bool finger1_moved = (curr1->position_x - prev1->position_x) != 0 ||
(curr1->position_y - prev1->position_y) != 0;
bool finger2_moved = (curr2->position_x - prev2->position_x) != 0 ||
(curr2->position_y - prev2->position_y) != 0;
if (!finger1_moved && !finger2_moved) {
motionless_cycles = true;
break;
}
}
if (motionless_cycles > 0 && early_decision)
return true;
Point delta1 = FingerTraveledVector(*finger1, true, true);
Point delta2 = FingerTraveledVector(*finger2, true, true);
float dot = delta1.x_ * delta2.x_ + delta1.y_ * delta2.y_;
if ((pinch_guess_start_ > 0 || dot < 0) && early_decision)
return true;
if (origin_timestamps_[finger1->tracking_id] -
origin_timestamps_[finger2->tracking_id] < evaluation_timeout_.val_ &&
origin_timestamps_[finger2->tracking_id] -
origin_timestamps_[finger1->tracking_id] < evaluation_timeout_.val_ &&
hwstate.timestamp - origin_timestamps_[finger1->tracking_id] <
thumb_pinch_evaluation_timeout_.val_)
return true;
return false;
}
bool ImmediateInterpreter::ZoomFingersAreConsistent(
const HardwareStateBuffer& state_buffer) const {
if (fingers_.size() != 2)
return false;
int id1 = *(fingers_.begin());
int id2 = *(fingers_.begin() + 1);
const FingerState* curr1 = state_buffer.Get(min<int>(state_buffer.Size() - 1,
pinch_zoom_min_events_.val_))->GetFingerState(id1);
const FingerState* curr2 = state_buffer.Get(min<int>(state_buffer.Size() - 1,
pinch_zoom_min_events_.val_))->GetFingerState(id2);
if (!curr1 || !curr2)
return false;
for (int i = 0;
i < min<int>(state_buffer.Size(), pinch_zoom_min_events_.val_); i++) {
const FingerState* prev1 = state_buffer.Get(i)->GetFingerState(id1);
const FingerState* prev2 = state_buffer.Get(i)->GetFingerState(id2);
if (!prev1 || !prev2)
return false;
float dot = FingersAngle(prev1, prev2, curr1, curr2);
if (dot >= 0)
return false;
}
const FingerState* last1 = state_buffer.Get(0)->GetFingerState(id1);
const FingerState* last2 = state_buffer.Get(0)->GetFingerState(id2);
float angle = FingersAngle(last1, last2, curr1, curr2);
if (angle > pinch_zoom_max_angle_.val_)
return false;
return true;
}
bool ImmediateInterpreter::InwardPinch(
const HardwareStateBuffer& state_buffer, const FingerState& fs) const {
if (fingers_.size() != 2)
return false;
int id = fs.tracking_id;
const FingerState* curr =
state_buffer.Get(min<int>(state_buffer.Size(),
pinch_zoom_min_events_.val_))->GetFingerState(id);
if (!curr)
return false;
for (int i = 0;
i < min<int>(state_buffer.Size(), pinch_zoom_min_events_.val_); i++) {
const FingerState* prev = state_buffer.Get(i)->GetFingerState(id);
if (!prev)
return false;
float dot = (curr->position_y - prev->position_y);
if (dot <= 0)
return false;
}
const FingerState* last = state_buffer.Get(0)->GetFingerState(id);
float dot_last = (curr->position_y - last->position_y);
float size_last = sqrt((curr->position_x - last->position_x) *
(curr->position_x - last->position_x) +
(curr->position_y - last->position_y) *
(curr->position_y - last->position_y));
float angle = dot_last / size_last;
if (angle < inward_pinch_min_angle_.val_)
return false;
return true;
}
float ImmediateInterpreter::FingersAngle(const FingerState* prev1,
const FingerState* prev2,
const FingerState* curr1,
const FingerState* curr2) const {
float dot_last = (curr1->position_x - prev1->position_x) *
(curr2->position_x - prev2->position_x) +
(curr1->position_y - prev1->position_y) *
(curr2->position_y - prev2->position_y);
float size_last1_sq = (curr1->position_x - prev1->position_x) *
(curr1->position_x - prev1->position_x) +
(curr1->position_y - prev1->position_y) *
(curr1->position_y - prev1->position_y);
float size_last2_sq = (curr2->position_x - prev2->position_x) *
(curr2->position_x - prev2->position_x) +
(curr2->position_y - prev2->position_y) *
(curr2->position_y - prev2->position_y);
float overall_size = sqrt(size_last1_sq * size_last2_sq);
// If one of the two vectors is too small, return 0.
if (overall_size < minimum_movement_direction_detection_.val_ *
minimum_movement_direction_detection_.val_)
return 0.0;
return dot_last / overall_size;
}
bool ImmediateInterpreter::ScrollAngle(const FingerState& finger1,
const FingerState& finger2) {
const FingerState* curr1 = state_buffer_.Get(
min<int>(state_buffer_.Size() - 1, 3))->
GetFingerState(finger1.tracking_id);
const FingerState* curr2 = state_buffer_.Get(
min<int>(state_buffer_.Size() - 1, 3))->
GetFingerState(finger2.tracking_id);
const FingerState* last1 =
state_buffer_.Get(0)->GetFingerState(finger1.tracking_id);
const FingerState* last2 =
state_buffer_.Get(0)->GetFingerState(finger2.tracking_id);
if (last1 && last2 && curr1 && curr2) {
if (FingersAngle(last1, last2, curr1, curr2) < scroll_min_angle_.val_)
return false;
}
return true;
}
float ImmediateInterpreter::TwoFingerDistanceSq(
const HardwareState& hwstate) const {
if (fingers_.size() == 2) {
return TwoSpecificFingerDistanceSq(hwstate, fingers_);
} else {
return -1;
}
}
float ImmediateInterpreter::TwoSpecificFingerDistanceSq(
const HardwareState& hwstate, const FingerMap& fingers) const {
if (fingers.size() == 2) {
const FingerState* finger_a = hwstate.GetFingerState(*fingers.begin());
const FingerState* finger_b = hwstate.GetFingerState(
*(fingers.begin() + 1));
if (finger_a == NULL || finger_b == NULL) {
Err("Finger unexpectedly NULL");
return -1;
}
return DistSq(*finger_a, *finger_b);
} else if (hwstate.finger_cnt == 2) {
return DistSq(hwstate.fingers[0], hwstate.fingers[1]);
} else {
return -1;
}
}
// Updates thumb_ below.
void ImmediateInterpreter::UpdateThumbState(const HardwareState& hwstate) {
// Remove old ids from thumb_
RemoveMissingIdsFromMap(&thumb_, hwstate);
RemoveMissingIdsFromMap(&thumb_eval_timer_, hwstate);
float min_pressure = INFINITY;
const FingerState* min_fs = NULL;
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState& fs = hwstate.fingers[i];
if (fs.flags & GESTURES_FINGER_PALM)
continue;
if (fs.pressure < min_pressure) {
min_pressure = fs.pressure;
min_fs = &fs;
}
}
if (!min_fs) {
// Only palms on the touchpad
return;
}
// We respect warp flags only if we really have little information of the
// finger positions and not just because we want to suppress unintentional
// cursor moves. See the definition of GESTURES_FINGER_WARP_TELEPORTATION
// for more detail.
bool min_warp_move = (min_fs->flags & GESTURES_FINGER_WARP_TELEPORTATION) &&
((min_fs->flags & GESTURES_FINGER_WARP_X_MOVE) ||
(min_fs->flags & GESTURES_FINGER_WARP_Y_MOVE));
float min_dist_sq = DistanceTravelledSq(*min_fs, false, true);
float min_dt = hwstate.timestamp -
origin_timestamps_[min_fs->tracking_id];
float thumb_dist_sq_thresh = min_dist_sq *
thumb_movement_factor_.val_ * thumb_movement_factor_.val_;
float thumb_speed_sq_thresh = min_dist_sq *
thumb_speed_factor_.val_ * thumb_speed_factor_.val_;
// Make all large-pressure, less moving contacts located below the
// min-pressure contact as thumbs.
bool similar_movement = false;
if (pinch_enable_.val_ && hwstate.finger_cnt == 2) {
float dt1 = hwstate.timestamp -
origin_timestamps_[hwstate.fingers[0].tracking_id];
float dist_sq1 = DistanceTravelledSq(hwstate.fingers[0], true, true);
float dt2 = hwstate.timestamp -
origin_timestamps_[hwstate.fingers[1].tracking_id];
float dist_sq2 = DistanceTravelledSq(hwstate.fingers[1], true, true);
if (dist_sq1 * dt1 && dist_sq2 * dt2)
similar_movement = max((dist_sq1 * dt1 * dt1) / (dist_sq2 * dt2 * dt2),
(dist_sq2 * dt2 * dt2) / (dist_sq1 * dt1 * dt1)) <
thumb_slow_pinch_similarity_ratio_.val_;
else
similar_movement = false;
}
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState& fs = hwstate.fingers[i];
if (fs.flags & GESTURES_FINGER_PALM)
continue;
if (pinch_enable_.val_ && InwardPinch(state_buffer_, fs)) {
thumb_speed_sq_thresh *= thumb_pinch_threshold_ratio_.val_;
thumb_dist_sq_thresh *= thumb_pinch_threshold_ratio_.val_;
}
float dist_sq = DistanceTravelledSq(fs, false, true);
float dt = hwstate.timestamp - origin_timestamps_[fs.tracking_id];
bool closer_to_origin = dist_sq <= thumb_dist_sq_thresh;
bool slower_moved = (dist_sq * min_dt &&
dist_sq * min_dt * min_dt <
thumb_speed_sq_thresh * dt * dt);
bool relatively_motionless = closer_to_origin || slower_moved;
bool likely_thumb =
(fs.pressure > min_pressure + two_finger_pressure_diff_thresh_.val_ &&
fs.pressure > min_pressure * two_finger_pressure_diff_factor_.val_ &&
fs.position_y > min_fs->position_y);
bool non_gs = (hwstate.timestamp > changed_time_ &&
(prev_active_gs_fingers_.find(fs.tracking_id) ==
prev_active_gs_fingers_.end()) &&
prev_result_.type != kGestureTypeNull);
non_gs |= moving_finger_id_ >= 0 && moving_finger_id_ != fs.tracking_id;
likely_thumb |= non_gs;
// We sometimes can't decide the thumb state if some fingers are undergoing
// warp moves as the decision could be off (DistanceTravelledSq may
// under-estimate the real distance). The cases that we need to re-evaluate
// the thumb in the next frame are:
// 1. Both fingers warp.
// 2. Min-pressure finger warps and relatively_motionless is false.
// 3. Thumb warps and relatively_motionless is true.
bool warp_move = (fs.flags & GESTURES_FINGER_WARP_TELEPORTATION) &&
((fs.flags & GESTURES_FINGER_WARP_X_MOVE) ||
(fs.flags & GESTURES_FINGER_WARP_Y_MOVE));
if (likely_thumb &&
((warp_move && min_warp_move) ||
(!warp_move && min_warp_move && !relatively_motionless) ||
(warp_move && !min_warp_move && relatively_motionless))) {
continue;
}
likely_thumb &= relatively_motionless;
if (MapContainsKey(thumb_, fs.tracking_id)) {
// Beyond the evaluation period. Stick to being thumbs.
if (thumb_eval_timer_[fs.tracking_id] <= 0.0) {
if (!pinch_enable_.val_ || hwstate.finger_cnt == 1)
continue;
bool slow_pinch_guess =
dist_sq * min_dt * min_dt / (thumb_speed_sq_thresh * dt * dt) >
thumb_pinch_min_movement_.val_ &&
similar_movement;
bool might_be_pinch = (slow_pinch_guess &&
hwstate.timestamp -
origin_timestamps_[fs.tracking_id] < 2 *
thumb_pinch_evaluation_timeout_.val_ &&
ZoomFingersAreConsistent(state_buffer_));
if (relatively_motionless ||
hwstate.timestamp - origin_timestamps_[fs.tracking_id] >
thumb_pinch_evaluation_timeout_.val_) {
if (!might_be_pinch)
continue;
else
likely_thumb = false;
}
}
// Finger is still under evaluation.
if (likely_thumb) {
// Decrease the timer as the finger is thumb-like in the previous
// frame.
const FingerState* prev =
state_buffer_.Get(1)->GetFingerState(fs.tracking_id);
if (!prev)
continue;
thumb_eval_timer_[fs.tracking_id] -=
hwstate.timestamp - state_buffer_.Get(1)->timestamp;
} else {
// The finger wasn't thumb-like in the frame. Remove it from the thumb
// list.
thumb_.erase(fs.tracking_id);
thumb_eval_timer_.erase(fs.tracking_id);
}
} else if (likely_thumb) {
// Finger is thumb-like, so we add it to the list.
thumb_[fs.tracking_id] = hwstate.timestamp;
thumb_eval_timer_[fs.tracking_id] = thumb_eval_timeout_.val_;
}
}
for (map<short, stime_t, kMaxFingers>::const_iterator it = thumb_.begin();
it != thumb_.end(); ++it)
pointing_.erase((*it).first);
}
void ImmediateInterpreter::UpdateNonGsFingers(const HardwareState& hwstate) {
RemoveMissingIdsFromSet(&non_gs_fingers_, hwstate);
// moving fingers may be gesturing, so take them out from the set.
non_gs_fingers_ = SetSubtract(non_gs_fingers_, moving_);
}
bool ImmediateInterpreter::KeyboardRecentlyUsed(stime_t now) const {
// For tests, values of 0 mean keyboard not used recently.
if (keyboard_touched_ == 0.0)
return false;
// Sanity check. If keyboard_touched_ is more than 10 seconds away from now,
// ignore it.
if (fabs(now - keyboard_touched_) > 10)
return false;
return keyboard_touched_ + keyboard_palm_prevent_timeout_.val_ > now;
}
namespace {
struct GetGesturingFingersCompare {
// Returns true if finger_a is strictly closer to keyboard than finger_b
bool operator()(const FingerState* finger_a, const FingerState* finger_b) {
return finger_a->position_y < finger_b->position_y;
}
};
} // namespace {}
FingerMap ImmediateInterpreter::GetGesturingFingers(
const HardwareState& hwstate) const {
// We support up to kMaxGesturingFingers finger gestures
if (pointing_.size() <= kMaxGesturingFingers)
return pointing_;
const FingerState* fs[hwstate.finger_cnt];
for (size_t i = 0; i < hwstate.finger_cnt; ++i)
fs[i] = &hwstate.fingers[i];
// Pull the kMaxSize FingerStates w/ the lowest position_y to the
// front of fs[].
GetGesturingFingersCompare compare;
FingerMap ret;
size_t sorted_cnt;
if (hwstate.finger_cnt > kMaxGesturingFingers) {
std::partial_sort(fs, fs + kMaxGesturingFingers, fs + hwstate.finger_cnt,
compare);
sorted_cnt = kMaxGesturingFingers;
} else {
std::sort(fs, fs + hwstate.finger_cnt, compare);
sorted_cnt = hwstate.finger_cnt;
}
for (size_t i = 0; i < sorted_cnt; i++)
ret.insert(fs[i]->tracking_id);
return ret;
}
void ImmediateInterpreter::UpdateCurrentGestureType(
const HardwareState& hwstate,
const FingerMap& gs_fingers,
FingerMap* active_gs_fingers) {
*active_gs_fingers = gs_fingers;
size_t num_gesturing = gs_fingers.size();
// Physical button or tap overrides current gesture state
if (sent_button_down_ || tap_to_click_state_ == kTtcDrag) {
current_gesture_type_ = kGestureTypeMove;
return;
}
// current gesture state machine
switch (current_gesture_type_) {
case kGestureTypeContactInitiated:
case kGestureTypeButtonsChange:
break;
case kGestureTypeScroll:
case kGestureTypeSwipe:
case kGestureTypeFourFingerSwipe:
case kGestureTypeSwipeLift:
case kGestureTypeFourFingerSwipeLift:
case kGestureTypeFling:
case kGestureTypeMove:
case kGestureTypeNull:
// When a finger leaves, we hold the gesture processing for
// change_timeout_ time.
if (hwstate.timestamp < finger_leave_time_ + change_timeout_.val_) {
current_gesture_type_ = kGestureTypeNull;
return;
}
// Scrolling detection for T5R2 devices
if ((hwprops_->supports_t5r2 || hwprops_->support_semi_mt) &&
(hwstate.touch_cnt > 2)) {
current_gesture_type_ = kGestureTypeScroll;
return;
}
// Finger gesture decision process
if (num_gesturing == 0) {
current_gesture_type_ = kGestureTypeNull;
} else if (num_gesturing == 1) {
const FingerState* finger =
hwstate.GetFingerState(*gs_fingers.begin());
if (PalmIsArrivingOrDeparting(*finger))
current_gesture_type_ = kGestureTypeNull;
else
current_gesture_type_ = kGestureTypeMove;
} else {
if (changed_time_ > started_moving_time_ ||
hwstate.timestamp - max(started_moving_time_, gs_changed_time_) <
evaluation_timeout_.val_ ||
current_gesture_type_ == kGestureTypeNull) {
// Try to recognize gestures, starting from many-finger gestures
// first. We choose this order b/c 3-finger gestures are very strict
// in their interpretation.
vector<short, kMaxGesturingFingers> sorted_ids;
SortFingersByProximity(gs_fingers, hwstate, &sorted_ids);
for (; sorted_ids.size() >= 2;
sorted_ids.erase(sorted_ids.end() - 1)) {
if (sorted_ids.size() == 2) {
GestureType new_gs_type = kGestureTypeNull;
const FingerState* fingers[] = {
hwstate.GetFingerState(*sorted_ids.begin()),
hwstate.GetFingerState(*(sorted_ids.begin() + 1))
};
if (!fingers[0] || !fingers[1]) {
Err("Unable to find gesturing fingers!");
return;
}
// See if two pointers are close together
bool potential_two_finger_gesture =
TwoFingersGesturing(*fingers[0], *fingers[1], false);
if (!potential_two_finger_gesture) {
new_gs_type = kGestureTypeMove;
} else {
new_gs_type =
GetTwoFingerGestureType(*fingers[0], *fingers[1]);
// Two fingers that don't end up causing scroll may be
// ambiguous. Only move if they've been down long enough.
if (new_gs_type == kGestureTypeMove &&
hwstate.timestamp -
min(origin_timestamps_[fingers[0]->tracking_id],
origin_timestamps_[fingers[1]->tracking_id]) <
evaluation_timeout_.val_)
new_gs_type = kGestureTypeNull;
}
if (new_gs_type != kGestureTypeMove ||
gs_fingers.size() == 2) {
// We only allow this path to set a move gesture if there
// are two fingers gesturing
current_gesture_type_ = new_gs_type;
}
} else if (sorted_ids.size() == 3) {
const FingerState* fingers[] = {
hwstate.GetFingerState(*sorted_ids.begin()),
hwstate.GetFingerState(*(sorted_ids.begin() + 1)),
hwstate.GetFingerState(*(sorted_ids.begin() + 2))
};
if (!fingers[0] || !fingers[1] || !fingers[2]) {
Err("Unable to find gesturing fingers!");
return;
}
current_gesture_type_ = GetMultiFingerGestureType(fingers, 3);
if (current_gesture_type_ == kGestureTypeSwipe)
last_swipe_timestamp_ = hwstate.timestamp;
} else if (sorted_ids.size() == 4) {
const FingerState* fingers[] = {
hwstate.GetFingerState(*sorted_ids.begin()),
hwstate.GetFingerState(*(sorted_ids.begin() + 1)),
hwstate.GetFingerState(*(sorted_ids.begin() + 2)),
hwstate.GetFingerState(*(sorted_ids.begin() + 3))
};
if (!fingers[0] || !fingers[1] || !fingers[2] || !fingers[3]) {
Err("Unable to find gesturing fingers!");
return;
}
current_gesture_type_ = GetMultiFingerGestureType(fingers, 4);
if (current_gesture_type_ == kGestureTypeFourFingerSwipe)
current_gesture_type_ = kGestureTypeFourFingerSwipe;
}
if (current_gesture_type_ != kGestureTypeNull) {
active_gs_fingers->clear();
for (vector<short, kMaxGesturingFingers>::const_iterator it =
sorted_ids.begin(), e = sorted_ids.end(); it != e; ++it)
active_gs_fingers->insert(*it);
break;
}
}
}
}
if ((current_gesture_type_ == kGestureTypeMove ||
current_gesture_type_ == kGestureTypeNull) &&
(pinch_enable_.val_ && !hwprops_->support_semi_mt) &&
!IsScrollOrSwipe(prev_gesture_type_)) {
bool do_pinch = UpdatePinchState(hwstate, false, gs_fingers);
if (do_pinch) {
current_gesture_type_ = kGestureTypePinch;
} else if (EarlyZoomPotential(hwstate)) {
current_gesture_type_ = kGestureTypeNull;
}
}
break;
case kGestureTypePinch:
if (fingers_.size() == 2 ||
(pinch_status_ == GESTURES_ZOOM_END &&
prev_gesture_type_ == kGestureTypePinch) ||
(prev_gesture_type_ == kGestureTypePinch &&
pinch_locked_ == true)) {
return;
} else {
current_gesture_type_ = kGestureTypeNull;
}
break;
case kGestureTypeMetrics:
// One shouldn't reach here
Err("Metrics gestures reached ImmediateInterpreter");
break;
}
return;
}
bool ImmediateInterpreter::IsScrollOrSwipe(GestureType gesture_type) {
switch(gesture_type) {
case kGestureTypeScroll:
case kGestureTypeSwipe:
case kGestureTypeFourFingerSwipe:
return true;
default:
return false;
}
}
void ImmediateInterpreter::GenerateFingerLiftGesture() {
// If we have just finished scrolling, we set current_gesture_type_ to the
// appropriate lift gesture.
if (IsScrollOrSwipe(prev_gesture_type_) &&
current_gesture_type_ != prev_gesture_type_) {
current_gesture_type_ = GetFingerLiftGesture(prev_gesture_type_);
}
}
namespace {
// Can't use tuple<float, short, short> b/c we want to make a variable
// sized array of them on the stack
struct DistSqElt {
float dist_sq;
short tracking_id[2];
};
struct DistSqCompare {
// Returns true if finger_a is strictly closer to keyboard than finger_b
bool operator()(const DistSqElt& finger_a, const DistSqElt& finger_b) {
return finger_a.dist_sq < finger_b.dist_sq;
}
};
} // namespace {}
void ImmediateInterpreter::SortFingersByProximity(
const FingerMap& finger_ids,
const HardwareState& hwstate,
vector<short, kMaxGesturingFingers>* out_sorted_ids) {
if (finger_ids.size() <= 2) {
for (short finger_id : finger_ids)
out_sorted_ids->push_back(finger_id);
return;
}
// To do the sort, we sort all inter-point distances^2, then scan through
// that until we have enough points
size_t dist_sq_capacity =
(finger_ids.size() * (finger_ids.size() - 1)) / 2;
DistSqElt dist_sq[dist_sq_capacity];
size_t dist_sq_len = 0;
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState& fs1 = hwstate.fingers[i];
if (!SetContainsValue(finger_ids, fs1.tracking_id))
continue;
for (size_t j = i + 1; j < hwstate.finger_cnt; j++) {
const FingerState& fs2 = hwstate.fingers[j];
if (!SetContainsValue(finger_ids, fs2.tracking_id))
continue;
DistSqElt elt = {
DistSq(fs1, fs2),
{ fs1.tracking_id, fs2.tracking_id }
};
if (dist_sq_len >= dist_sq_capacity) {
Err("%s: Array overrun", __func__);
break;
}
dist_sq[dist_sq_len++] = elt;
}
}
DistSqCompare distSqCompare;
std::sort(dist_sq, dist_sq + dist_sq_len, distSqCompare);
if (out_sorted_ids == NULL) {
Err("out_sorted_ids became NULL");
return;
}
for (size_t i = 0; i < dist_sq_len; i++) {
short id1 = dist_sq[i].tracking_id[0];
short id2 = dist_sq[i].tracking_id[1];
bool contains1 = out_sorted_ids->find(id1) != out_sorted_ids->end();
bool contains2 = out_sorted_ids->find(id2) != out_sorted_ids->end();
if (contains1 == contains2 && !out_sorted_ids->empty()) {
// Assuming we have some ids in the out vector, then either we have both
// of these new ids, we have neither. Either way, we can't use this edge.
continue;
}
if (!contains1)
out_sorted_ids->push_back(id1);
if (!contains2)
out_sorted_ids->push_back(id2);
if (out_sorted_ids->size() == finger_ids.size())
break; // We've got all the IDs
}
}
bool ImmediateInterpreter::UpdatePinchState(
const HardwareState& hwstate, bool reset, const FingerMap& gs_fingers) {
if (reset) {
if (pinch_locked_ && prev_gesture_type_ == kGestureTypePinch) {
current_gesture_type_ = kGestureTypePinch;
pinch_status_ = GESTURES_ZOOM_END;
}
// perform reset to "don't know" state
pinch_guess_start_ = -1.0f;
pinch_locked_ = false;
pinch_prev_distance_sq_ = -1.0f;
return false;
}
// once locked stay locked until reset.
if (pinch_locked_) {
pinch_status_ = GESTURES_ZOOM_UPDATE;
return false;
}
// check if we have two valid fingers
if (gs_fingers.size() != 2) {
return false;
}
const FingerState* finger1 = hwstate.GetFingerState(*(gs_fingers.begin()));
const FingerState* finger2 =
hwstate.GetFingerState(*(gs_fingers.begin() + 1));
if (finger1 == NULL || finger2 == NULL) {
Err("Finger unexpectedly NULL");
return false;
}
// don't allow pinching with possible palms
if ((finger1->flags & GESTURES_FINGER_POSSIBLE_PALM) ||
(finger2->flags & GESTURES_FINGER_POSSIBLE_PALM)) {
return false;
}
// assign the bottom finger to finger2
if (finger1->position_y > finger2->position_y) {
std::swap(finger1, finger2);
}
// Check if the two fingers have start positions
if (!MapContainsKey(start_positions_, finger1->tracking_id) ||
!MapContainsKey(start_positions_, finger2->tracking_id)) {
return false;
}
if (pinch_prev_distance_sq_ < 0)
pinch_prev_distance_sq_ = TwoFingerDistanceSq(hwstate);
// Pinch gesture detection
//
// The pinch gesture detection will try to make a guess about whether a pinch
// or not-a-pinch is performed. If the guess stays valid for a specific time
// (slow but consistent movement) or we get a certain decision (fast
// gesturing) the decision is locked until the state is reset.
// * A high ratio of the traveled distances between fingers indicates
// that a pinch is NOT performed.
// * Strong movement of both fingers in opposite directions indicates
// that a pinch IS performed.
Point delta1 = FingerTraveledVector(*finger1, false, true);
Point delta2 = FingerTraveledVector(*finger2, false, true);
// dot product. dot < 0 if fingers move away from each other.
float dot = delta1.x_ * delta2.x_ + delta1.y_ * delta2.y_;
// squared distances both finger have been traveled.
float d1sq = delta1.x_ * delta1.x_ + delta1.y_ * delta1.y_;
float d2sq = delta2.x_ * delta2.x_ + delta2.y_ * delta2.y_;
// True if movement is not strong enough to be distinguished from noise.
// This is not equivalent to a comparison of unsquared values, but seems to
// work well in practice.
bool movement_below_noise = (d1sq + d2sq < pinch_noise_level_sq_.val_);
// guesses if a pinch is being performed or not.
double guess_min_mov_sq = pinch_guess_min_movement_.val_;
guess_min_mov_sq *= guess_min_mov_sq;
bool guess_no = (d1sq > guess_min_mov_sq) ^ (d2sq > guess_min_mov_sq) ||
dot > 0;
bool guess_yes = ((d1sq > guess_min_mov_sq || d2sq > guess_min_mov_sq) &&
dot < 0);
bool pinch_certain = false;
// true if the lower finger is in the dampened zone
bool in_dampened_zone = origin_positions_[finger2->tracking_id].y_ >
hwprops_->bottom - bottom_zone_size_.val_;
float lo_dsq;
float hi_dsq;
if (d1sq < d2sq) {
lo_dsq = d1sq;
hi_dsq = d2sq;
} else {
lo_dsq = d2sq;
hi_dsq = d1sq;
}
bool bad_mov_ratio = lo_dsq <= hi_dsq *
pinch_guess_consistent_mov_ratio_.val_ *
pinch_guess_consistent_mov_ratio_.val_;
if (!bad_mov_ratio &&
!in_dampened_zone &&
guess_yes &&
!guess_no &&
ZoomFingersAreConsistent(state_buffer_)) {
pinch_certain = true;
}
// Thumb is in dampened zone: Only allow inward pinch
if (in_dampened_zone &&
(d2sq < pinch_thumb_min_movement_.val_ * pinch_thumb_min_movement_.val_ ||
!InwardPinch(state_buffer_, *finger2))) {
guess_yes = false;
guess_no = true;
pinch_certain = false;
}
// do state transitions and final decision
if (pinch_guess_start_ < 0) {
// "Don't Know"-state
// Determine guess.
if (!movement_below_noise) {
if (guess_no && !guess_yes) {
pinch_guess_ = false;
pinch_guess_start_ = hwstate.timestamp;
}
if (guess_yes && !guess_no) {
pinch_guess_ = true;
pinch_guess_start_ = hwstate.timestamp;
}
}
}
if (pinch_guess_start_ >= 0) {
// "Guessed"-state
// suppress cursor movement when we guess a pinch gesture
if (pinch_guess_) {
for (size_t i = 0; i < hwstate.finger_cnt; ++i) {
FingerState* finger_state = &hwstate.fingers[i];
finger_state->flags |= GESTURES_FINGER_WARP_X;
finger_state->flags |= GESTURES_FINGER_WARP_Y;
}
}
// Go back to "Don't Know"-state if guess is no longer valid
if (pinch_guess_ != guess_yes ||
pinch_guess_ == guess_no ||
movement_below_noise) {
pinch_guess_start_ = -1.0f;
return false;
}
// certain decisions if pinch is being performed or not
double cert_min_mov_sq = pinch_certain_min_movement_.val_;
cert_min_mov_sq *= cert_min_mov_sq;
pinch_certain |= (d1sq > cert_min_mov_sq &&
d2sq > cert_min_mov_sq) &&
dot < 0;
bool no_pinch_certain = (d1sq > cert_min_mov_sq ||
d2sq > cert_min_mov_sq) &&
dot > 0;
pinch_guess_ |= pinch_certain;
pinch_guess_ &= !no_pinch_certain;
// guessed for long enough or certain decision was made: lock
if ((hwstate.timestamp - pinch_guess_start_ >
pinch_evaluation_timeout_.val_) ||
pinch_certain ||
no_pinch_certain) {
pinch_status_ = GESTURES_ZOOM_START;
pinch_locked_ = true;
return pinch_guess_;
}
}
return false;
}
bool ImmediateInterpreter::PalmIsArrivingOrDeparting(
const FingerState& finger) const {
if (((finger.flags & GESTURES_FINGER_POSSIBLE_PALM) ||
(finger.flags & GESTURES_FINGER_PALM)) &&
((finger.flags & GESTURES_FINGER_TREND_INC_TOUCH_MAJOR) ||
(finger.flags & GESTURES_FINGER_TREND_DEC_TOUCH_MAJOR)) &&
((finger.flags & GESTURES_FINGER_TREND_INC_PRESSURE) ||
(finger.flags & GESTURES_FINGER_TREND_DEC_PRESSURE)))
return true;
return false;
}
bool ImmediateInterpreter::IsTooCloseToThumb(const FingerState& finger) const {
const float kMin2fDistThreshSq = tapping_finger_min_separation_.val_ *
tapping_finger_min_separation_.val_;
for (map<short, stime_t, kMaxFingers>::const_iterator it = thumb_.begin();
it != thumb_.end(); ++it) {
const FingerState* thumb = state_buffer_.Get(0)->GetFingerState(it->first);
float xdist = fabsf(finger.position_x - thumb->position_x);
float ydist = fabsf(finger.position_y - thumb->position_y);
if (xdist * xdist + ydist * ydist < kMin2fDistThreshSq)
return true;
}
return false;
}
bool ImmediateInterpreter::TwoFingersGesturing(
const FingerState& finger1,
const FingerState& finger2,
bool check_button_type) const {
// Make sure distance between fingers isn't too great
if (!metrics_->CloseEnoughToGesture(Vector2(finger1), Vector2(finger2)))
return false;
// Next, if two fingers are moving a lot, they are gesturing together.
if (started_moving_time_ > changed_time_) {
// Fingers are moving
float dist1_sq = DistanceTravelledSq(finger1, false);
float dist2_sq = DistanceTravelledSq(finger2, false);
if (thumb_movement_factor_.val_ * thumb_movement_factor_.val_ *
max(dist1_sq, dist2_sq) < min(dist1_sq, dist2_sq)) {
return true;
}
}
// Make sure the pressure difference isn't too great for vertically
// aligned contacts
float pdiff = fabsf(finger1.pressure - finger2.pressure);
float xdist = fabsf(finger1.position_x - finger2.position_x);
float ydist = fabsf(finger1.position_y - finger2.position_y);
if (pdiff > two_finger_pressure_diff_thresh_.val_ && ydist > xdist &&
((finger1.pressure > finger2.pressure) ==
(finger1.position_y > finger2.position_y)))
return false;
const float kMin2fDistThreshSq = tapping_finger_min_separation_.val_ *
tapping_finger_min_separation_.val_;
float dist_sq = xdist * xdist + ydist * ydist;
// Make sure distance between fingers isn't too small
if ((dist_sq < kMin2fDistThreshSq) &&
!(finger1.flags & GESTURES_FINGER_MERGE))
return false;
// If both fingers have a tendency of moving at the same direction, they
// are gesturing together. This check is disabled if we are using the
// function to distinguish left/right clicks.
if (!check_button_type) {
unsigned and_flags = finger1.flags & finger2.flags;
if ((and_flags & GESTURES_FINGER_TREND_INC_X) ||
(and_flags & GESTURES_FINGER_TREND_DEC_X) ||
(and_flags & GESTURES_FINGER_TREND_INC_Y) ||
(and_flags & GESTURES_FINGER_TREND_DEC_Y))
return true;
}
// Next, if fingers are vertically aligned and one is in the bottom zone,
// consider that one a resting thumb (thus, do not scroll/right click)
// if it has greater pressure. For clicking, we relax the pressure requirement
// because we may not have enough time to determine.
if (xdist < ydist && (FingerInDampenedZone(finger1) ||
FingerInDampenedZone(finger2)) &&
(FingerInDampenedZone(finger1) == (finger1.pressure > finger2.pressure) ||
check_button_type))
return false;
return true;
}
GestureType ImmediateInterpreter::GetTwoFingerGestureType(
const FingerState& finger1,
const FingerState& finger2) {
if (!MapContainsKey(start_positions_, finger1.tracking_id) ||
!MapContainsKey(start_positions_, finger2.tracking_id))
return kGestureTypeNull;
// If a finger is close to any thumb, we believe it to be due to thumb-splits
// and ignore it.
int num_close_to_thumb = 0;
num_close_to_thumb += static_cast<int>(IsTooCloseToThumb(finger1));
num_close_to_thumb += static_cast<int>(IsTooCloseToThumb(finger2));
if (num_close_to_thumb == 1)
return kGestureTypeMove;
else if (num_close_to_thumb == 2)
return kGestureTypeNull;
// Compute distance traveled since fingers changed for each finger
float dx1 = finger1.position_x - start_positions_[finger1.tracking_id].x_;
float dy1 = finger1.position_y - start_positions_[finger1.tracking_id].y_;
float dx2 = finger2.position_x - start_positions_[finger2.tracking_id].x_;
float dy2 = finger2.position_y - start_positions_[finger2.tracking_id].y_;
float large_dx = MaxMag(dx1, dx2);
float large_dy = MaxMag(dy1, dy2);
// These compares are okay if d{x,y}1 == d{x,y}2:
short large_dx_id =
(large_dx == dx1) ? finger1.tracking_id : finger2.tracking_id;
short large_dy_id =
(large_dy == dy1) ? finger1.tracking_id : finger2.tracking_id;
float small_dx = MinMag(dx1, dx2);
float small_dy = MinMag(dy1, dy2);
short small_dx_id =
(small_dx == dx1) ? finger1.tracking_id : finger2.tracking_id;
short small_dy_id =
(small_dy == dy1) ? finger1.tracking_id : finger2.tracking_id;
bool dampened_zone_occupied = false;
// movements of the finger in the dampened zone. If there are multiple
// fingers in the dampened zone, dx is min(dx_1, dx_2), dy is min(dy_1, dy_2).
float damp_dx = INFINITY;
float damp_dy = INFINITY;
float non_damp_dx = 0.0;
float non_damp_dy = 0.0;
if (FingerInDampenedZone(finger1) ||
(finger1.flags & GESTURES_FINGER_POSSIBLE_PALM)) {
dampened_zone_occupied = true;
damp_dx = dx1;
damp_dy = dy1;
non_damp_dx = dx2;
non_damp_dy = dy2;
}
if (FingerInDampenedZone(finger2) ||
(finger2.flags & GESTURES_FINGER_POSSIBLE_PALM)) {
dampened_zone_occupied = true;
damp_dx = MinMag(damp_dx, dx2);
damp_dy = MinMag(damp_dy, dy2);
non_damp_dx = MaxMag(non_damp_dx, dx1);
non_damp_dy = MaxMag(non_damp_dy, dy1);
}
// Trending in the same direction?
const unsigned kTrendX =
GESTURES_FINGER_TREND_INC_X | GESTURES_FINGER_TREND_DEC_X;
const unsigned kTrendY =
GESTURES_FINGER_TREND_INC_Y | GESTURES_FINGER_TREND_DEC_Y;
unsigned common_trend_flags = finger1.flags & finger2.flags &
(kTrendX | kTrendY);
bool large_dx_moving =
fabsf(large_dx) >= two_finger_scroll_distance_thresh_.val_ ||
SetContainsValue(moving_, large_dx_id);
bool large_dy_moving =
fabsf(large_dy) >= two_finger_scroll_distance_thresh_.val_ ||
SetContainsValue(moving_, large_dy_id);
bool small_dx_moving =
fabsf(small_dx) >= two_finger_scroll_distance_thresh_.val_ ||
SetContainsValue(moving_, small_dx_id);
bool small_dy_moving =
fabsf(small_dy) >= two_finger_scroll_distance_thresh_.val_ ||
SetContainsValue(moving_, small_dy_id);
bool trend_scrolling_x = (common_trend_flags & kTrendX) &&
large_dx_moving && small_dx_moving;
bool trend_scrolling_y = (common_trend_flags & kTrendY) &&
large_dy_moving && small_dy_moving;
if (pointing_.size() == 2 && (trend_scrolling_x || trend_scrolling_y)) {
if (pinch_enable_.val_ && !ScrollAngle(finger1, finger2))
return kGestureTypeNull;
return kGestureTypeScroll;
}
if (fabsf(large_dx) > fabsf(large_dy)) {
// consider horizontal scroll
if (fabsf(small_dx) < two_finger_scroll_distance_thresh_.val_)
small_dx = 0.0;
if (large_dx * small_dx <= 0.0) {
// not same direction
if (fabsf(large_dx) < two_finger_move_distance_thresh_.val_)
return kGestureTypeNull;
else
return kGestureTypeMove;
}
if (fabsf(large_dx) < two_finger_scroll_distance_thresh_.val_)
return kGestureTypeNull;
if (dampened_zone_occupied) {
// Require damp to move at least some amount with the other finger
if (fabsf(damp_dx) <
damp_scroll_min_movement_factor_.val_ * fabsf(non_damp_dx)) {
return kGestureTypeNull;
}
}
if (pinch_enable_.val_ && !ScrollAngle(finger1, finger2))
return kGestureTypeNull;
return kGestureTypeScroll;
} else {
// consider vertical scroll
if (fabsf(small_dy) < two_finger_scroll_distance_thresh_.val_)
small_dy = 0.0;
if (large_dy * small_dy <= 0.0) {
if (fabsf(large_dy) < two_finger_move_distance_thresh_.val_)
return kGestureTypeNull;
else
return kGestureTypeMove;
}
if (dampened_zone_occupied) {
// Require damp to move at least some amount with the other finger
if (fabsf(damp_dy) <
damp_scroll_min_movement_factor_.val_ * fabsf(non_damp_dy)) {
return kGestureTypeNull;
}
}
if (pinch_enable_.val_ && !ScrollAngle(finger1, finger2))
return kGestureTypeNull;
return kGestureTypeScroll;
}
}
GestureType ImmediateInterpreter::GetFingerLiftGesture(
GestureType current_gesture_type) {
switch(current_gesture_type) {
case kGestureTypeScroll: return kGestureTypeFling;
case kGestureTypeSwipe: return kGestureTypeSwipeLift;
case kGestureTypeFourFingerSwipe: return kGestureTypeFourFingerSwipeLift;
default: return kGestureTypeNull;
}
}
GestureType ImmediateInterpreter::GetMultiFingerGestureType(
const FingerState* const fingers[], const int num_fingers) {
float swipe_distance_thresh;
float swipe_distance_ratio;
map<short, Point, kMaxFingers> *swipe_start_positions;
GestureType gesture_type;
if (num_fingers == 4) {
swipe_distance_thresh = four_finger_swipe_distance_thresh_.val_;
swipe_distance_ratio = four_finger_swipe_distance_ratio_.val_;
swipe_start_positions = &four_finger_swipe_start_positions_;
gesture_type = kGestureTypeFourFingerSwipe;
} else if (num_fingers == 3) {
swipe_distance_thresh = three_finger_swipe_distance_thresh_.val_;
swipe_distance_ratio = three_finger_swipe_distance_ratio_.val_;
swipe_start_positions = &three_finger_swipe_start_positions_;
gesture_type = kGestureTypeSwipe;
} else {
return kGestureTypeNull;
}
const FingerState* x_fingers[num_fingers];
const FingerState* y_fingers[num_fingers];
for (int i = 0; i < num_fingers; i++) {
x_fingers[i] = fingers[i];
y_fingers[i] = fingers[i];
}
std::sort(x_fingers, x_fingers + num_fingers,
[] (const FingerState* a, const FingerState* b) ->
bool { return a->position_x < b->position_x; });
std::sort(y_fingers, y_fingers + num_fingers,
[] (const FingerState* a, const FingerState* b) ->
bool { return a->position_y < b->position_y; });
bool horizontal =
(x_fingers[num_fingers - 1]->position_x - x_fingers[0]->position_x) >=
(y_fingers[num_fingers -1]->position_y - y_fingers[0]->position_y);
const FingerState* sorted_fingers[num_fingers];
for (int i = 0; i < num_fingers; i++) {
sorted_fingers[i] = horizontal ? x_fingers[i] : y_fingers[i];
}
float dx[num_fingers];
float dy[num_fingers];
float dy_sum = 0;
float dx_sum = 0;
for (int i = 0; i < num_fingers; i++) {
dx[i] = sorted_fingers[i]->position_x -
(*swipe_start_positions)[sorted_fingers[i]->tracking_id].x_;
dy[i] = sorted_fingers[i]->position_y -
(*swipe_start_positions)[sorted_fingers[i]->tracking_id].y_;
dx_sum += dx[i];
dy_sum += dy[i];
}
// pick horizontal or vertical
float *deltas = fabsf(dx_sum) > fabsf(dy_sum) ? dx : dy;
swipe_is_vertical_ = deltas == dy;
// All fingers must move in the same direction.
for (int i = 1; i < num_fingers; i++) {
if (deltas[i] * deltas[0] <= 0.0) {
for (int i = 0; i < num_fingers; i++) {
Point point(sorted_fingers[i]->position_x,
sorted_fingers[i]->position_y);
(*swipe_start_positions)[sorted_fingers[i]->tracking_id] =
point;
}
return kGestureTypeNull;
}
}
// All fingers must have traveled far enough.
float max_delta = fabsf(deltas[0]);
float min_delta = fabsf(deltas[0]);
for (int i = 1; i < num_fingers; i++) {
max_delta = max(max_delta, fabsf(deltas[i]));
min_delta = min(min_delta, fabsf(deltas[i]));
}
if (max_delta >= swipe_distance_thresh &&
min_delta >= swipe_distance_ratio * max_delta)
return gesture_type;
return kGestureTypeNull;
}
const char* ImmediateInterpreter::TapToClickStateName(TapToClickState state) {
switch (state) {
case kTtcIdle: return "Idle";
case kTtcFirstTapBegan: return "FirstTapBegan";
case kTtcTapComplete: return "TapComplete";
case kTtcSubsequentTapBegan: return "SubsequentTapBegan";
case kTtcDrag: return "Drag";
case kTtcDragRelease: return "DragRelease";
case kTtcDragRetouch: return "DragRetouch";
default: return "<unknown>";
}
}
stime_t ImmediateInterpreter::TimeoutForTtcState(TapToClickState state) {
switch (state) {
case kTtcIdle: return tap_timeout_.val_;
case kTtcFirstTapBegan: return tap_timeout_.val_;
case kTtcTapComplete: return inter_tap_timeout_.val_;
case kTtcSubsequentTapBegan: return tap_timeout_.val_;
case kTtcDrag: return tap_timeout_.val_;
case kTtcDragRelease: return tap_drag_timeout_.val_;
case kTtcDragRetouch: return tap_timeout_.val_;
default:
Log("Unknown state!");
return 0.0;
}
}
void ImmediateInterpreter::SetTapToClickState(TapToClickState state,
stime_t now) {
if (tap_to_click_state_ != state) {
tap_to_click_state_ = state;
tap_to_click_state_entered_ = now;
}
}
void ImmediateInterpreter::UpdateTapGesture(
const HardwareState* hwstate,
const FingerMap& gs_fingers,
const bool same_fingers,
stime_t now,
stime_t* timeout) {
unsigned down = 0;
unsigned up = 0;
UpdateTapState(hwstate, gs_fingers, same_fingers, now, &down, &up, timeout);
if (down == 0 && up == 0) {
return;
}
Log("UpdateTapGesture: Tap Generated");
result_ = Gesture(kGestureButtonsChange,
state_buffer_.Get(1)->timestamp,
now,
down,
up);
}
void ImmediateInterpreter::UpdateTapState(
const HardwareState* hwstate,
const FingerMap& gs_fingers,
const bool same_fingers,
stime_t now,
unsigned* buttons_down,
unsigned* buttons_up,
stime_t* timeout) {
if (tap_to_click_state_ == kTtcIdle && (!tap_enable_.val_ ||
tap_paused_.val_))
return;
FingerMap tap_gs_fingers;
if (hwstate)
RemoveMissingIdsFromSet(&tap_dead_fingers_, *hwstate);
bool cancel_tapping = false;
if (hwstate) {
for (int i = 0; i < hwstate->finger_cnt; ++i) {
if (hwstate->fingers[i].flags &
(GESTURES_FINGER_NO_TAP | GESTURES_FINGER_MERGE))
cancel_tapping = true;
}
for (FingerMap::const_iterator it =
gs_fingers.begin(), e = gs_fingers.end(); it != e; ++it) {
const FingerState* fs = hwstate->GetFingerState(*it);
if (!fs) {
Err("Missing finger state?!");
continue;
}
tap_gs_fingers.insert(*it);
}
}
set<short, kMaxTapFingers> added_fingers;
// Fingers removed from the pad entirely
set<short, kMaxTapFingers> removed_fingers;
// Fingers that were gesturing, but now aren't
set<short, kMaxFingers> dead_fingers;
const bool phys_click_in_progress = hwstate && hwstate->buttons_down != 0 &&
(zero_finger_click_enable_.val_ || finger_seen_shortly_after_button_down_);
bool is_timeout = (now - tap_to_click_state_entered_ >
TimeoutForTtcState(tap_to_click_state_));
if (phys_click_in_progress) {
// Don't allow any current fingers to tap ever
for (size_t i = 0; i < hwstate->finger_cnt; i++)
tap_dead_fingers_.insert(hwstate->fingers[i].tracking_id);
}
if (hwstate && (!same_fingers || prev_tap_gs_fingers_ != tap_gs_fingers)) {
// See if fingers were added
for (FingerMap::const_iterator it =
tap_gs_fingers.begin(), e = tap_gs_fingers.end(); it != e; ++it) {
// If the finger was marked as a thumb before, it is not new.
if (hwstate->timestamp - finger_origin_timestamp(*it) >
thumb_click_prevention_timeout_.val_)
continue;
if (!SetContainsValue(prev_tap_gs_fingers_, *it)) {
// Gesturing finger wasn't in prev state. It's new.
const FingerState* fs = hwstate->GetFingerState(*it);
if (FingerTooCloseToTap(*hwstate, *fs) ||
FingerTooCloseToTap(*state_buffer_.Get(1), *fs) ||
SetContainsValue(tap_dead_fingers_, fs->tracking_id))
continue;
added_fingers.insert(*it);
Log("TTC: Added %d", *it);
}
}
// See if fingers were removed or are now non-gesturing (dead)
for (FingerMap::const_iterator it =
prev_tap_gs_fingers_.begin(), e = prev_tap_gs_fingers_.end();
it != e; ++it) {
if (tap_gs_fingers.find(*it) != tap_gs_fingers.end())
// still gesturing; neither removed nor dead
continue;
if (!hwstate->GetFingerState(*it)) {
// Previously gesturing finger isn't in current state. It's gone.
removed_fingers.insert(*it);
Log("TTC: Removed %d", *it);
} else {
// Previously gesturing finger is in current state. It's dead.
dead_fingers.insert(*it);
Log("TTC: Dead %d", *it);
}
}
}
prev_tap_gs_fingers_ = tap_gs_fingers;
// The state machine:
// If you are updating the code, keep this diagram correct.
// We have a TapRecord which stores current tap state.
// Also, if the physical button is down or previous gesture type is scroll,
// we go to (or stay in) Idle state.
// Start
// ↓
// [Idle**] <----------------------------------------------------------,
// ↓ added finger(s) |
// ,>[FirstTapBegan] -<right click: send right click, timeout/movement>->|
// | ↓ released all fingers |
// ,->[TapComplete*] --<timeout: send click>----------------------------->|
// || | | two finger touching: send left click. |
// |'----+-' |
// | ↓ add finger(s) |
// | [SubsequentTapBegan] --<timeout/move w/o delay: send click>-------->|
// | | | | release all fingers: send left click |
// |<----+-+-' |
// | | `-> start non-left click: send left click; goto FirstTapBegan |
// | ↓ timeout/movement with delay: send button down |
// | ,->[Drag] --<detect 2 finger gesture: send button up>--------------->|
// | | ↓ release all fingers |
// | | [DragRelease*] --<timeout: send button up>---------------------->|
// | | ↓ add finger(s) |
// | | [DragRetouch] --<remove fingers (left tap): send button up>----->|
// | | | | timeout/movement
// | '---+-'
// | | remove all fingers (non-left tap): send button up
// '-----'
//
// * When entering TapComplete or DragRelease, we set a timer, since
// we will have no fingers on the pad and want to run possibly before
// fingers are put on the pad. Note that we use different timeouts
// based on which state we're in (tap_timeout_ or tap_drag_timeout_).
// ** When entering idle, we reset the TapRecord.
if (tap_to_click_state_ != kTtcIdle)
Log("TTC State: %s", TapToClickStateName(tap_to_click_state_));
if (!hwstate)
Log("TTC: This is a timer callback");
if (phys_click_in_progress || KeyboardRecentlyUsed(now) ||
prev_result_.type == kGestureTypeScroll ||
cancel_tapping) {
Log("TTC: Forced to idle");
SetTapToClickState(kTtcIdle, now);
return;
}
switch (tap_to_click_state_) {
case kTtcIdle:
tap_record_.Clear();
if (hwstate &&
hwstate->timestamp - last_movement_timestamp_ >=
motion_tap_prevent_timeout_.val_) {
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers, removed_fingers,
dead_fingers);
if (tap_record_.TapBegan())
SetTapToClickState(kTtcFirstTapBegan, now);
}
break;
case kTtcFirstTapBegan:
if (is_timeout) {
SetTapToClickState(kTtcIdle, now);
break;
}
if (!hwstate) {
Log("hwstate NULL but no timeout?!");
break;
}
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers,
removed_fingers, dead_fingers);
Log("TTC: Is tap? %d Is moving? %d",
tap_record_.TapComplete(),
tap_record_.Moving(*hwstate, tap_move_dist_.val_));
if (tap_record_.TapComplete()) {
if (!tap_record_.MinTapPressureMet() ||
!tap_record_.FingersBelowMaxAge()) {
SetTapToClickState(kTtcIdle, now);
} else if (tap_record_.TapType() == GESTURES_BUTTON_LEFT &&
tap_drag_enable_.val_) {
SetTapToClickState(kTtcTapComplete, now);
} else {
*buttons_down = *buttons_up = tap_record_.TapType();
SetTapToClickState(kTtcIdle, now);
}
} else if (tap_record_.Moving(*hwstate, tap_move_dist_.val_)) {
SetTapToClickState(kTtcIdle, now);
}
break;
case kTtcTapComplete:
if (!added_fingers.empty()) {
tap_record_.Clear();
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers, removed_fingers,
dead_fingers);
// If more than one finger is touching: Send click
// and return to FirstTapBegan state.
if (tap_record_.TapType() != GESTURES_BUTTON_LEFT) {
*buttons_down = *buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcFirstTapBegan, now);
} else {
tap_drag_last_motion_time_ = now;
tap_drag_finger_was_stationary_ = false;
SetTapToClickState(kTtcSubsequentTapBegan, now);
}
} else if (is_timeout) {
*buttons_down = *buttons_up =
tap_record_.MinTapPressureMet() ? tap_record_.TapType() : 0;
SetTapToClickState(kTtcIdle, now);
}
break;
case kTtcSubsequentTapBegan:
if (!is_timeout && !hwstate) {
Log("hwstate NULL but not a timeout?!");
break;
}
if (hwstate)
tap_record_.Update(*hwstate, *state_buffer_.Get(1), added_fingers,
removed_fingers, dead_fingers);
if (!tap_record_.Motionless(*hwstate, *state_buffer_.Get(1),
tap_max_movement_.val_)) {
tap_drag_last_motion_time_ = now;
}
if (tap_record_.TapType() == GESTURES_BUTTON_LEFT &&
now - tap_drag_last_motion_time_ > tap_drag_stationary_time_.val_) {
tap_drag_finger_was_stationary_ = true;
}
if (is_timeout || tap_record_.Moving(*hwstate, tap_move_dist_.val_)) {
if (tap_record_.TapType() == GESTURES_BUTTON_LEFT) {
if (is_timeout) {
// moving with just one finger. Start dragging.
*buttons_down = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcDrag, now);
} else {
bool drag_delay_met = (now - tap_to_click_state_entered_
> tap_drag_delay_.val_);
if (drag_delay_met && tap_drag_finger_was_stationary_) {
*buttons_down = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcDrag, now);
} else {
*buttons_down = GESTURES_BUTTON_LEFT;
*buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcIdle, now);
}
}
} else if (!tap_record_.TapComplete()) {
// not just one finger. Send button click and go to idle.
*buttons_down = *buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcIdle, now);
}
break;
}
if (tap_record_.TapType() != GESTURES_BUTTON_LEFT) {
// We aren't going to drag, so send left click now and handle current
// tap afterwards.
*buttons_down = *buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcFirstTapBegan, now);
}
if (tap_record_.TapComplete()) {
*buttons_down = *buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcTapComplete, now);
Log("TTC: Subsequent left tap complete");
}
break;
case kTtcDrag:
if (hwstate)
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers, removed_fingers,
dead_fingers);
if (tap_record_.TapComplete()) {
tap_record_.Clear();
if (drag_lock_enable_.val_) {
SetTapToClickState(kTtcDragRelease, now);
} else {
*buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcIdle, now);
}
}
if (tap_record_.TapType() != GESTURES_BUTTON_LEFT &&
now - tap_to_click_state_entered_ <= evaluation_timeout_.val_) {
// We thought we were dragging, but actually we're doing a
// non-tap-to-click multitouch gesture.
*buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcIdle, now);
}
break;
case kTtcDragRelease:
if (!added_fingers.empty()) {
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers, removed_fingers,
dead_fingers);
SetTapToClickState(kTtcDragRetouch, now);
} else if (is_timeout) {
*buttons_up = GESTURES_BUTTON_LEFT;
SetTapToClickState(kTtcIdle, now);
}
break;
case kTtcDragRetouch:
if (hwstate)
tap_record_.Update(
*hwstate, *state_buffer_.Get(1), added_fingers, removed_fingers,
dead_fingers);
if (tap_record_.TapComplete()) {
*buttons_up = GESTURES_BUTTON_LEFT;
if (tap_record_.TapType() == GESTURES_BUTTON_LEFT)
SetTapToClickState(kTtcIdle, now);
else
SetTapToClickState(kTtcTapComplete, now);
break;
}
if (is_timeout) {
SetTapToClickState(kTtcDrag, now);
break;
}
if (!hwstate) {
Log("not timeout but hwstate is NULL?!");
break;
}
if (tap_record_.Moving(*hwstate, tap_move_dist_.val_))
SetTapToClickState(kTtcDrag, now);
break;
}
if (tap_to_click_state_ != kTtcIdle)
Log("TTC: New state: %s", TapToClickStateName(tap_to_click_state_));
// Take action based on new state:
switch (tap_to_click_state_) {
case kTtcTapComplete:
*timeout = TimeoutForTtcState(tap_to_click_state_);
break;
case kTtcDragRelease:
*timeout = TimeoutForTtcState(tap_to_click_state_);
break;
default: // so gcc doesn't complain about missing enums
break;
}
}
bool ImmediateInterpreter::FingerTooCloseToTap(const HardwareState& hwstate,
const FingerState& fs) {
const float kMinAllowableSq =
tapping_finger_min_separation_.val_ * tapping_finger_min_separation_.val_;
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState* iter_fs = &hwstate.fingers[i];
if (iter_fs->tracking_id == fs.tracking_id)
continue;
float dist_sq = DistSq(fs, *iter_fs);
if (dist_sq < kMinAllowableSq)
return true;
}
return false;
}
bool ImmediateInterpreter::FingerInDampenedZone(
const FingerState& finger) const {
// TODO(adlr): cache thresh
float thresh = hwprops_->bottom - bottom_zone_size_.val_;
return finger.position_y > thresh;
}
void ImmediateInterpreter::FillStartPositions(const HardwareState& hwstate) {
RemoveMissingIdsFromMap(&origin_positions_, hwstate);
for (short i = 0; i < hwstate.finger_cnt; i++) {
Point point(hwstate.fingers[i].position_x,
hwstate.fingers[i].position_y);
start_positions_[hwstate.fingers[i].tracking_id] = point;
three_finger_swipe_start_positions_[hwstate.fingers[i].tracking_id] = point;
four_finger_swipe_start_positions_[hwstate.fingers[i].tracking_id] = point;
if (!MapContainsKey(origin_positions_, hwstate.fingers[i].tracking_id))
origin_positions_[hwstate.fingers[i].tracking_id] = point;
}
}
int ImmediateInterpreter::GetButtonTypeFromPosition(
const HardwareState& hwstate) {
if (hwstate.finger_cnt <= 0 || hwstate.finger_cnt > 1 ||
!button_right_click_zone_enable_.val_) {
return GESTURES_BUTTON_LEFT;
}
const FingerState& fs = hwstate.fingers[0];
if (fs.position_x > hwprops_->right - button_right_click_zone_size_.val_) {
return GESTURES_BUTTON_RIGHT;
}
return GESTURES_BUTTON_LEFT;
}
int ImmediateInterpreter::EvaluateButtonType(
const HardwareState& hwstate, stime_t button_down_time) {
// Handle T5R2/SemiMT touchpads
if ((hwprops_->supports_t5r2 || hwprops_->support_semi_mt) &&
hwstate.touch_cnt > 2) {
if (hwstate.touch_cnt - thumb_.size() == 3 &&
three_finger_click_enable_.val_ && t5r2_three_finger_click_enable_.val_)
return GESTURES_BUTTON_MIDDLE;
return GESTURES_BUTTON_RIGHT;
}
// Just return the hardware state button, based on finger position,
// if no further analysis is needed.
bool finger_update = finger_button_click_.Update(hwstate, button_down_time);
if (!finger_update && hwprops_->is_button_pad &&
hwstate.buttons_down == GESTURES_BUTTON_LEFT) {
return GetButtonTypeFromPosition(hwstate);
} else if (!finger_update) {
return hwstate.buttons_down;
}
Log("EvaluateButtonType: R/C/H: %d/%d/%d",
finger_button_click_.num_recent(),
finger_button_click_.num_cold(),
finger_button_click_.num_hot());
// Handle 2 finger cases:
if (finger_button_click_.num_fingers() == 2)
return finger_button_click_.EvaluateTwoFingerButtonType();
// Handle cases with 3 or more fingers:
return finger_button_click_.EvaluateThreeOrMoreFingerButtonType();
}
FingerMap ImmediateInterpreter::UpdateMovingFingers(
const HardwareState& hwstate) {
FingerMap newly_moving_fingers;
if (moving_.size() == hwstate.finger_cnt)
return newly_moving_fingers; // All fingers already started moving
const float kMinDistSq =
change_move_distance_.val_ * change_move_distance_.val_;
for (size_t i = 0; i < hwstate.finger_cnt; i++) {
const FingerState& fs = hwstate.fingers[i];
if (!MapContainsKey(start_positions_, fs.tracking_id)) {
Err("Missing start position!");
continue;
}
if (SetContainsValue(moving_, fs.tracking_id)) {
// This finger already moving
continue;
}
float dist_sq = DistanceTravelledSq(fs, false);
if (dist_sq > kMinDistSq) {
moving_.insert(fs.tracking_id);
newly_moving_fingers.insert(fs.tracking_id);
}
}
return newly_moving_fingers;
}
void ImmediateInterpreter::UpdateStartedMovingTime(
stime_t now,
const FingerMap& gs_fingers,
const FingerMap& newly_moving_fingers) {
// Update started moving time if any gesturing finger is newly moving.
for (auto it = gs_fingers.begin(), e = gs_fingers.end(); it != e; ++it) {
if (SetContainsValue(newly_moving_fingers, *it)) {
started_moving_time_ = now;
// Extend the thumb evaluation period for any finger that is still under
// evaluation as there is a new moving finger.
for (map<short, stime_t, kMaxFingers>::iterator it = thumb_.begin();
it != thumb_.end(); ++it)
if ((*it).second < thumb_eval_timeout_.val_ && (*it).second > 0.0)
(*it).second = thumb_eval_timeout_.val_;
return;
}
}
}
void ImmediateInterpreter::UpdateButtons(const HardwareState& hwstate,
stime_t* timeout) {
// TODO(miletus): To distinguish between left/right buttons down
bool prev_button_down = state_buffer_.Get(1)->buttons_down;
bool button_down = hwstate.buttons_down;
if (!prev_button_down && !button_down)
return;
bool phys_down_edge = button_down && !prev_button_down;
bool phys_up_edge = !button_down && prev_button_down;
if (phys_down_edge) {
finger_seen_shortly_after_button_down_ = false;
sent_button_down_ = false;
button_down_timeout_ = hwstate.timestamp + button_evaluation_timeout_.val_;
}
// If we haven't seen a finger on the pad shortly after the click, do nothing
if (!finger_seen_shortly_after_button_down_ &&
hwstate.timestamp <= button_down_timeout_)
finger_seen_shortly_after_button_down_ = (hwstate.finger_cnt > 0);
if (!finger_seen_shortly_after_button_down_ &&
!zero_finger_click_enable_.val_)
return;
if (!sent_button_down_) {
stime_t button_down_time = button_down_timeout_ -
button_evaluation_timeout_.val_;
button_type_ = EvaluateButtonType(hwstate, button_down_time);
if (!hwstate.SameFingersAs(*state_buffer_.Get(0))) {
// Fingers have changed since last state, reset timeout
button_down_timeout_ = hwstate.timestamp + button_finger_timeout_.val_;
}
// button_up before button_evaluation_timeout_ expired.
// Send up & down for button that was previously down, but not yet sent.
if (button_type_ == GESTURES_BUTTON_NONE)
button_type_ = prev_button_down;
// Send button down if timeout has been reached or button up happened
if (button_down_timeout_ <= hwstate.timestamp ||
phys_up_edge) {
// Send button down
if (result_.type == kGestureTypeButtonsChange)
Err("Gesture type already button?!");
result_ = Gesture(kGestureButtonsChange,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
button_type_,
0);
sent_button_down_ = true;
} else if (timeout) {
*timeout = button_down_timeout_ - hwstate.timestamp;
}
}
if (phys_up_edge) {
// Send button up
if (result_.type != kGestureTypeButtonsChange)
result_ = Gesture(kGestureButtonsChange,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
0,
button_type_);
else
result_.details.buttons.up = button_type_;
// Reset button state
button_type_ = GESTURES_BUTTON_NONE;
button_down_timeout_ = 0;
sent_button_down_ = false;
// When a buttons_up event is generated, we need to reset the
// finger_leave_time_ in order to defer any gesture generation
// right after it.
finger_leave_time_ = hwstate.timestamp;
}
}
void ImmediateInterpreter::UpdateButtonsTimeout(stime_t now) {
if (sent_button_down_) {
Err("How is sent_button_down_ set?");
return;
}
if (button_type_ == GESTURES_BUTTON_NONE)
return;
sent_button_down_ = true;
result_ = Gesture(kGestureButtonsChange,
state_buffer_.Get(1)->timestamp,
now,
button_type_,
0);
}
void ImmediateInterpreter::FillResultGesture(
const HardwareState& hwstate,
const FingerMap& fingers) {
bool zero_move = false;
switch (current_gesture_type_) {
case kGestureTypeMove: {
if (fingers.empty())
return;
// Use the finger which has moved the most to compute motion.
// First, need to find out which finger that is.
const FingerState* current = NULL;
if (moving_finger_id_ >= 0)
current = hwstate.GetFingerState(moving_finger_id_);
const HardwareState* prev_hs = state_buffer_.Get(1);
if (prev_hs && !current) {
float curr_dist_sq = -1;
for (FingerMap::const_iterator it =
fingers.begin(), e = fingers.end(); it != e; ++it) {
const FingerState* fs = hwstate.GetFingerState(*it);
const FingerState* prev_fs = prev_hs->GetFingerState(fs->tracking_id);
if (!prev_fs)
break;
float dist_sq = DistSq(*fs, *prev_fs);
if (dist_sq > curr_dist_sq) {
current = fs;
curr_dist_sq = dist_sq;
}
}
}
if (!current)
return;
// Find corresponding finger id in previous state
const FingerState* prev =
state_buffer_.Get(1)->GetFingerState(current->tracking_id);
const FingerState* prev2 = !state_buffer_.Get(2) ? NULL :
state_buffer_.Get(2)->GetFingerState(current->tracking_id);
if (!prev || !current)
return;
if (current->flags & GESTURES_FINGER_MERGE)
return;
stime_t dt = hwstate.timestamp - state_buffer_.Get(1)->timestamp;
bool suppress_finger_movement =
scroll_manager_.SuppressStationaryFingerMovement(
*current, *prev, dt) ||
scroll_manager_.StationaryFingerPressureChangingSignificantly(
state_buffer_, *current);
if (quick_acceleration_factor_.val_ && prev2) {
stime_t dt2 =
state_buffer_.Get(1)->timestamp - state_buffer_.Get(2)->timestamp;
float dist_sq = DistSq(*current, *prev);
float dist_sq2 = DistSq(*prev, *prev2);
if (dist_sq2 * dt && // have prev dist and current time
dist_sq2 * dt * dt *
quick_acceleration_factor_.val_ * quick_acceleration_factor_.val_ <
dist_sq * dt2 * dt2) {
return;
}
}
if (suppress_finger_movement) {
scroll_manager_.prev_result_suppress_finger_movement_ = true;
result_ = Gesture(kGestureMove,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
0,
0);
return;
}
scroll_manager_.prev_result_suppress_finger_movement_ = false;
float dx = current->position_x - prev->position_x;
if (current->flags & GESTURES_FINGER_WARP_X_MOVE)
dx = 0.0;
float dy = current->position_y - prev->position_y;
if (current->flags & GESTURES_FINGER_WARP_Y_MOVE)
dy = 0.0;
float dsq = dx * dx + dy * dy;
float dx_total = current->position_x -
start_positions_[current->tracking_id].x_;
float dy_total = current->position_y -
start_positions_[current->tracking_id].y_;
float dsq_total = dx_total * dx_total + dy_total * dy_total;
float dsq_thresh = (move_lock_speed_.val_ * move_lock_speed_.val_) *
(dt * dt);
if (dsq > dsq_thresh) {
// lock onto this finger
moving_finger_id_ = current->tracking_id;
}
float dsq_total_thresh =
move_report_distance_.val_ * move_report_distance_.val_;
if (dsq_total >= dsq_total_thresh) {
zero_move = dsq == 0.0;
result_ = Gesture(kGestureMove,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
dx,
dy);
}
break;
}
case kGestureTypeScroll: {
if (!scroll_manager_.FillResultScroll(state_buffer_,
prev_active_gs_fingers_,
fingers,
prev_gesture_type_,
prev_result_,
&result_,
&scroll_buffer_))
return;
break;
}
case kGestureTypeFling: {
scroll_manager_.FillResultFling(state_buffer_, scroll_buffer_, &result_);
break;
}
case kGestureTypeSwipe:
case kGestureTypeFourFingerSwipe: {
if (!three_finger_swipe_enable_.val_)
break;
float sum_delta[] = { 0.0, 0.0 };
bool valid[] = { true, true };
float finger_cnt[] = { 0.0, 0.0 };
float FingerState::*fields[] = { &FingerState::position_x,
&FingerState::position_y };
for (FingerMap::const_iterator it =
fingers.begin(), e = fingers.end(); it != e; ++it) {
if (!state_buffer_.Get(1)->GetFingerState(*it)) {
Err("missing prev state?");
continue;
}
// We have this loop in case we want to compute diagonal swipes at
// some point, even if currently we go with just one axis.
for (size_t i = 0; i < arraysize(fields); i++) {
bool correct_axis = (i == 1) == swipe_is_vertical_;
if (!valid[i] || !correct_axis)
continue;
float FingerState::*field = fields[i];
float delta = hwstate.GetFingerState(*it)->*field -
state_buffer_.Get(1)->GetFingerState(*it)->*field;
// The multiply is to see if they have the same sign:
if (sum_delta[i] == 0.0 || sum_delta[i] * delta > 0) {
sum_delta[i] += delta;
finger_cnt[i] += 1.0;
} else {
sum_delta[i] = 0.0;
valid[i] = false;
}
}
}
if (current_gesture_type_ == kGestureTypeSwipe) {
result_ = Gesture(
kGestureSwipe, state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
(!swipe_is_vertical_ && finger_cnt[0]) ?
sum_delta[0] / finger_cnt[0] : 0.0,
(swipe_is_vertical_ && finger_cnt[1]) ?
sum_delta[1] / finger_cnt[1] : 0.0);
} else if (current_gesture_type_ == kGestureTypeFourFingerSwipe) {
result_ = Gesture(
kGestureFourFingerSwipe, state_buffer_.Get(1)->timestamp,
hwstate.timestamp,
(!swipe_is_vertical_ && finger_cnt[0]) ?
sum_delta[0] / finger_cnt[0] : 0.0,
(swipe_is_vertical_ && finger_cnt[1]) ?
sum_delta[1] / finger_cnt[1] : 0.0);
}
break;
}
case kGestureTypeSwipeLift: {
result_ = Gesture(kGestureSwipeLift,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp);
break;
}
case kGestureTypeFourFingerSwipeLift: {
result_ = Gesture(kGestureFourFingerSwipeLift,
state_buffer_.Get(1)->timestamp,
hwstate.timestamp);
break;
}
case kGestureTypePinch: {
if (pinch_status_ == GESTURES_ZOOM_START ||
(pinch_status_ == GESTURES_ZOOM_END &&
prev_gesture_type_ == kGestureTypePinch)) {
result_ = Gesture(kGesturePinch, changed_time_, hwstate.timestamp,
1.0, pinch_status_);
pinch_prev_time_ = hwstate.timestamp;
if (pinch_status_ == GESTURES_ZOOM_END) {
current_gesture_type_ = kGestureTypeNull;
pinch_prev_direction_ = 0;
}
} else if (pinch_status_ == GESTURES_ZOOM_UPDATE) {
float current_dist_sq = TwoSpecificFingerDistanceSq(hwstate, fingers);
if (current_dist_sq < 0) {
current_dist_sq = pinch_prev_distance_sq_;
}
// Check if pinch scale has changed enough since last update to send a
// new update. To prevent stationary jitter, we always require the
// scale to change by at least a small amount. We require more change
// if the pinch has been stationary or changed direction recently.
float jitter_threshold = pinch_res_.val_;
if (hwstate.timestamp - pinch_prev_time_ > pinch_stationary_time_.val_)
jitter_threshold = pinch_stationary_res_.val_;
if ((current_dist_sq - pinch_prev_distance_sq_) *
pinch_prev_direction_ < 0)
jitter_threshold = jitter_threshold > pinch_hysteresis_res_.val_ ?
jitter_threshold :
pinch_hysteresis_res_.val_;
bool above_jitter_threshold =
(pinch_prev_distance_sq_ > jitter_threshold * current_dist_sq ||
current_dist_sq > jitter_threshold * pinch_prev_distance_sq_);
if (above_jitter_threshold) {
result_ = Gesture(kGesturePinch, changed_time_, hwstate.timestamp,
sqrt(current_dist_sq / pinch_prev_distance_sq_),
GESTURES_ZOOM_UPDATE);
pinch_prev_direction_ =
current_dist_sq > pinch_prev_distance_sq_ ? 1 : -1;
pinch_prev_distance_sq_ = current_dist_sq;
pinch_prev_time_ = hwstate.timestamp;
}
}
if (pinch_status_ == GESTURES_ZOOM_START) {
pinch_status_ = GESTURES_ZOOM_UPDATE;
// If there is a slow pinch, it may take a little while to detect it,
// allowing the fingers to travel a significant distance, and causing an
// inappropriately large scale in a single frame, followed by slow
// scaling. Here we reduce the initial scale factor depending on how
// quickly we detected the pinch.
float current_dist_sq = TwoSpecificFingerDistanceSq(hwstate, fingers);
float pinch_slowness_ratio = (hwstate.timestamp - changed_time_) *
pinch_initial_scale_time_inv_.val_;
pinch_slowness_ratio = fmin(1.0, pinch_slowness_ratio);
pinch_prev_distance_sq_ =
(pinch_slowness_ratio * current_dist_sq) +
((1 - pinch_slowness_ratio) * pinch_prev_distance_sq_);
}
break;
}
default:
result_.type = kGestureTypeNull;
}
scroll_manager_.UpdateScrollEventBuffer(current_gesture_type_,
&scroll_buffer_);
if ((result_.type == kGestureTypeMove && !zero_move) ||
result_.type == kGestureTypeScroll)
last_movement_timestamp_ = hwstate.timestamp;
}
void ImmediateInterpreter::IntWasWritten(IntProperty* prop) {
if (prop == &keyboard_touched_timeval_low_) {
struct timeval tv = {
keyboard_touched_timeval_high_.val_,
keyboard_touched_timeval_low_.val_
};
keyboard_touched_ = StimeFromTimeval(&tv);
}
}
void ImmediateInterpreter::Initialize(const HardwareProperties* hwprops,
Metrics* metrics,
MetricsProperties* mprops,
GestureConsumer* consumer) {
Interpreter::Initialize(hwprops, metrics, mprops, consumer);
state_buffer_.Reset(hwprops_->max_finger_cnt);
}
bool AnyGesturingFingerLeft(const HardwareState& state,
const FingerMap& prev_gs_fingers) {
for (FingerMap::const_iterator it = prev_gs_fingers.begin(),
e = prev_gs_fingers.end(); it != e; ++it) {
if (!state.GetFingerState(*it)) {
return true;
}
}
return false;
}
} // namespace gestures