// Copyright 2022 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/capture_mode/capture_mode_demo_tools_controller.h"
#include <memory>
#include <vector>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/key_combo_view.h"
#include "ash/capture_mode/pointer_highlight_layer.h"
#include "ash/capture_mode/video_recording_watcher.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/shell.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/location.h"
#include "base/notreached.h"
#include "ui/base/accelerators/ash/right_alt_event_property.h"
#include "ui/base/ime/constants.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/pointer_details.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
constexpr float kHighlightLayerFinalOpacity = 0.f;
// Make the initial value as small as possible so that the dot is not visible in
// the center of the ripple.
constexpr float kHighlightLayerInitialScale = 0.0001f;
constexpr float kHighlightLayerFinalScale = 1.0f;
constexpr float kTouchHighlightLayerTouchDownScale = 56.f / 72;
constexpr base::TimeDelta kMouseScaleUpDuration = base::Milliseconds(1500);
constexpr base::TimeDelta kTouchDownScaleUpDuration = base::Milliseconds(200);
constexpr base::TimeDelta kTouchUpScaleUpDuration = base::Milliseconds(1000);
constexpr int kSpaceBetweenKeyComboAndCaptureBar = 8;
constexpr int kModifiersToConsider = ui::EF_COMMAND_DOWN | ui::EF_CONTROL_DOWN |
ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN;
int GetModifierFlagForKeyCode(ui::KeyboardCode key_code) {
switch (key_code) {
case ui::VKEY_COMMAND:
case ui::VKEY_RWIN:
return ui::EF_COMMAND_DOWN;
case ui::VKEY_CONTROL:
case ui::VKEY_LCONTROL:
case ui::VKEY_RCONTROL:
return ui::EF_CONTROL_DOWN;
case ui::VKEY_MENU:
case ui::VKEY_LMENU:
case ui::VKEY_RMENU:
return ui::EF_ALT_DOWN;
case ui::VKEY_SHIFT:
case ui::VKEY_LSHIFT:
case ui::VKEY_RSHIFT:
return ui::EF_SHIFT_DOWN;
default:
return ui::EF_NONE;
}
}
// Includes non-modifier keys that can be shown independently without a modifier
// key being pressed.
constexpr ui::KeyboardCode kNotNeedingModifierKeys[] = {
ui::VKEY_CAPITAL,
ui::VKEY_RWIN,
ui::VKEY_ESCAPE,
ui::VKEY_TAB,
ui::VKEY_RETURN,
ui::VKEY_BACK,
ui::VKEY_BROWSER_BACK,
ui::VKEY_BROWSER_FORWARD,
ui::VKEY_BROWSER_REFRESH,
ui::VKEY_ZOOM,
ui::VKEY_MEDIA_LAUNCH_APP1,
ui::VKEY_BRIGHTNESS_DOWN,
ui::VKEY_BRIGHTNESS_UP,
ui::VKEY_VOLUME_MUTE,
ui::VKEY_VOLUME_DOWN,
ui::VKEY_VOLUME_UP,
ui::VKEY_UP,
ui::VKEY_DOWN,
ui::VKEY_LEFT,
ui::VKEY_RIGHT,
ui::VKEY_ASSISTANT,
ui::VKEY_SETTINGS,
ui::VKEY_RIGHT_ALT};
// Returns true if `key_code` is a non-modifier key for which a `KeyComboViewer`
// can be shown even if there are no modifier keys are currently pressed.
bool ShouldConsiderKey(ui::KeyboardCode key_code) {
return base::Contains(kNotNeedingModifierKeys, key_code);
}
views::Widget::InitParams CreateWidgetParams(
VideoRecordingWatcher* video_recording_watcher) {
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
params.parent =
video_recording_watcher->GetOnCaptureSurfaceWidgetParentWindow();
params.child = true;
params.name = "KeyComboWidget";
return params;
}
bool IsKeyEventFromVirtualKeyboard(const ui::KeyEvent* event) {
const auto* event_properties = event->properties();
return event_properties &&
event_properties->find(ui::kPropertyFromVK) != event_properties->end();
}
} // namespace
CaptureModeDemoToolsController::CaptureModeDemoToolsController(
VideoRecordingWatcher* video_recording_watcher)
: video_recording_watcher_(video_recording_watcher) {
ui::InputMethod* input_method =
Shell::Get()->window_tree_host_manager()->input_method();
input_method->AddObserver(this);
UpdateTextInputType(input_method->GetTextInputClient());
}
CaptureModeDemoToolsController::~CaptureModeDemoToolsController() {
Shell::Get()->window_tree_host_manager()->input_method()->RemoveObserver(
this);
}
void CaptureModeDemoToolsController::OnKeyEvent(ui::KeyEvent* event) {
if (event->type() == ui::EventType::kKeyReleased) {
OnKeyUpEvent(event);
return;
}
DCHECK_EQ(event->type(), ui::EventType::kKeyPressed);
OnKeyDownEvent(event);
}
void CaptureModeDemoToolsController::PerformMousePressAnimation(
const gfx::PointF& event_location_in_window) {
std::unique_ptr<PointerHighlightLayer> mouse_highlight_layer =
std::make_unique<PointerHighlightLayer>(
event_location_in_window,
video_recording_watcher_->GetOnCaptureSurfaceWidgetParentWindow()
->layer());
PointerHighlightLayer* mouse_highlight_layer_ptr =
mouse_highlight_layer.get();
mouse_highlight_layers_.push_back(std::move(mouse_highlight_layer));
ui::Layer* highlight_layer = mouse_highlight_layer_ptr->layer();
highlight_layer->SetTransform(capture_mode_util::GetScaleTransformAboutCenter(
highlight_layer, kHighlightLayerInitialScale));
const gfx::Transform scale_up_transform =
capture_mode_util::GetScaleTransformAboutCenter(
highlight_layer, kHighlightLayerFinalScale);
views::AnimationBuilder()
.OnEnded(base::BindOnce(
&CaptureModeDemoToolsController::OnMouseHighlightAnimationEnded,
weak_ptr_factory_.GetWeakPtr(), mouse_highlight_layer_ptr))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kMouseScaleUpDuration)
.SetTransform(highlight_layer, scale_up_transform,
gfx::Tween::ACCEL_0_40_DECEL_100)
.SetOpacity(highlight_layer, kHighlightLayerFinalOpacity,
gfx::Tween::ACCEL_0_80_DECEL_80);
}
void CaptureModeDemoToolsController::RefreshBounds() {
if (key_combo_widget_) {
key_combo_widget_->SetBounds(CalculateKeyComboWidgetBounds());
// Update the autoclick menu bounds and sticky overlay bounds if it collides
// with the bounds of the `key_combo_widget_`.
Shell::Get()
->accessibility_controller()
->UpdateFloatingPanelBoundsIfNeeded();
}
}
void CaptureModeDemoToolsController::OnTouchEvent(
ui::EventType event_type,
ui::PointerId pointer_id,
const gfx::PointF& event_location_in_window) {
switch (event_type) {
case ui::EventType::kTouchPressed: {
OnTouchDown(pointer_id, event_location_in_window);
return;
}
case ui::EventType::kTouchReleased:
case ui::EventType::kTouchCancelled: {
OnTouchUp(pointer_id, event_location_in_window);
return;
}
case ui::EventType::kTouchMoved: {
OnTouchDragged(pointer_id, event_location_in_window);
return;
}
default:
NOTREACHED();
}
}
void CaptureModeDemoToolsController::OnTextInputStateChanged(
const ui::TextInputClient* client) {
UpdateTextInputType(client);
}
void CaptureModeDemoToolsController::OnKeyUpEvent(ui::KeyEvent* event) {
// The RightAlt key is diferentiated via the Right alt proprerty attached to
// the event. If we see this property, we must overwrite the keycode for the
// purposes of showing the icon visually.
const ui::KeyboardCode key_code =
ui::HasRightAltProperty(*event) ? ui::VKEY_RIGHT_ALT : event->key_code();
if (IsKeyEventFromVirtualKeyboard(event)) {
// The virtual keyboard does not send key up events for modifier keys, such
// as 'Ctrl' or 'Alt'. Therefore on key up of non-modifier key we clear the
// `modifiers_` and rely on `Event::flags()` to refill it properly when we
// get a key down event from a virtual keyboard.
modifiers_ = 0;
} else {
const int modifier_flag = GetModifierFlagForKeyCode(key_code);
modifiers_ &= ~modifier_flag;
}
if (last_non_modifier_key_ == key_code) {
last_non_modifier_key_ = ui::VKEY_UNKNOWN;
}
if (key_up_refresh_timer_.IsRunning() &&
key_up_refresh_timer_.GetCurrentDelay() ==
capture_mode::kRefreshKeyComboWidgetLongDelay) {
// If the timer is running with a delay of
// `capture_mode::kRefreshKeyComboWidgetLongDelay`, it means that the
// non-modifier key of the key combo has recently been released with no
// modifier keys pressed or the last modifier key has been released with
// no non-modifier key that can be displayed when independently pressed
// which will trigger the hide timer to hide the entire widget when it
// expires. If there are other key up events, we want to ignore them such
// that the key combo continues to show on the screen as a complete combo
// until the timer expires.
return;
}
const auto& target_delay =
ShouldResetKeyComboWidget()
? capture_mode::kRefreshKeyComboWidgetLongDelay
: capture_mode::kRefreshKeyComboWidgetShortDelay;
key_up_refresh_timer_.Start(
FROM_HERE, target_delay, this,
&CaptureModeDemoToolsController::RefreshKeyComboViewer);
}
void CaptureModeDemoToolsController::OnKeyDownEvent(ui::KeyEvent* event) {
// We will not show key combo widget if the cursor is in the input text field
// to respect the user privacy. This check needs to be placed after checking
// the key up event as the key combo widget on display will still need to be
// refreshed.
if (in_text_input_) {
return;
}
const ui::KeyboardCode key_code = event->key_code();
// Return directly if it is a repeated key event for non-modifier key.
if (key_code == last_non_modifier_key_) {
return;
}
key_up_refresh_timer_.Stop();
const int modifier_flag = GetModifierFlagForKeyCode(key_code);
const bool is_vk_event = IsKeyEventFromVirtualKeyboard(event);
// For key event coming from on-screen keyboard, `event->flags()` will reflect
// the currently pressed modifier key(s). For key event coming from physical
// keyboard, `GetModifierFlagForKeyCode()` will give us the currently pressed
// modifier key.
if (is_vk_event) {
modifiers_ |= (event->flags() & kModifiersToConsider);
} else {
modifiers_ |= modifier_flag;
}
if (modifier_flag == ui::EF_NONE) {
// The RightAlt key is diferentiated via the Right alt proprerty attached to
// the event. If we see this property, we must overwrite the keycode for the
// purposes of showing the icon visually.
last_non_modifier_key_ =
ui::HasRightAltProperty(*event) ? ui::VKEY_RIGHT_ALT : key_code;
}
RefreshKeyComboViewer();
}
void CaptureModeDemoToolsController::RefreshKeyComboViewer() {
if (ShouldResetKeyComboWidget()) {
AnimateToResetKeyComboWidget();
return;
}
if (!key_combo_widget_) {
key_combo_widget_ = std::make_unique<views::Widget>();
key_combo_widget_->Init(CreateWidgetParams(video_recording_watcher_));
key_combo_view_ =
key_combo_widget_->SetContentsView(std::make_unique<KeyComboView>());
key_combo_widget_->SetVisibilityAnimationTransition(
views::Widget::ANIMATE_NONE);
ui::Layer* layer = key_combo_widget_->GetLayer();
layer->SetFillsBoundsOpaquely(false);
key_combo_widget_->Show();
}
key_combo_view_->RefreshView(modifiers_, last_non_modifier_key_);
RefreshBounds();
}
gfx::Rect CaptureModeDemoToolsController::CalculateKeyComboWidgetBounds()
const {
const gfx::Size preferred_size = key_combo_view_->GetPreferredSize();
const auto confine_bounds =
video_recording_watcher_->GetCaptureSurfaceConfineBounds();
const int key_combo_x =
preferred_size.width() > confine_bounds.width()
? confine_bounds.right() - preferred_size.width() -
capture_mode::kKeyWidgetBorderPadding
: confine_bounds.CenterPoint().x() - preferred_size.width() / 2;
int key_combo_y = confine_bounds.bottom() -
capture_mode::kKeyWidgetDistanceFromBottom -
preferred_size.height();
// Check the existence of capture mode bar and re-calculate `key_combo_y` to
// avoid collision.
auto* capture_mode_controller = CaptureModeController::Get();
if (capture_mode_controller->IsActive() &&
video_recording_watcher_->recording_source() !=
CaptureModeSource::kWindow) {
const auto* capture_bar_widget =
capture_mode_controller->capture_mode_session()
->GetCaptureModeBarWidget();
DCHECK(capture_bar_widget);
key_combo_y = std::min(key_combo_y,
capture_bar_widget->GetWindowBoundsInScreen().y() -
kSpaceBetweenKeyComboAndCaptureBar -
preferred_size.height());
}
return gfx::Rect(gfx::Point(key_combo_x, key_combo_y), preferred_size);
}
bool CaptureModeDemoToolsController::ShouldResetKeyComboWidget() const {
return (modifiers_ == 0) && !ShouldConsiderKey(last_non_modifier_key_);
}
void CaptureModeDemoToolsController::AnimateToResetKeyComboWidget() {
// TODO(http://b/258349669): apply animation to the hide process when the
// specs are ready.
key_combo_widget_.reset();
key_combo_view_ = nullptr;
}
void CaptureModeDemoToolsController::UpdateTextInputType(
const ui::TextInputClient* client) {
in_text_input_ =
client && client->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE;
}
void CaptureModeDemoToolsController::OnMouseHighlightAnimationEnded(
PointerHighlightLayer* pointer_highlight_layer_ptr) {
std::erase_if(mouse_highlight_layers_,
base::MatchesUniquePtr(pointer_highlight_layer_ptr));
if (on_mouse_highlight_animation_ended_callback_for_test_)
std::move(on_mouse_highlight_animation_ended_callback_for_test_).Run();
}
void CaptureModeDemoToolsController::OnTouchDown(
const ui::PointerId& pointer_id,
const gfx::PointF& event_location_in_window) {
std::unique_ptr<PointerHighlightLayer> touch_highlight_layer =
std::make_unique<PointerHighlightLayer>(
event_location_in_window,
video_recording_watcher_->GetOnCaptureSurfaceWidgetParentWindow()
->layer());
ui::Layer* highlight_layer = touch_highlight_layer->layer();
highlight_layer->SetTransform(capture_mode_util::GetScaleTransformAboutCenter(
highlight_layer, kHighlightLayerInitialScale));
touch_pointer_id_to_highlight_layer_map_.emplace(
pointer_id, std::move(touch_highlight_layer));
const gfx::Transform scale_up_transform =
capture_mode_util::GetScaleTransformAboutCenter(
highlight_layer, kTouchHighlightLayerTouchDownScale);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kTouchDownScaleUpDuration)
.SetTransform(highlight_layer, scale_up_transform,
gfx::Tween::ACCEL_0_40_DECEL_100);
}
void CaptureModeDemoToolsController::OnTouchUp(
const ui::PointerId& pointer_id,
const gfx::PointF& event_location_in_window) {
auto iter = touch_pointer_id_to_highlight_layer_map_.find(pointer_id);
// Touch up may happen without been registered to
// `touch_pointer_id_to_highlight_layer_map_` for example a touch down may
// happen before video recording starts and touch up happens after video
// recording starts.
if (iter == touch_pointer_id_to_highlight_layer_map_.end()) {
return;
}
std::unique_ptr<PointerHighlightLayer> touch_highlight_layer =
std::move(iter->second);
touch_pointer_id_to_highlight_layer_map_.erase(pointer_id);
ui::Layer* highlight_layer = touch_highlight_layer->layer();
DCHECK(highlight_layer);
const gfx::Transform scale_up_transform =
capture_mode_util::GetScaleTransformAboutCenter(
highlight_layer, kHighlightLayerFinalScale);
views::AnimationBuilder()
.OnEnded(base::BindOnce(
[](std::unique_ptr<PointerHighlightLayer> touch_highlight_layer) {},
std::move(touch_highlight_layer)))
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.Once()
.SetDuration(kTouchUpScaleUpDuration)
.SetTransform(highlight_layer, scale_up_transform,
gfx::Tween::ACCEL_0_40_DECEL_100)
.SetOpacity(highlight_layer, kHighlightLayerFinalOpacity,
gfx::Tween::ACCEL_0_80_DECEL_80);
}
void CaptureModeDemoToolsController::OnTouchDragged(
const ui::PointerId& pointer_id,
const gfx::PointF& event_location_in_window) {
auto iter = touch_pointer_id_to_highlight_layer_map_.find(pointer_id);
if (iter == touch_pointer_id_to_highlight_layer_map_.end()) {
return;
}
auto* highlight_layer = iter->second.get();
DCHECK(highlight_layer);
highlight_layer->CenterAroundPoint(event_location_in_window);
}
} // namespace ash