blob: 2bd7d00675988ab815ad5c41b51b91a2a2d8b254 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/highlighter/highlighter_controller.h"
#include <memory>
#include <utility>
#include "ash/highlighter/highlighter_gesture_util.h"
#include "ash/highlighter/highlighter_result_view.h"
#include "ash/highlighter/highlighter_view.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/shell.h"
#include "ash/system/palette/palette_utils.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/timer/timer.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/events/base_event_utils.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Bezel stroke detection margin, in DP.
const int kScreenEdgeMargin = 2;
const int kInterruptedStrokeTimeoutMs = 500;
// Adjust the height of the bounding box to match the pen tip height,
// while keeping the same vertical center line. Adjust the width to
// account for the pen tip width.
gfx::RectF AdjustHorizontalStroke(const gfx::RectF& box,
const gfx::SizeF& pen_tip_size) {
return gfx::RectF(box.x() - pen_tip_size.width() / 2,
box.CenterPoint().y() - pen_tip_size.height() / 2,
box.width() + pen_tip_size.width(), pen_tip_size.height());
}
} // namespace
HighlighterController::HighlighterController() {
Shell::Get()->AddPreTargetHandler(this);
}
HighlighterController::~HighlighterController() {
Shell::Get()->RemovePreTargetHandler(this);
}
void HighlighterController::AddObserver(Observer* observer) {
DCHECK(observer);
observers_.AddObserver(observer);
}
void HighlighterController::RemoveObserver(Observer* observer) {
DCHECK(observer);
observers_.RemoveObserver(observer);
}
void HighlighterController::SetExitCallback(base::OnceClosure exit_callback,
bool require_success) {
exit_callback_ = std::move(exit_callback);
require_success_ = require_success;
}
void HighlighterController::UpdateEnabledState(
HighlighterEnabledState enabled_state) {
if (enabled_state_ == enabled_state)
return;
enabled_state_ = enabled_state;
SetEnabled(enabled_state == HighlighterEnabledState::kEnabled);
for (auto& observer : observers_)
observer.OnHighlighterEnabledChanged(enabled_state);
}
void HighlighterController::AbortSession() {
if (enabled_state_ == HighlighterEnabledState::kEnabled)
UpdateEnabledState(HighlighterEnabledState::kDisabledBySessionAbort);
}
void HighlighterController::SetEnabled(bool enabled) {
FastInkPointerController::SetEnabled(enabled);
if (enabled) {
session_start_ = ui::EventTimeForNow();
gesture_counter_ = 0;
recognized_gesture_counter_ = 0;
} else {
UMA_HISTOGRAM_COUNTS_100("Ash.Shelf.Palette.Assistant.GesturesPerSession",
gesture_counter_);
UMA_HISTOGRAM_COUNTS_100(
"Ash.Shelf.Palette.Assistant.GesturesPerSession.Recognized",
recognized_gesture_counter_);
// If |highlighter_view_widget_| is animating it will delete itself when
// done animating. |result_view_widget_| will exist only if
// |highlighter_view_widget_| is animating, and it will also delete itself
// when done animating.
if (highlighter_view_widget_ && !GetHighlighterView()->animating())
DestroyPointerView();
}
}
views::View* HighlighterController::GetPointerView() const {
return const_cast<HighlighterController*>(this)->GetHighlighterView();
}
void HighlighterController::CreatePointerView(
base::TimeDelta presentation_delay,
aura::Window* root_window) {
highlighter_view_widget_ = HighlighterView::Create(
presentation_delay,
Shell::GetContainer(root_window, kShellWindowId_OverlayContainer));
result_view_widget_.reset();
}
void HighlighterController::UpdatePointerView(ui::TouchEvent* event) {
interrupted_stroke_timer_.reset();
GetHighlighterView()->AddNewPoint(event->root_location_f(),
event->time_stamp());
if (event->type() != ui::ET_TOUCH_RELEASED)
return;
gfx::Rect bounds =
highlighter_view_widget_->GetNativeWindow()->GetRootWindow()->bounds();
bounds.Inset(kScreenEdgeMargin);
const gfx::PointF pos = GetHighlighterView()->points().GetNewest().location;
if (bounds.Contains(
gfx::Point(static_cast<int>(pos.x()), static_cast<int>(pos.y())))) {
// The stroke has ended far enough from the screen edge, process it
// immediately.
RecognizeGesture();
return;
}
// The stroke has ended close to the screen edge. Delay gesture recognition
// a little to give the pen a chance to re-enter the screen.
GetHighlighterView()->AddGap();
interrupted_stroke_timer_ = std::make_unique<base::OneShotTimer>();
interrupted_stroke_timer_->Start(
FROM_HERE, base::Milliseconds(kInterruptedStrokeTimeoutMs),
base::BindOnce(&HighlighterController::RecognizeGesture,
base::Unretained(this)));
}
void HighlighterController::RecognizeGesture() {
interrupted_stroke_timer_.reset();
aura::Window* current_window =
highlighter_view_widget_->GetNativeWindow()->GetRootWindow();
const gfx::Rect bounds = current_window->bounds();
const fast_ink::FastInkPoints& points = GetHighlighterView()->points();
gfx::RectF box = points.GetBoundingBoxF();
const HighlighterGestureType gesture_type =
DetectHighlighterGesture(box, HighlighterView::kPenTipSize, points);
if (gesture_type == HighlighterGestureType::kHorizontalStroke) {
UMA_HISTOGRAM_COUNTS_10000("Ash.Shelf.Palette.Assistant.HighlighterLength",
static_cast<int>(box.width()));
box = AdjustHorizontalStroke(box, HighlighterView::kPenTipSize);
} else if (gesture_type == HighlighterGestureType::kClosedShape) {
const float fraction =
box.width() * box.height() / (bounds.width() * bounds.height());
UMA_HISTOGRAM_PERCENTAGE("Ash.Shelf.Palette.Assistant.CircledPercentage",
static_cast<int>(fraction * 100));
}
GetHighlighterView()->Animate(
box.CenterPoint(), gesture_type,
base::BindOnce(&HighlighterController::DestroyHighlighterView,
base::Unretained(this)));
// |box| is not guaranteed to be inside the screen bounds, clip it.
// Not converting |box| to gfx::Rect here to avoid accumulating rounding
// errors, instead converting |bounds| to gfx::RectF.
box.Intersect(
gfx::RectF(bounds.x(), bounds.y(), bounds.width(), bounds.height()));
if (!box.IsEmpty() &&
gesture_type != HighlighterGestureType::kNotRecognized) {
// The window for selection should be the root window to show assistant.
Shell::SetRootWindowForNewWindows(current_window->GetRootWindow());
const gfx::Rect selection_rect = gfx::ToEnclosingRect(box);
for (auto& observer : observers_)
observer.OnHighlighterSelectionRecognized(selection_rect);
result_view_widget_ = HighlighterResultView::Create(current_window);
static_cast<HighlighterResultView*>(result_view_widget_->GetContentsView())
->Animate(box, gesture_type,
base::BindOnce(&HighlighterController::DestroyResultView,
base::Unretained(this)));
recognized_gesture_counter_++;
CallExitCallback();
} else {
if (!require_success_)
CallExitCallback();
}
gesture_counter_++;
const base::TimeTicks gesture_start = points.GetOldest().time;
if (gesture_counter_ > 1) {
// Up to 3 minutes.
UMA_HISTOGRAM_MEDIUM_TIMES("Ash.Shelf.Palette.Assistant.GestureInterval",
gesture_start - previous_gesture_end_);
}
previous_gesture_end_ = points.GetNewest().time;
// Up to 10 seconds.
UMA_HISTOGRAM_TIMES("Ash.Shelf.Palette.Assistant.GestureDuration",
points.GetNewest().time - gesture_start);
UMA_HISTOGRAM_ENUMERATION("Ash.Shelf.Palette.Assistant.GestureType",
gesture_type,
HighlighterGestureType::kGestureCount);
}
void HighlighterController::DestroyPointerView() {
DestroyHighlighterView();
DestroyResultView();
}
bool HighlighterController::CanStartNewGesture(ui::LocatedEvent* event) {
// Ignore events over the palette.
if (palette_utils::PaletteContainsPointInScreen(event->root_location()))
return false;
return !interrupted_stroke_timer_ &&
FastInkPointerController::CanStartNewGesture(event);
}
bool HighlighterController::ShouldProcessEvent(ui::LocatedEvent* event) {
// Allow mouse clicking when Assistant tool is enabled.
if (event->type() == ui::ET_MOUSE_PRESSED ||
event->type() == ui::ET_MOUSE_RELEASED) {
return false;
}
return FastInkPointerController::ShouldProcessEvent(event);
}
void HighlighterController::DestroyHighlighterView() {
highlighter_view_widget_.reset();
// |interrupted_stroke_timer_| should never be non null when
// |highlighter_view_widget_| is null.
interrupted_stroke_timer_.reset();
}
void HighlighterController::DestroyResultView() {
result_view_widget_.reset();
}
void HighlighterController::CallExitCallback() {
if (!exit_callback_.is_null())
std::move(exit_callback_).Run();
}
HighlighterView* HighlighterController::GetHighlighterView() {
return highlighter_view_widget_
? static_cast<HighlighterView*>(
highlighter_view_widget_->GetContentsView())
: nullptr;
}
} // namespace ash