Added side slide gestures to touch exploration controller.
The user can control settings that might be normally changed using sliders by sliding along the edge of the screen when ChromeVox is on. For example, the user can slide along the right edge of the screen and adjust the volume.
If the user enters this mode and leaves the boundaries without releasing their touch, they will stop adjusting the setting, however, they will not enter touch exploration. If they return to the given boundaries, they will be able to modify the setting again. If the user does not touch a "hot edge" of the screen, they will not enter this state if they move to the an of the screen.
BUG=393326
Review URL: https://codereview.chromium.org/385073009
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@284819 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/ash/ash.gyp b/ash/ash.gyp
index 1f1e85f..554564a 100644
--- a/ash/ash.gyp
+++ b/ash/ash.gyp
@@ -42,6 +42,8 @@
'ash_constants.h',
'ash_switches.cc',
'ash_switches.h',
+ 'ash_touch_exploration_manager_chromeos.cc',
+ 'ash_touch_exploration_manager_chromeos.h',
'cancel_mode.cc',
'cancel_mode.h',
'debug.cc',
@@ -753,6 +755,7 @@
'accelerators/accelerator_table_unittest.cc',
'accelerators/magnifier_key_scroller_unittest.cc',
'accelerators/spoken_feedback_toggler_unittest.cc',
+ 'ash_touch_exploration_manager_chromeos_unittest.cc',
'autoclick/autoclick_unittest.cc',
'desktop_background/desktop_background_controller_unittest.cc',
'desktop_background/wallpaper_resizer_unittest.cc',
diff --git a/ash/ash_touch_exploration_manager_chromeos.cc b/ash/ash_touch_exploration_manager_chromeos.cc
new file mode 100644
index 0000000..f5e212b
--- /dev/null
+++ b/ash/ash_touch_exploration_manager_chromeos.cc
@@ -0,0 +1,78 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/ash_touch_exploration_manager_chromeos.h"
+
+#include "ash/accessibility_delegate.h"
+#include "ash/audio/sounds.h"
+#include "ash/root_window_controller.h"
+#include "ash/shell.h"
+#include "ash/system/tray/system_tray_notifier.h"
+#include "base/command_line.h"
+#include "chromeos/audio/chromeos_sounds.h"
+#include "chromeos/audio/cras_audio_handler.h"
+#include "chromeos/chromeos_switches.h"
+#include "ui/chromeos/touch_exploration_controller.h"
+
+namespace ash {
+
+AshTouchExplorationManager::AshTouchExplorationManager(
+ RootWindowController* root_window_controller)
+ : root_window_controller_(root_window_controller),
+ audio_handler_(chromeos::CrasAudioHandler::Get()) {
+ Shell::GetInstance()->system_tray_notifier()->AddAccessibilityObserver(this);
+ UpdateTouchExplorationState();
+}
+
+AshTouchExplorationManager::~AshTouchExplorationManager() {
+ SystemTrayNotifier* system_tray_notifier =
+ Shell::GetInstance()->system_tray_notifier();
+ if (system_tray_notifier)
+ system_tray_notifier->RemoveAccessibilityObserver(this);
+}
+
+void AshTouchExplorationManager::OnAccessibilityModeChanged(
+ AccessibilityNotificationVisibility notify) {
+ UpdateTouchExplorationState();
+}
+
+void AshTouchExplorationManager::PlayVolumeAdjustSound() {
+ if (!VolumeAdjustSoundEnabled())
+ return;
+ if ((!audio_handler_->IsOutputMuted()) ||
+ !(audio_handler_->GetOutputVolumePercent() == 100))
+ PlaySystemSoundIfSpokenFeedback(chromeos::SOUND_VOLUME_ADJUST);
+}
+
+void AshTouchExplorationManager::SetOutputLevel(int volume) {
+ if (volume > 0) {
+ if (audio_handler_->IsOutputMuted()) {
+ audio_handler_->SetOutputMute(false);
+ }
+ }
+ audio_handler_->SetOutputVolumePercent(volume);
+ // Avoid negative volume.
+ if (audio_handler_->IsOutputVolumeBelowDefaultMuteLevel())
+ audio_handler_->SetOutputMute(true);
+}
+
+void AshTouchExplorationManager::UpdateTouchExplorationState() {
+ AccessibilityDelegate* delegate =
+ Shell::GetInstance()->accessibility_delegate();
+ bool enabled = delegate->IsSpokenFeedbackEnabled();
+
+ if (enabled && !touch_exploration_controller_.get()) {
+ touch_exploration_controller_.reset(new ui::TouchExplorationController(
+ root_window_controller_->GetRootWindow(), this));
+ } else if (!enabled) {
+ touch_exploration_controller_.reset();
+ }
+}
+
+bool AshTouchExplorationManager::VolumeAdjustSoundEnabled() {
+ return !CommandLine::ForCurrentProcess()->HasSwitch(
+ chromeos::switches::kDisableVolumeAdjustSound);
+}
+
+} // namespace ash
diff --git a/ash/ash_touch_exploration_manager_chromeos.h b/ash/ash_touch_exploration_manager_chromeos.h
new file mode 100644
index 0000000..fc9cbc1
--- /dev/null
+++ b/ash/ash_touch_exploration_manager_chromeos.h
@@ -0,0 +1,52 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef ASH_TOUCH_EXPLORATION_MANAGER_CHROMEOS_H_
+#define ASH_TOUCH_EXPLORATION_MANAGER_CHROMEOS_H_
+
+#include "ash/ash_export.h"
+#include "ash/system/tray_accessibility.h"
+#include "ui/chromeos/touch_exploration_controller.h"
+
+namespace chromeos {
+class CrasAudioHandler;
+}
+
+namespace ash {
+class RootWindowController;
+
+// Responsible for initializing TouchExplorationController when spoken
+// feedback is on for ChromeOS only. This class implements
+// TouchExplorationControllerDelegate which allows touch gestures to manipulate
+// the system.
+class ASH_EXPORT AshTouchExplorationManager
+ : public ash::AccessibilityObserver,
+ public ui::TouchExplorationControllerDelegate {
+ public:
+ explicit AshTouchExplorationManager(
+ RootWindowController* root_window_controller);
+ virtual ~AshTouchExplorationManager();
+
+ // AccessibilityObserver overrides:
+ virtual void OnAccessibilityModeChanged(
+ AccessibilityNotificationVisibility notify) OVERRIDE;
+
+ // TouchExplorationControllerDelegate overrides:
+ virtual void PlayVolumeAdjustSound() OVERRIDE;
+ virtual void SetOutputLevel(int volume) OVERRIDE;
+
+ private:
+ void UpdateTouchExplorationState();
+ bool VolumeAdjustSoundEnabled();
+
+ scoped_ptr<ui::TouchExplorationController> touch_exploration_controller_;
+ RootWindowController* root_window_controller_;
+ chromeos::CrasAudioHandler* audio_handler_;
+
+ DISALLOW_COPY_AND_ASSIGN(AshTouchExplorationManager);
+};
+
+} // namespace ash
+
+#endif // ASH_TOUCH_EXPLORATION_MANAGER_CHROMEOS_H_
diff --git a/ash/ash_touch_exploration_manager_chromeos_unittest.cc b/ash/ash_touch_exploration_manager_chromeos_unittest.cc
new file mode 100644
index 0000000..4262cbf
--- /dev/null
+++ b/ash/ash_touch_exploration_manager_chromeos_unittest.cc
@@ -0,0 +1,39 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ash/ash_touch_exploration_manager_chromeos.h"
+
+#include "ash/root_window_controller.h"
+#include "ash/shell.h"
+#include "ash/test/ash_test_base.h"
+#include "chromeos/audio/cras_audio_handler.h"
+
+namespace ash {
+
+typedef test::AshTestBase AshTouchExplorationManagerTest;
+
+TEST_F(AshTouchExplorationManagerTest, AdjustSound) {
+ RootWindowController* controller = Shell::GetPrimaryRootWindowController();
+ AshTouchExplorationManager touch_exploration_manager(controller);
+ chromeos::CrasAudioHandler* audio_handler =
+ chromeos::CrasAudioHandler::Get();
+
+ touch_exploration_manager.SetOutputLevel(10);
+ EXPECT_EQ(audio_handler->GetOutputVolumePercent(), 10);
+ EXPECT_FALSE(audio_handler->IsOutputMuted());
+
+ touch_exploration_manager.SetOutputLevel(100);
+ EXPECT_EQ(audio_handler->GetOutputVolumePercent(), 100);
+ EXPECT_FALSE(audio_handler->IsOutputMuted());
+
+ touch_exploration_manager.SetOutputLevel(0);
+ EXPECT_EQ(audio_handler->GetOutputVolumePercent(), 0);
+ EXPECT_TRUE(audio_handler->IsOutputMuted());
+
+ touch_exploration_manager.SetOutputLevel(-10);
+ EXPECT_EQ(audio_handler->GetOutputVolumePercent(), 0);
+ EXPECT_TRUE(audio_handler->IsOutputMuted());
+}
+
+} // namespace ash
diff --git a/ash/root_window_controller.cc b/ash/root_window_controller.cc
index 26b925c..0e07dbe 100644
--- a/ash/root_window_controller.cc
+++ b/ash/root_window_controller.cc
@@ -76,7 +76,7 @@
#include "ui/wm/public/window_types.h"
#if defined(OS_CHROMEOS)
-#include "ash/system/tray_accessibility.h"
+#include "ash/ash_touch_exploration_manager_chromeos.h"
#include "ash/wm/boot_splash_screen_chromeos.h"
#include "ui/chromeos/touch_exploration_controller.h"
#endif
@@ -261,54 +261,6 @@
DISALLOW_COPY_AND_ASSIGN(EmptyWindowDelegate);
};
-#if defined(OS_CHROMEOS)
-// Responsible for initializing TouchExplorationController when spoken
-// feedback is on.
-class CrosAccessibilityObserver : public AccessibilityObserver {
- public:
- explicit CrosAccessibilityObserver(
- RootWindowController* root_window_controller)
- : root_window_controller_(root_window_controller) {
- Shell::GetInstance()->system_tray_notifier()->
- AddAccessibilityObserver(this);
- UpdateTouchExplorationState();
- }
-
- virtual ~CrosAccessibilityObserver() {
- SystemTrayNotifier* system_tray_notifier =
- Shell::GetInstance()->system_tray_notifier();
- if (system_tray_notifier)
- system_tray_notifier->RemoveAccessibilityObserver(this);
- }
-
- private:
- void UpdateTouchExplorationState() {
- AccessibilityDelegate* delegate =
- Shell::GetInstance()->accessibility_delegate();
- bool enabled = delegate->IsSpokenFeedbackEnabled();
-
- if (enabled && !touch_exploration_controller_.get()) {
- touch_exploration_controller_.reset(
- new ui::TouchExplorationController(
- root_window_controller_->GetRootWindow()));
- } else if (!enabled) {
- touch_exploration_controller_.reset();
- }
- }
-
- // Overridden from AccessibilityObserver.
- virtual void OnAccessibilityModeChanged(
- AccessibilityNotificationVisibility notify) OVERRIDE {
- UpdateTouchExplorationState();
- }
-
- scoped_ptr<ui::TouchExplorationController> touch_exploration_controller_;
- RootWindowController* root_window_controller_;
-
- DISALLOW_COPY_AND_ASSIGN(CrosAccessibilityObserver);
-};
-#endif // OS_CHROMEOS
-
} // namespace
void RootWindowController::CreateForPrimaryDisplay(AshWindowTreeHost* host) {
@@ -396,8 +348,8 @@
shell->RemoveShellObserver(this);
#if defined(OS_CHROMEOS)
- if (cros_accessibility_observer_) {
- cros_accessibility_observer_.reset();
+ if (touch_exploration_manager_) {
+ touch_exploration_manager_.reset();
}
#endif
@@ -807,7 +759,7 @@
#if defined(OS_CHROMEOS)
if (!CommandLine::ForCurrentProcess()->HasSwitch(
switches::kAshDisableTouchExplorationMode)) {
- cros_accessibility_observer_.reset(new CrosAccessibilityObserver(this));
+ touch_exploration_manager_.reset(new AshTouchExplorationManager(this));
}
#endif
}
diff --git a/ash/root_window_controller.h b/ash/root_window_controller.h
index 5c91f56..014aadc 100644
--- a/ash/root_window_controller.h
+++ b/ash/root_window_controller.h
@@ -68,7 +68,7 @@
#if defined(OS_CHROMEOS)
class BootSplashScreen;
-class AccessibilityObserver;
+class AshTouchExplorationManager;
#endif
// This class maintains the per root window state for ash. This class
@@ -298,7 +298,7 @@
scoped_ptr<BootSplashScreen> boot_splash_screen_;
// Responsible for initializing TouchExplorationController when spoken
// feedback is on.
- scoped_ptr<AccessibilityObserver> cros_accessibility_observer_;
+ scoped_ptr<AshTouchExplorationManager> touch_exploration_manager_;
#endif
scoped_ptr<ScreenDimmer> screen_dimmer_;
diff --git a/chrome/browser/ui/ash/volume_controller_chromeos.cc b/chrome/browser/ui/ash/volume_controller_chromeos.cc
index 8acade0..10a4114 100644
--- a/chrome/browser/ui/ash/volume_controller_chromeos.cc
+++ b/chrome/browser/ui/ash/volume_controller_chromeos.cc
@@ -67,7 +67,7 @@
audio_handler->SetOutputVolumePercent(0);
} else {
audio_handler->AdjustOutputVolumeByPercent(-kStepPercentage);
- if (audio_handler->IsOutputVolumeBelowDefaultMuteLvel())
+ if (audio_handler->IsOutputVolumeBelowDefaultMuteLevel())
audio_handler->SetOutputMute(true);
else
PlayVolumeAdjustSound();
diff --git a/chromeos/audio/cras_audio_handler.cc b/chromeos/audio/cras_audio_handler.cc
index 16805d6..1714b85 100644
--- a/chromeos/audio/cras_audio_handler.cc
+++ b/chromeos/audio/cras_audio_handler.cc
@@ -116,7 +116,7 @@
return audio_pref_handler_->GetMuteValue(*device);
}
-bool CrasAudioHandler::IsOutputVolumeBelowDefaultMuteLvel() {
+bool CrasAudioHandler::IsOutputVolumeBelowDefaultMuteLevel() {
return output_volume_ <= kMuteThresholdPercent;
}
diff --git a/chromeos/audio/cras_audio_handler.h b/chromeos/audio/cras_audio_handler.h
index cc28c50..fe4cee2 100644
--- a/chromeos/audio/cras_audio_handler.h
+++ b/chromeos/audio/cras_audio_handler.h
@@ -97,7 +97,7 @@
virtual bool IsInputMutedForDevice(uint64 device_id);
// Returns true if the output volume is below the default mute volume level.
- virtual bool IsOutputVolumeBelowDefaultMuteLvel();
+ virtual bool IsOutputVolumeBelowDefaultMuteLevel();
// Returns volume level in 0-100% range at which the volume should be muted.
virtual int GetOutputDefaultVolumeMuteThreshold();
diff --git a/ui/chromeos/touch_exploration_controller.cc b/ui/chromeos/touch_exploration_controller.cc
index 1a3b8fa..6bc08c7 100644
--- a/ui/chromeos/touch_exploration_controller.cc
+++ b/ui/chromeos/touch_exploration_controller.cc
@@ -4,7 +4,6 @@
#include "ui/chromeos/touch_exploration_controller.h"
-#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
@@ -12,6 +11,7 @@
#include "ui/aura/window_tree_host.h"
#include "ui/events/event.h"
#include "ui/events/event_processor.h"
+#include "ui/gfx/geometry/rect.h"
#define VLOG_STATE() if (VLOG_IS_ON(0)) VlogState(__func__)
#define VLOG_EVENT(event) if (VLOG_IS_ON(0)) VlogEvent(event, __func__)
@@ -19,13 +19,19 @@
namespace ui {
namespace {
+
+// Delay between adjustment sounds.
+const base::TimeDelta kSoundDelay = base::TimeDelta::FromMilliseconds(150);
+
// In ChromeOS, VKEY_LWIN is synonymous for the search key.
const ui::KeyboardCode kChromeOSSearchKey = ui::VKEY_LWIN;
} // namespace
TouchExplorationController::TouchExplorationController(
- aura::Window* root_window)
+ aura::Window* root_window,
+ TouchExplorationControllerDelegate* delegate)
: root_window_(root_window),
+ delegate_(delegate),
state_(NO_FINGERS_DOWN),
event_handler_for_testing_(NULL),
gesture_provider_(this),
@@ -77,6 +83,15 @@
current_touch_ids_.push_back(touch_id);
touch_locations_.insert(std::pair<int, gfx::PointF>(touch_id, location));
} else if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
+ // In order to avoid accidentally double tapping when moving off the edge of
+ // the screen, the state will be rewritten to NoFingersDown.
+ TouchEvent touch_event = static_cast<const TouchEvent&>(event);
+ if (FindEdgesWithinBounds(touch_event.location(), kLeavingScreenEdge) !=
+ NO_EDGE) {
+ if (current_touch_ids_.size() == 0)
+ ResetToNoFingersDown();
+ }
+
std::vector<int>::iterator it = std::find(
current_touch_ids_.begin(), current_touch_ids_.end(), touch_id);
@@ -121,6 +136,8 @@
return InPassthrough(touch_event, rewritten_event);
case WAIT_FOR_RELEASE:
return InWaitForRelease(touch_event, rewritten_event);
+ case SLIDE_GESTURE:
+ return InSlideGesture(touch_event, rewritten_event);
}
NOTREACHED();
return ui::EVENT_REWRITE_CONTINUE;
@@ -191,6 +208,14 @@
<< "\n Minimum swipe velocity: "
<< gesture_detector_config_.minimum_swipe_velocity;
+ // Change to slide gesture if the slide occurred at the right edge.
+ int edge = FindEdgesWithinBounds(event.location(), kMaxDistanceFromEdge);
+ if (edge & RIGHT_EDGE) {
+ state_ = SLIDE_GESTURE;
+ VLOG_STATE();
+ return InSlideGesture(event, rewritten_event);
+ }
+
// If the user moves fast enough from the initial touch location, start
// gesture detection. Otherwise, jump to the touch exploration mode early.
if (velocity > gesture_detector_config_.minimum_swipe_velocity) {
@@ -497,6 +522,69 @@
return EVENT_REWRITE_DISCARD;
}
+void TouchExplorationController::PlaySoundForTimer() {
+ delegate_->PlayVolumeAdjustSound();
+}
+
+ui::EventRewriteStatus TouchExplorationController::InSlideGesture(
+ const ui::TouchEvent& event,
+ scoped_ptr<ui::Event>* rewritten_event) {
+ // The timer should not fire when sliding.
+ if (tap_timer_.IsRunning())
+ tap_timer_.Stop();
+
+ ui::EventType type = event.type();
+ // If additional fingers are added before a swipe gesture has been registered,
+ // then wait until all fingers have been lifted.
+ if (type == ui::ET_TOUCH_PRESSED ||
+ event.touch_id() != initial_press_->touch_id()) {
+ if (sound_timer_.IsRunning())
+ sound_timer_.Stop();
+ // Discard any pending gestures.
+ ignore_result(gesture_provider_.GetAndResetPendingGestures());
+ state_ = WAIT_FOR_RELEASE;
+ return EVENT_REWRITE_DISCARD;
+ }
+
+ // Allows user to return to the edge to adjust the sound if they have left the
+ // boundaries.
+ int edge = FindEdgesWithinBounds(event.location(), kSlopDistanceFromEdge);
+ if (!(edge & RIGHT_EDGE) && (type != ui::ET_TOUCH_RELEASED)) {
+ if (sound_timer_.IsRunning()) {
+ sound_timer_.Stop();
+ }
+ return EVENT_REWRITE_DISCARD;
+ }
+
+ // This can occur if the user leaves the screen edge and then returns to it to
+ // continue adjusting the sound.
+ if (!sound_timer_.IsRunning()) {
+ sound_timer_.Start(FROM_HERE,
+ kSoundDelay,
+ this,
+ &ui::TouchExplorationController::PlaySoundForTimer);
+ delegate_->PlayVolumeAdjustSound();
+ }
+
+ // There should not be more than one finger down.
+ DCHECK(current_touch_ids_.size() <= 1);
+ if (type == ui::ET_TOUCH_MOVED) {
+ gesture_provider_.OnTouchEvent(event);
+ gesture_provider_.OnTouchEventAck(false);
+ }
+ if (type == ui::ET_TOUCH_RELEASED || type == ui::ET_TOUCH_CANCELLED) {
+ gesture_provider_.OnTouchEvent(event);
+ gesture_provider_.OnTouchEventAck(false);
+ ignore_result(gesture_provider_.GetAndResetPendingGestures());
+ if (current_touch_ids_.size() == 0)
+ ResetToNoFingersDown();
+ return ui::EVENT_REWRITE_DISCARD;
+ }
+
+ ProcessGestureEvents();
+ return ui::EVENT_REWRITE_DISCARD;
+}
+
void TouchExplorationController::OnTapTimerFired() {
switch (state_) {
case SINGLE_TAP_RELEASED:
@@ -521,7 +609,7 @@
CreateMouseMoveEvent(initial_press_->location(), initial_press_->flags());
DispatchEvent(mouse_move.get());
last_touch_exploration_.reset(new TouchEvent(*initial_press_));
- }
+}
void TouchExplorationController::DispatchEvent(ui::Event* event) {
if (event_handler_for_testing_) {
@@ -532,12 +620,14 @@
root_window_->GetHost()->dispatcher()->OnEventFromSource(event);
}
-void TouchExplorationController::OnGestureEvent(ui::GestureEvent* gesture) {
+void TouchExplorationController::OnGestureEvent(
+ ui::GestureEvent* gesture) {
CHECK(gesture->IsGestureEvent());
+ ui::EventType type = gesture->type();
VLOG(0) << " \n Gesture Triggered: " << gesture->name();
- if (gesture->type() == ui::ET_GESTURE_SWIPE) {
- if (tap_timer_.IsRunning())
- tap_timer_.Stop();
+ if (type == ui::ET_GESTURE_SWIPE && state_ != SLIDE_GESTURE) {
+ VLOG(0) << "Swipe!";
+ ignore_result(gesture_provider_.GetAndResetPendingGestures());
OnSwipeEvent(gesture);
return;
}
@@ -550,11 +640,61 @@
for (ScopedVector<GestureEvent>::iterator i = gestures->begin();
i != gestures->end();
++i) {
- OnGestureEvent(*i);
+ if (state_ == SLIDE_GESTURE)
+ SideSlideControl(*i);
+ else
+ OnGestureEvent(*i);
}
}
}
+void TouchExplorationController::SideSlideControl(ui::GestureEvent* gesture) {
+ ui::EventType type = gesture->type();
+ if (!gesture->IsScrollGestureEvent())
+ return;
+
+ if (type == ET_GESTURE_SCROLL_BEGIN) {
+ delegate_->PlayVolumeAdjustSound();
+ }
+
+ if (type == ET_GESTURE_SCROLL_END) {
+ if (sound_timer_.IsRunning())
+ sound_timer_.Stop();
+ delegate_->PlayVolumeAdjustSound();
+ }
+
+ // If the user is in the corner of the right side of the screen, the volume
+ // will be automatically set to 100% or muted depending on which corner they
+ // are in. Otherwise, the user will be able to adjust the volume by sliding
+ // their finger along the right side of the screen. Volume is relative to
+ // where they are on the right side of the screen.
+ gfx::Point location = gesture->location();
+ int edge = FindEdgesWithinBounds(location, kSlopDistanceFromEdge);
+ if (!(edge & RIGHT_EDGE))
+ return;
+
+ if (edge & TOP_EDGE) {
+ delegate_->SetOutputLevel(100);
+ return;
+ }
+ if (edge & BOTTOM_EDGE) {
+ delegate_->SetOutputLevel(0);
+ return;
+ }
+
+ location = gesture->location();
+ root_window_->GetHost()->ConvertPointFromNativeScreen(&location);
+ float volume_adjust_height =
+ root_window_->bounds().height() - 2 * kMaxDistanceFromEdge;
+ float ratio = (location.y() - kMaxDistanceFromEdge) / volume_adjust_height;
+ float volume = 100 - 100 * ratio;
+ VLOG(0) << "\n Volume = " << volume << "\n Location = " << location.ToString()
+ << "\n Bounds = " << root_window_->bounds().right();
+
+ delegate_->SetOutputLevel(int(volume));
+}
+
+
void TouchExplorationController::OnSwipeEvent(ui::GestureEvent* swipe_gesture) {
// A swipe gesture contains details for the direction in which the swipe
// occurred.
@@ -574,6 +714,34 @@
}
}
+int TouchExplorationController::FindEdgesWithinBounds(gfx::Point point,
+ float bounds) {
+ // Since GetBoundsInScreen is in DIPs but point is not, then point needs to be
+ // converted.
+ root_window_->GetHost()->ConvertPointFromNativeScreen(&point);
+ gfx::Rect window = root_window_->GetBoundsInScreen();
+
+ float left_edge_limit = window.x() + bounds;
+ float right_edge_limit = window.right() - bounds;
+ float top_edge_limit = window.y() + bounds;
+ float bottom_edge_limit = window.bottom() - bounds;
+
+ // Bitwise manipulation in order to determine where on the screen the point
+ // lies. If more than one bit is turned on, then it is a corner where the two
+ // bit/edges intersect. Otherwise, if no bits are turned on, the point must be
+ // in the center of the screen.
+ int result = NO_EDGE;
+ if (point.x() < left_edge_limit)
+ result |= LEFT_EDGE;
+ if (point.x() > right_edge_limit)
+ result |= RIGHT_EDGE;
+ if (point.y() < top_edge_limit)
+ result |= TOP_EDGE;
+ if (point.y() > bottom_edge_limit)
+ result |= BOTTOM_EDGE;
+ return result;
+}
+
void TouchExplorationController::DispatchShiftSearchKeyEvent(
const ui::KeyboardCode direction) {
// In order to activate the shortcut shift+search+<arrow key>
@@ -623,6 +791,9 @@
}
void TouchExplorationController::ResetToNoFingersDown() {
+ ProcessGestureEvents();
+ if (sound_timer_.IsRunning())
+ sound_timer_.Stop();
state_ = NO_FINGERS_DOWN;
VLOG_STATE();
if (tap_timer_.IsRunning())
@@ -694,6 +865,8 @@
return "PASSTHROUGH";
case WAIT_FOR_RELEASE:
return "WAIT_FOR_RELEASE";
+ case SLIDE_GESTURE:
+ return "SLIDE_GESTURE";
}
return "Not a state";
}
diff --git a/ui/chromeos/touch_exploration_controller.h b/ui/chromeos/touch_exploration_controller.h
index 17731ff..12f0bfa 100644
--- a/ui/chromeos/touch_exploration_controller.h
+++ b/ui/chromeos/touch_exploration_controller.h
@@ -26,9 +26,25 @@
class GestureProviderAura;
class TouchEvent;
+// A delegate to handle commands in response to detected accessibility gesture
+// events.
+class TouchExplorationControllerDelegate {
+ public:
+ virtual ~TouchExplorationControllerDelegate() {}
+
+ // This function should be called whenever the delegate wants to play a sound
+ // when the volume adjusts.
+ virtual void PlayVolumeAdjustSound() = 0;
+
+ // Takes an int from 0.0 to 100.0 that indicates the percent the volume
+ // should be set to.
+ virtual void SetOutputLevel(int volume) = 0;
+};
+
// TouchExplorationController is used in tandem with "Spoken Feedback" to
-// make the touch UI accessible. Gestures are mapped to accessiblity key
-// shortcuts.
+// make the touch UI accessible. Gestures performed in the middle of the screen
+// are mapped to accessiblity key shortcuts while gestures performed on the edge
+// of the screen can change settings.
//
// ** Short version **
//
@@ -40,7 +56,10 @@
// right would correspond to the keyboard short cut shift+search+right.
// When two or more fingers are pressed initially, from then on the events
// are passed through, but with the initial finger removed - so if you swipe
-// down with two fingers, the running app will see a one-finger swipe.
+// down with two fingers, the running app will see a one-finger swipe. Slide
+// gestures performed on the edge of the screen can change settings
+// continuously. For example, sliding a finger along the right side of the
+// screen will change the volume.
//
// ** Long version **
//
@@ -91,6 +110,24 @@
// adds a third finger while in two to one finger mode, all fingers and touch
// events are passed through from then on.
//
+// If the user places a finger on the edge of the screen and moves their finger
+// past slop, a slide gesture is performed. The user can then slide one finger
+// along an edge of the screen and continuously control a setting. Once the user
+// enters this state, the boundaries that define an edge expand so that the user
+// can now adjust the setting within a slightly bigger width along the screen.
+// If the user exits this area without lifting their finger, they will not be
+// able to perform any actions, however if they keep their finger down and
+// return to the "hot edge," then they can still adjust the setting. In order to
+// perform other touch accessibility movements, the user must lift their finger.
+// If additional fingers are added while in this state, the user will transition
+// to passthrough.
+//
+// Currently, only the right edge is mapped to control the volume. Volume
+// control along the edge of the screen is directly proportional to where the
+// user's finger is located on the screen. The top right corner of the screen
+// automatically sets the volume to 100% and the bottome right corner of the
+// screen automatically sets the volume to 0% once the user has moved past slop.
+//
// Once touch exploration mode has been activated,
// it remains in that mode until all fingers have been released.
//
@@ -100,7 +137,9 @@
: public ui::EventRewriter,
public ui::GestureProviderAuraClient {
public:
- explicit TouchExplorationController(aura::Window* root_window);
+ explicit TouchExplorationController(
+ aura::Window* root_window,
+ ui::TouchExplorationControllerDelegate* delegate);
virtual ~TouchExplorationController();
private:
@@ -134,6 +173,8 @@
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
ui::EventRewriteStatus InWaitForRelease(
const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
+ ui::EventRewriteStatus InSlideGesture(
+ const ui::TouchEvent& event, scoped_ptr<ui::Event>* rewritten_event);
// This timer is started every time we get the first press event, and
// it fires after the double-click timeout elapses (300 ms by default).
@@ -157,6 +198,8 @@
void OnSwipeEvent(ui::GestureEvent* swipe_gesture);
+ void SideSlideControl(ui::GestureEvent* gesture);
+
// Dispatches the keyboard short cut Shift+Search+<arrow key>
// outside the event rewritting flow.
void DispatchShiftSearchKeyEvent(const ui::KeyboardCode direction);
@@ -170,6 +213,22 @@
// default value.
void ResetToNoFingersDown();
+ void PlaySoundForTimer();
+
+ // Some constants used in touch_exploration_controller:
+
+ // Within this many dips of the screen edge, the release event generated will
+ // reset the state to NoFingersDown.
+ const float kLeavingScreenEdge = 6;
+
+ // Swipe/scroll gestures within these bounds (in DIPs) will change preset
+ // settings.
+ const float kMaxDistanceFromEdge = 75;
+
+ // After a slide gesture has been triggered, if the finger is still within
+ // these bounds (in DIPs), the preset settings will still change.
+ const float kSlopDistanceFromEdge = kMaxDistanceFromEdge + 40;
+
enum State {
// No fingers are down and no events are pending.
NO_FINGERS_DOWN,
@@ -236,8 +295,28 @@
// generally useful for developing new features, because it creates a
// simple way to handle a dead end in user flow.
WAIT_FOR_RELEASE,
+
+ // If the user is within the given bounds from an edge of the screen, not
+ // including corners, then the resulting movements will be interpreted as
+ // slide gestures.
+ SLIDE_GESTURE,
};
+ enum ScreenLocation {
+ // Hot "edges" of the screen are each represented by a respective bit.
+ NO_EDGE = 0,
+ RIGHT_EDGE = 1 << 0,
+ TOP_EDGE = 1 << 1,
+ LEFT_EDGE = 1 << 2,
+ BOTTOM_EDGE = 1 << 3,
+ };
+
+ // Given a point, if it is within the given bounds of an edge, returns the
+ // edge. If it is within the given bounds of two edges, returns an int with
+ // both bits that represent the respective edges turned on. Otherwise returns
+ // SCREEN_CENTER.
+ int FindEdgesWithinBounds(gfx::Point point, float bounds);
+
void VlogState(const char* function_name);
void VlogEvent(const ui::TouchEvent& event, const char* function_name);
@@ -247,6 +326,9 @@
aura::Window* root_window_;
+ // Handles volume control. Not owned.
+ ui::TouchExplorationControllerDelegate* delegate_;
+
// A set of touch ids for fingers currently touching the screen.
std::vector<int> current_touch_ids_;
@@ -276,6 +358,9 @@
// A timer to fire the mouse move event after the double-tap delay.
base::OneShotTimer<TouchExplorationController> tap_timer_;
+ // A timer to fire an indicating sound when sliding to change volume.
+ base::RepeatingTimer<TouchExplorationController> sound_timer_;
+
// For testing only, an event handler to use for generated events
// outside of the normal event rewriting flow.
ui::EventHandler* event_handler_for_testing_;
diff --git a/ui/chromeos/touch_exploration_controller_unittest.cc b/ui/chromeos/touch_exploration_controller_unittest.cc
index 70bac63..d190578 100644
--- a/ui/chromeos/touch_exploration_controller_unittest.cc
+++ b/ui/chromeos/touch_exploration_controller_unittest.cc
@@ -72,6 +72,24 @@
return n * Factorial(n - 1);
}
+class MockTouchExplorationControllerDelegate
+ : public ui::TouchExplorationControllerDelegate {
+ public:
+ virtual void PlayVolumeAdjustSound() OVERRIDE {
+ ++num_times_adjust_sound_played_;
+ }
+ virtual void SetOutputLevel(int volume) OVERRIDE {
+ volume_changes_.push_back(volume);
+ }
+
+ const std::vector<float> VolumeChanges() { return volume_changes_; }
+ const size_t NumAdjustSounds() { return num_times_adjust_sound_played_; }
+
+ private:
+ std::vector<float> volume_changes_;
+ size_t num_times_adjust_sound_played_ = 0;
+};
+
} // namespace
class TouchExplorationControllerTestApi {
@@ -110,12 +128,29 @@
touch_exploration_controller_->GESTURE_IN_PROGRESS;
}
+ bool IsInSlideGestureStateForTesting() const {
+ return touch_exploration_controller_->state_ ==
+ touch_exploration_controller_->SLIDE_GESTURE;
+ }
+
+ gfx::Rect BoundsOfRootWindowInDIPForTesting() const {
+ return touch_exploration_controller_->root_window_->GetBoundsInScreen();
+ }
+
// VLOGs should be suppressed in tests that generate a lot of logs,
// for example permutations of nine touch events.
void SuppressVLOGsForTesting(bool suppress) {
touch_exploration_controller_->VLOG_on_ = !suppress;
}
+ float GetMaxDistanceFromEdge() const {
+ return touch_exploration_controller_->kMaxDistanceFromEdge;
+ }
+
+ float GetSlopDistanceFromEdge() const {
+ return touch_exploration_controller_->kSlopDistanceFromEdge;
+ }
+
private:
scoped_ptr<TouchExplorationController> touch_exploration_controller_;
@@ -212,8 +247,9 @@
if (!on && touch_exploration_controller_.get()) {
touch_exploration_controller_.reset();
} else if (on && !touch_exploration_controller_.get()) {
- touch_exploration_controller_.reset(new TouchExplorationControllerTestApi(
- new ui::TouchExplorationController(root_window())));
+ touch_exploration_controller_.reset(
+ new ui::TouchExplorationControllerTestApi(
+ new TouchExplorationController(root_window(), &delegate_)));
touch_exploration_controller_->SetEventHandlerForTesting(
&event_capturer_);
cursor_client()->ShowCursor();
@@ -256,6 +292,22 @@
->IsInGestureInProgressStateForTesting();
}
+ bool IsInSlideGestureState() {
+ return touch_exploration_controller_->IsInSlideGestureStateForTesting();
+ }
+
+ gfx::Rect BoundsOfRootWindowInDIP() {
+ return touch_exploration_controller_->BoundsOfRootWindowInDIPForTesting();
+ }
+
+ float GetMaxDistanceFromEdge() const{
+ return touch_exploration_controller_->GetMaxDistanceFromEdge();
+ }
+
+ float GetSlopDistanceFromEdge() const{
+ return touch_exploration_controller_->GetSlopDistanceFromEdge();
+ }
+
base::TimeDelta Now() {
// This is the same as what EventTimeForNow() does, but here we do it
// with our simulated clock.
@@ -267,6 +319,7 @@
ui::GestureDetector::Config gesture_detector_config_;
// Owned by |generator_|.
base::SimpleTestTickClock* simulated_clock_;
+ MockTouchExplorationControllerDelegate delegate_;
private:
EventCapturer event_capturer_;
@@ -509,6 +562,9 @@
TEST_F(TouchExplorationTest, TimerFiresLateDuringTouchExploration) {
SwitchTouchExplorationMode(true);
+ // Make sure the touch is not in a corner of the screen.
+ generator_->MoveTouch(gfx::Point(100, 200));
+
// Send a press, then add another finger after the double-tap timeout.
generator_->PressTouchId(1);
simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(1000));
@@ -1357,10 +1413,11 @@
EXPECT_FALSE(IsInGestureInProgressState());
float distance = gesture_detector_config_.touch_slop + 1;
- ui::TouchEvent first_press(ui::ET_TOUCH_PRESSED, gfx::Point(0, 1), 0, Now());
+ ui::TouchEvent first_press(
+ ui::ET_TOUCH_PRESSED, gfx::Point(100, 200), 0, Now());
generator_->Dispatch(&first_press);
simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
- gfx::Point second_location(distance, 1);
+ gfx::Point second_location(100 + distance, 200);
generator_->MoveTouch(second_location);
EXPECT_TRUE(IsInGestureInProgressState());
EXPECT_FALSE(IsInTouchToMouseMode());
@@ -1388,4 +1445,193 @@
ASSERT_EQ(0U, captured_events.size());
}
+TEST_F(TouchExplorationTest, EnterSlideGestureState) {
+ SwitchTouchExplorationMode(true);
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_FALSE(IsInGestureInProgressState());
+
+ gfx::Rect window = BoundsOfRootWindowInDIP();
+ float distance = gesture_detector_config_.touch_slop + 1;
+ ui::TouchEvent first_press(
+ ui::ET_TOUCH_PRESSED, gfx::Point(window.right(), 1), 0, Now());
+ gfx::Point second_location(window.right(), 1 + distance / 2);
+ gfx::Point third_location(window.right(), 1 + distance);
+
+ generator_->Dispatch(&first_press);
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ // Since we haven't moved past slop yet, we should not be in slide gesture.
+ generator_->MoveTouch(second_location);
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ // Once we are out of slop, we should be in slide gesture since we are along
+ // the edge of the screen.
+ generator_->MoveTouch(third_location);
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_TRUE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ const ScopedVector<ui::Event>& captured_events = GetCapturedEvents();
+ ASSERT_EQ(0U, captured_events.size());
+
+ // Since we are at the right edge of the screen, but the sound timer has not
+ // elapsed, there should have two sounds that fired and two volume
+ // changes (one for each movement).
+ size_t num_adjust_sounds = delegate_.NumAdjustSounds();
+ ASSERT_EQ(2U, num_adjust_sounds);
+ ASSERT_EQ(2U, delegate_.VolumeChanges().size());
+
+ // Exit out of slide gesture once touch is lifted, but not before even if the
+ // grace period is over.
+
+ AdvanceSimulatedTimePastPotentialTapDelay();
+ ASSERT_EQ(0U, captured_events.size());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_TRUE(IsInSlideGestureState());
+
+ generator_->ReleaseTouch();
+ ASSERT_EQ(0U, captured_events.size());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+}
+
+// If a press + move occurred outside the boundaries, but within the slop
+// boundaries and then moved into the boundaries of an edge, there still should
+// not be a slide gesture.
+TEST_F(TouchExplorationTest, AvoidEnteringSlideGesture) {
+ SwitchTouchExplorationMode(true);
+
+ gfx::Rect window = BoundsOfRootWindowInDIP();
+ float distance = gesture_detector_config_.touch_slop + 1;
+ ui::TouchEvent first_press(
+ ui::ET_TOUCH_PRESSED,
+ gfx::Point(window.right() - GetSlopDistanceFromEdge(), 1),
+ 0,
+ Now());
+ gfx::Point out_of_slop(window.right() - GetSlopDistanceFromEdge() + distance,
+ 1);
+ gfx::Point into_boundaries(window.right() - GetMaxDistanceFromEdge() / 2, 1);
+
+ generator_->Dispatch(&first_press);
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ generator_->MoveTouch(out_of_slop);
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_TRUE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ // Since we did not start moving while in the boundaries, we should not be in
+ // slide gestures.
+ generator_->MoveTouch(into_boundaries);
+ EXPECT_TRUE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ const ScopedVector<ui::Event>& captured_events = GetCapturedEvents();
+ ASSERT_EQ(0U, captured_events.size());
+
+ generator_->ReleaseTouch();
+}
+
+// If the slide gesture begins within the boundaries and then moves
+// SlopDistanceFromEdge there should still be a sound change. If the finger
+// moves into the center screen, there should no longer be a sound change but it
+// should still be in slide gesture. If the finger moves back into the edges
+// without lifting, it should start changing sound again.
+TEST_F(TouchExplorationTest, TestingBoundaries) {
+ SwitchTouchExplorationMode(true);
+
+ gfx::Rect window = BoundsOfRootWindowInDIP();
+ gfx::Point initial_press(window.right() - GetMaxDistanceFromEdge() / 2, 1);
+ ui::TouchEvent first_press(ui::ET_TOUCH_PRESSED, initial_press, 0, Now());
+ gfx::Point touch_move(initial_press.x() + gesture_detector_config_.touch_slop,
+ 1);
+ gfx::Point into_slop_boundaries(
+ window.right() - GetSlopDistanceFromEdge() / 2, 1);
+ gfx::Point center_screen(window.right() / 2, window.bottom() / 2);
+
+ generator_->Dispatch(&first_press);
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ generator_->MoveTouch(touch_move);
+ EXPECT_FALSE(IsInTouchToMouseMode());
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(10));
+
+ // Move the touch into slop boundaries. It should stil be in slide gestures
+ // and adjust the volume.
+ generator_->MoveTouch(into_slop_boundaries);
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_TRUE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+
+ // The sound is rate limiting so it only activates every 150ms.
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(200));
+
+ size_t num_adjust_sounds = delegate_.NumAdjustSounds();
+ ASSERT_EQ(2U, num_adjust_sounds);
+ ASSERT_EQ(2U, delegate_.VolumeChanges().size());
+
+ // Move the touch into the center of the window. It should still be in slide
+ // gestures, but there should not be anymore volume adjustments.
+ generator_->MoveTouch(center_screen);
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_TRUE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(200));
+ num_adjust_sounds = delegate_.NumAdjustSounds();
+ ASSERT_EQ(2U, num_adjust_sounds);
+ ASSERT_EQ(2U, delegate_.VolumeChanges().size());
+
+ // Move the touch back into slop edge distance and volume should be changing
+ // again.
+ generator_->MoveTouch(into_slop_boundaries);
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_TRUE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+
+ generator_->MoveTouch(
+ gfx::Point(into_slop_boundaries.x() + gesture_detector_config_.touch_slop,
+ into_slop_boundaries.y()));
+ simulated_clock_->Advance(base::TimeDelta::FromMilliseconds(200));
+
+ num_adjust_sounds = delegate_.NumAdjustSounds();
+ ASSERT_EQ(3U, num_adjust_sounds);
+ ASSERT_EQ(3U, delegate_.VolumeChanges().size());
+
+ const ScopedVector<ui::Event>& captured_events = GetCapturedEvents();
+ ASSERT_EQ(0U, captured_events.size());
+
+ generator_->ReleaseTouch();
+}
+
+// Even if the gesture starts within bounds, if it has not moved past slop
+// within the grace period, it should go to touch exploration.
+TEST_F(TouchExplorationTest, InBoundariesTouchExploration) {
+ SwitchTouchExplorationMode(true);
+
+ gfx::Rect window = BoundsOfRootWindowInDIP();
+ gfx::Point initial_press(window.right() - GetMaxDistanceFromEdge() / 2, 1);
+ ui::TouchEvent first_press(
+ ui::ET_TOUCH_PRESSED,
+ initial_press,
+ 0,
+ Now());
+ generator_->Dispatch(&first_press);
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ EXPECT_FALSE(IsInTouchToMouseMode());
+
+ AdvanceSimulatedTimePastTapDelay();
+ EXPECT_FALSE(IsInGestureInProgressState());
+ EXPECT_FALSE(IsInSlideGestureState());
+ EXPECT_TRUE(IsInTouchToMouseMode());
+}
+
} // namespace ui