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