| // 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/quick_insert_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_QUICK_INSERT}; |
| |
| // 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 QuickInsert key is differentiated via the Quick insert 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::HasQuickInsertProperty(*event) |
| ? ui::VKEY_QUICK_INSERT |
| : 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 QuickInsert key is differentiated via the Quick insert 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::HasQuickInsertProperty(*event) ? ui::VKEY_QUICK_INSERT : 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 |