1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/highlighter/highlighter_controller.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "ash/highlighter/highlighter_gesture_util.h"
11 #include "ash/highlighter/highlighter_result_view.h"
12 #include "ash/highlighter/highlighter_view.h"
13 #include "ash/public/cpp/shell_window_ids.h"
14 #include "ash/shell.h"
15 #include "ash/system/palette/palette_utils.h"
16 #include "base/bind.h"
17 #include "base/metrics/histogram_macros.h"
18 #include "base/timer/timer.h"
19 #include "chromeos/constants/chromeos_switches.h"
20 #include "ui/aura/window.h"
21 #include "ui/aura/window_tree_host.h"
22 #include "ui/events/base_event_utils.h"
23 #include "ui/views/widget/widget.h"
24
25 namespace ash {
26
27 namespace {
28
29 // Bezel stroke detection margin, in DP.
30 const int kScreenEdgeMargin = 2;
31
32 const int kInterruptedStrokeTimeoutMs = 500;
33
34 // Adjust the height of the bounding box to match the pen tip height,
35 // while keeping the same vertical center line. Adjust the width to
36 // account for the pen tip width.
AdjustHorizontalStroke(const gfx::RectF & box,const gfx::SizeF & pen_tip_size)37 gfx::RectF AdjustHorizontalStroke(const gfx::RectF& box,
38 const gfx::SizeF& pen_tip_size) {
39 return gfx::RectF(box.x() - pen_tip_size.width() / 2,
40 box.CenterPoint().y() - pen_tip_size.height() / 2,
41 box.width() + pen_tip_size.width(), pen_tip_size.height());
42 }
43
44 } // namespace
45
HighlighterController()46 HighlighterController::HighlighterController() {
47 Shell::Get()->AddPreTargetHandler(this);
48 }
49
~HighlighterController()50 HighlighterController::~HighlighterController() {
51 Shell::Get()->RemovePreTargetHandler(this);
52 }
53
AddObserver(Observer * observer)54 void HighlighterController::AddObserver(Observer* observer) {
55 DCHECK(observer);
56 observers_.AddObserver(observer);
57 }
58
RemoveObserver(Observer * observer)59 void HighlighterController::RemoveObserver(Observer* observer) {
60 DCHECK(observer);
61 observers_.RemoveObserver(observer);
62 }
63
SetExitCallback(base::OnceClosure exit_callback,bool require_success)64 void HighlighterController::SetExitCallback(base::OnceClosure exit_callback,
65 bool require_success) {
66 exit_callback_ = std::move(exit_callback);
67 require_success_ = require_success;
68 }
69
UpdateEnabledState(HighlighterEnabledState enabled_state)70 void HighlighterController::UpdateEnabledState(
71 HighlighterEnabledState enabled_state) {
72 if (enabled_state_ == enabled_state)
73 return;
74 enabled_state_ = enabled_state;
75
76 SetEnabled(enabled_state == HighlighterEnabledState::kEnabled);
77 for (auto& observer : observers_)
78 observer.OnHighlighterEnabledChanged(enabled_state);
79 }
80
AbortSession()81 void HighlighterController::AbortSession() {
82 if (enabled_state_ == HighlighterEnabledState::kEnabled)
83 UpdateEnabledState(HighlighterEnabledState::kDisabledBySessionAbort);
84 }
85
SetEnabled(bool enabled)86 void HighlighterController::SetEnabled(bool enabled) {
87 FastInkPointerController::SetEnabled(enabled);
88 if (enabled) {
89 session_start_ = ui::EventTimeForNow();
90 gesture_counter_ = 0;
91 recognized_gesture_counter_ = 0;
92 } else {
93 UMA_HISTOGRAM_COUNTS_100("Ash.Shelf.Palette.Assistant.GesturesPerSession",
94 gesture_counter_);
95 UMA_HISTOGRAM_COUNTS_100(
96 "Ash.Shelf.Palette.Assistant.GesturesPerSession.Recognized",
97 recognized_gesture_counter_);
98
99 // If |highlighter_view_widget_| is animating it will delete itself when
100 // done animating. |result_view_widget_| will exist only if
101 // |highlighter_view_widget_| is animating, and it will also delete itself
102 // when done animating.
103 if (highlighter_view_widget_ && !GetHighlighterView()->animating())
104 DestroyPointerView();
105 }
106 }
107
GetPointerView() const108 views::View* HighlighterController::GetPointerView() const {
109 return const_cast<HighlighterController*>(this)->GetHighlighterView();
110 }
111
CreatePointerView(base::TimeDelta presentation_delay,aura::Window * root_window)112 void HighlighterController::CreatePointerView(
113 base::TimeDelta presentation_delay,
114 aura::Window* root_window) {
115 highlighter_view_widget_ = HighlighterView::Create(
116 presentation_delay,
117 Shell::GetContainer(root_window, kShellWindowId_OverlayContainer));
118 result_view_widget_.reset();
119 }
120
UpdatePointerView(ui::TouchEvent * event)121 void HighlighterController::UpdatePointerView(ui::TouchEvent* event) {
122 interrupted_stroke_timer_.reset();
123
124 GetHighlighterView()->AddNewPoint(event->root_location_f(),
125 event->time_stamp());
126
127 if (event->type() != ui::ET_TOUCH_RELEASED)
128 return;
129
130 gfx::Rect bounds =
131 highlighter_view_widget_->GetNativeWindow()->GetRootWindow()->bounds();
132 bounds.Inset(kScreenEdgeMargin, kScreenEdgeMargin);
133
134 const gfx::PointF pos = GetHighlighterView()->points().GetNewest().location;
135 if (bounds.Contains(
136 gfx::Point(static_cast<int>(pos.x()), static_cast<int>(pos.y())))) {
137 // The stroke has ended far enough from the screen edge, process it
138 // immediately.
139 RecognizeGesture();
140 return;
141 }
142
143 // The stroke has ended close to the screen edge. Delay gesture recognition
144 // a little to give the pen a chance to re-enter the screen.
145 GetHighlighterView()->AddGap();
146
147 interrupted_stroke_timer_ = std::make_unique<base::OneShotTimer>();
148 interrupted_stroke_timer_->Start(
149 FROM_HERE, base::TimeDelta::FromMilliseconds(kInterruptedStrokeTimeoutMs),
150 base::BindOnce(&HighlighterController::RecognizeGesture,
151 base::Unretained(this)));
152 }
153
RecognizeGesture()154 void HighlighterController::RecognizeGesture() {
155 interrupted_stroke_timer_.reset();
156
157 aura::Window* current_window =
158 highlighter_view_widget_->GetNativeWindow()->GetRootWindow();
159 const gfx::Rect bounds = current_window->bounds();
160
161 const fast_ink::FastInkPoints& points = GetHighlighterView()->points();
162 gfx::RectF box = points.GetBoundingBoxF();
163
164 const HighlighterGestureType gesture_type =
165 DetectHighlighterGesture(box, HighlighterView::kPenTipSize, points);
166
167 if (gesture_type == HighlighterGestureType::kHorizontalStroke) {
168 UMA_HISTOGRAM_COUNTS_10000("Ash.Shelf.Palette.Assistant.HighlighterLength",
169 static_cast<int>(box.width()));
170
171 box = AdjustHorizontalStroke(box, HighlighterView::kPenTipSize);
172 } else if (gesture_type == HighlighterGestureType::kClosedShape) {
173 const float fraction =
174 box.width() * box.height() / (bounds.width() * bounds.height());
175 UMA_HISTOGRAM_PERCENTAGE("Ash.Shelf.Palette.Assistant.CircledPercentage",
176 static_cast<int>(fraction * 100));
177 }
178
179 GetHighlighterView()->Animate(
180 box.CenterPoint(), gesture_type,
181 base::BindOnce(&HighlighterController::DestroyHighlighterView,
182 base::Unretained(this)));
183
184 // |box| is not guaranteed to be inside the screen bounds, clip it.
185 // Not converting |box| to gfx::Rect here to avoid accumulating rounding
186 // errors, instead converting |bounds| to gfx::RectF.
187 box.Intersect(
188 gfx::RectF(bounds.x(), bounds.y(), bounds.width(), bounds.height()));
189
190 if (!box.IsEmpty() &&
191 gesture_type != HighlighterGestureType::kNotRecognized) {
192 // The window for selection should be the root window to show assistant.
193 Shell::SetRootWindowForNewWindows(current_window->GetRootWindow());
194
195 const gfx::Rect selection_rect = gfx::ToEnclosingRect(box);
196 for (auto& observer : observers_)
197 observer.OnHighlighterSelectionRecognized(selection_rect);
198
199 result_view_widget_ = HighlighterResultView::Create(current_window);
200 static_cast<HighlighterResultView*>(result_view_widget_->GetContentsView())
201 ->Animate(box, gesture_type,
202 base::BindOnce(&HighlighterController::DestroyResultView,
203 base::Unretained(this)));
204
205 recognized_gesture_counter_++;
206 CallExitCallback();
207 } else {
208 if (!require_success_)
209 CallExitCallback();
210 }
211
212 gesture_counter_++;
213
214 const base::TimeTicks gesture_start = points.GetOldest().time;
215 if (gesture_counter_ > 1) {
216 // Up to 3 minutes.
217 UMA_HISTOGRAM_MEDIUM_TIMES("Ash.Shelf.Palette.Assistant.GestureInterval",
218 gesture_start - previous_gesture_end_);
219 }
220 previous_gesture_end_ = points.GetNewest().time;
221
222 // Up to 10 seconds.
223 UMA_HISTOGRAM_TIMES("Ash.Shelf.Palette.Assistant.GestureDuration",
224 points.GetNewest().time - gesture_start);
225
226 UMA_HISTOGRAM_ENUMERATION("Ash.Shelf.Palette.Assistant.GestureType",
227 gesture_type,
228 HighlighterGestureType::kGestureCount);
229 }
230
DestroyPointerView()231 void HighlighterController::DestroyPointerView() {
232 DestroyHighlighterView();
233 DestroyResultView();
234 }
235
CanStartNewGesture(ui::TouchEvent * event)236 bool HighlighterController::CanStartNewGesture(ui::TouchEvent* event) {
237 // Ignore events over the palette.
238 if (palette_utils::PaletteContainsPointInScreen(event->root_location()))
239 return false;
240 return !interrupted_stroke_timer_ &&
241 FastInkPointerController::CanStartNewGesture(event);
242 }
243
DestroyHighlighterView()244 void HighlighterController::DestroyHighlighterView() {
245 highlighter_view_widget_.reset();
246 // |interrupted_stroke_timer_| should never be non null when
247 // |highlighter_view_widget_| is null.
248 interrupted_stroke_timer_.reset();
249 }
250
DestroyResultView()251 void HighlighterController::DestroyResultView() {
252 result_view_widget_.reset();
253 }
254
CallExitCallback()255 void HighlighterController::CallExitCallback() {
256 if (!exit_callback_.is_null())
257 std::move(exit_callback_).Run();
258 }
259
GetHighlighterView()260 HighlighterView* HighlighterController::GetHighlighterView() {
261 return highlighter_view_widget_
262 ? static_cast<HighlighterView*>(
263 highlighter_view_widget_->GetContentsView())
264 : nullptr;
265 }
266
267 } // namespace ash
268