blob: c40545a6ccf6a9f54a399f595240cdb169dd0f49 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
#include <algorithm>
#include <memory>
#include "ash/frame/frame_view_ash.h"
#include "ash/game_dashboard/game_dashboard_controller.h"
#include "ash/game_dashboard/game_dashboard_utils.h"
#include "ash/public/cpp/arc_game_controls_flag.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/style/style_util.h"
#include "ash/wm/window_state.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "chrome/browser/ash/arc/input_overlay/actions/action.h"
#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
#include "chrome/browser/ash/arc/input_overlay/touch_injector.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_highlight.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
#include "chrome/browser/ash/arc/input_overlay/ui/button_options_menu.h"
#include "chrome/browser/ash/arc/input_overlay/ui/delete_edit_shortcut.h"
#include "chrome/browser/ash/arc/input_overlay/ui/editing_list.h"
#include "chrome/browser/ash/arc/input_overlay/ui/input_mapping_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/rich_nudge.h"
#include "chrome/browser/ash/arc/input_overlay/ui/target_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h"
#include "chrome/browser/ash/arc/input_overlay/util.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/exo/shell_surface_base.h"
#include "components/exo/shell_surface_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/transient_window_manager.h"
#include "ui/wm/core/window_util.h"
namespace arc::input_overlay {
namespace {
using ash::game_dashboard_utils::GetNextWidgetToFocus;
using ash::game_dashboard_utils::UpdateAccessibilityTree;
constexpr char kButtonOptionsMenu[] = "GameControlsButtonOptionsMenu";
constexpr char kEditingList[] = "GameControlsEditingList";
constexpr char kInputMapping[] = "GameControlsInputMapping";
constexpr char kActionHighlight[] = "ActionHighlight";
std::unique_ptr<views::Widget> CreateTransientWidget(
aura::Window* parent_window,
const std::string& widget_name,
bool accept_events) {
views::Widget::InitParams params(
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.parent = parent_window;
params.name = widget_name;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.activatable = views::Widget::InitParams::Activatable::kYes;
params.accept_events = accept_events;
auto widget = std::make_unique<views::Widget>();
widget->Init(std::move(params));
auto* widget_window = widget->GetNativeWindow();
DCHECK_EQ(parent_window, wm::GetTransientParent(widget_window));
wm::TransientWindowManager::GetOrCreate(widget_window)
->set_parent_controls_visibility(false);
widget->SetVisibilityAnimationTransition(views::Widget::ANIMATE_NONE);
return widget;
}
} // namespace
DisplayOverlayController::DisplayOverlayController(
TouchInjector* touch_injector)
: touch_injector_(touch_injector) {
touch_injector_->set_display_overlay_controller(this);
auto* window = touch_injector_->window();
window->AddObserver(this);
UpdateDisplayMode();
ash::Shell::Get()->AddPreTargetHandler(this);
}
DisplayOverlayController::~DisplayOverlayController() {
touch_injector_->set_display_overlay_controller(nullptr);
touch_injector_->window()->RemoveObserver(this);
RemoveAllWidgets();
ash::Shell::Get()->RemovePreTargetHandler(this);
}
void DisplayOverlayController::UpdateDisplayMode() {
ash::ArcGameControlsFlag flags =
touch_injector_->window()->GetProperty(ash::kArcGameControlsFlagsKey);
SetDisplayMode(IsFlagSet(flags, ash::ArcGameControlsFlag::kEnabled)
? (IsFlagSet(flags, ash::ArcGameControlsFlag::kEdit)
? DisplayMode::kEdit
: DisplayMode::kView)
: DisplayMode::kNone);
}
void DisplayOverlayController::ChangeActionType(Action* reference_action,
ActionType type) {
touch_injector_->ChangeActionType(reference_action, type);
}
void DisplayOverlayController::SetDisplayMode(DisplayMode mode) {
if (display_mode_ == mode) {
return;
}
switch (mode) {
case DisplayMode::kNone:
RemoveAllWidgets();
break;
case DisplayMode::kView: {
DCHECK(!rich_nudge_widget_);
DCHECK(!target_widget_);
DCHECK(!button_options_widget_);
RemoveActionHighlightWidget();
RemoveDeleteEditShortcutWidget();
RemoveEditingListWidget();
if (GetActiveActionsSize() == 0u) {
// If there is no active action in `kView` mode, it doesn't create
// `input_mapping_widget_` to save resources. When switching from
// `kEdit` mode, destroy `input_mapping_widget_` for no active actions.
RemoveInputMappingWidget();
} else {
AddInputMappingWidget();
if (auto* input_mapping = GetInputMapping()) {
input_mapping->SetDisplayMode(mode);
}
SetInputMappingVisible(
/*visible=*/touch_injector_->input_mapping_visible());
// In the view mode, make sure the input mapping is displayed under game
// dashboard UIs.
StackInputMappingAtBottomForViewMode();
auto* input_mapping_window = input_mapping_widget_->GetNativeWindow();
input_mapping_window->SetEventTargetingPolicy(
aura::EventTargetingPolicy::kNone);
}
} break;
case DisplayMode::kEdit: {
if (GetActiveActionsSize() == 0u) {
// Because`input_mapping_widget_` was not created in `kView` mode,
// create `input_mapping_widget_` in `kEdit` mode for adding new
// actions.
AddInputMappingWidget();
}
// No matter if the mapping hint is hidden, `input_mapping_widget_` needs
// to show up in `kEdit` mode.
SetInputMappingVisible(/*visible=*/true);
UpdateAccessibilityTree(GetTraversableWidgets());
if (auto* input_mapping = GetInputMapping()) {
input_mapping->SetDisplayMode(mode);
}
auto* input_mapping_window = input_mapping_widget_->GetNativeWindow();
input_mapping_window->SetEventTargetingPolicy(
aura::EventTargetingPolicy::kTargetAndDescendants);
AddEditingListWidget();
} break;
default:
break;
}
display_mode_ = mode;
}
void DisplayOverlayController::OnInputBindingChange(
Action* action,
std::unique_ptr<InputElement> input_element) {
touch_injector_->OnInputBindingChange(action, std::move(input_element));
}
void DisplayOverlayController::SaveToProtoFile() {
touch_injector_->OnSaveProtoFile();
}
void DisplayOverlayController::OnCustomizeSave() {
touch_injector_->OnBindingSave();
SetDisplayMode(DisplayMode::kView);
}
const std::string& DisplayOverlayController::GetPackageName() const {
return touch_injector_->package_name();
}
void DisplayOverlayController::OnApplyMenuState() {
if (display_mode_ != DisplayMode::kView) {
return;
}
SetInputMappingVisible(
/*visible=*/GetTouchInjectorEnable() && GetInputMappingViewVisible(),
/*store_visible_state=*/true);
}
InputOverlayWindowStateType DisplayOverlayController::GetWindowStateType()
const {
DCHECK(touch_injector_);
auto* window = touch_injector_->window();
DCHECK(window);
auto* state = ash::WindowState::Get(window);
InputOverlayWindowStateType type = InputOverlayWindowStateType::kInvalid;
if (state) {
if (state->IsNormalStateType()) {
type = InputOverlayWindowStateType::kNormal;
} else if (state->IsMaximized()) {
type = InputOverlayWindowStateType::kMaximized;
} else if (state->IsFullscreen()) {
type = InputOverlayWindowStateType::kFullscreen;
} else if (state->IsSnapped()) {
type = InputOverlayWindowStateType::kSnapped;
}
}
return type;
}
void DisplayOverlayController::AddNewAction(ActionType action_type,
const gfx::Point& target_pos) {
// Keep adding new action before exiting button placement mode because
// `target_pos` is from button placement mode.
touch_injector_->AddNewAction(action_type, target_pos);
ExitButtonPlaceMode(/*is_action_added=*/true);
}
void DisplayOverlayController::RemoveAction(Action* action) {
touch_injector_->RemoveAction(action);
}
void DisplayOverlayController::RemoveActionNewState(Action* action) {
if (!action->is_new()) {
return;
}
touch_injector_->RemoveActionNewState(action);
}
size_t DisplayOverlayController::GetActiveActionsSize() const {
return touch_injector_->GetActiveActionsSize();
}
bool DisplayOverlayController::HasSingleUserAddedAction() const {
return touch_injector_->HasSingleUserAddedAction();
}
bool DisplayOverlayController::IsActiveAction(Action* action) const {
const auto& actions = touch_injector_->actions();
if (actions.empty()) {
return false;
}
auto it = std::find_if(
actions.begin(), actions.end(),
[&](const std::unique_ptr<Action>& p) { return action == p.get(); });
return it != actions.end() && !(it->get()->IsDeleted());
}
MappingSource DisplayOverlayController::GetMappingSource() const {
const auto& actions = touch_injector_->actions();
if (actions.empty()) {
return MappingSource::kEmpty;
}
// Check if there is any default action.
auto default_it = std::find_if(
actions.begin(), actions.end(),
[&](const std::unique_ptr<Action>& p) { return p->IsDefaultAction(); });
// Check if there is any user added action.
auto user_added_it = std::find_if(
actions.begin(), actions.end(),
[&](const std::unique_ptr<Action>& p) { return !p->IsDefaultAction(); });
return default_it != actions.end() && user_added_it != actions.end()
? MappingSource::kDefaultAndUserAdded
: (default_it != actions.end() ? MappingSource::kDefault
: MappingSource::kUserAdded);
}
void DisplayOverlayController::AddTouchInjectorObserver(
TouchInjectorObserver* observer) {
touch_injector_->AddObserver(observer);
}
void DisplayOverlayController::RemoveTouchInjectorObserver(
TouchInjectorObserver* observer) {
touch_injector_->RemoveObserver(observer);
}
void DisplayOverlayController::AddButtonOptionsMenuWidget(Action* action) {
if (button_options_widget_) {
auto* menu = GetButtonOptionsMenu();
if (menu && menu->action() == action) {
return;
}
RemoveButtonOptionsMenuWidget();
}
button_options_widget_ =
CreateTransientWidget(input_mapping_widget_->GetNativeWindow(),
/*widget_name=*/kButtonOptionsMenu,
/*accept_events=*/true);
button_options_widget_->SetContentsView(
std::make_unique<ButtonOptionsMenu>(this, action));
UpdateButtonOptionsMenuWidgetBounds();
button_options_widget_->widget_delegate()->SetAccessibleTitle(
l10n_util::GetStringUTF16(IDS_INPUT_OVERLAY_BUTTON_OPTIONS_A11Y_LABEL));
// Always hide editing list when button options menu shows up.
SetEditingListVisibility(/*visible=*/false);
button_options_widget_->Show();
UpdateAccessibilityTree(GetTraversableWidgets());
}
void DisplayOverlayController::RemoveButtonOptionsMenuWidget() {
if (!button_options_widget_) {
return;
}
// Check if related action is already deleted.
if (const auto* menu = GetButtonOptionsMenu()) {
if (auto* menu_action = menu->action(); IsActiveAction(menu_action)) {
RemoveActionNewState(menu_action);
}
}
button_options_widget_->Close();
button_options_widget_.reset();
UpdateAccessibilityTree(GetTraversableWidgets());
}
void DisplayOverlayController::SetButtonOptionsMenuWidgetVisibility(
bool is_visible) {
if (!button_options_widget_) {
return;
}
if (is_visible) {
if (GetButtonOptionsMenu()) {
UpdateButtonOptionsMenuWidgetBounds();
}
button_options_widget_->Show();
} else {
button_options_widget_->Hide();
}
UpdateAccessibilityTree(GetTraversableWidgets());
}
void DisplayOverlayController::AddDeleteEditShortcutWidget(
ActionViewListItem* anchor_view) {
if (!delete_edit_shortcut_widget_) {
delete_edit_shortcut_widget_ =
base::WrapUnique(views::BubbleDialogDelegateView::CreateBubble(
std::make_unique<DeleteEditShortcut>(this, anchor_view)));
}
if (auto* shortcut = GetDeleteEditShortcut();
shortcut->GetAnchorView() != anchor_view) {
shortcut->UpdateAnchorView(anchor_view);
}
delete_edit_shortcut_widget_->Show();
UpdateAccessibilityTree(GetTraversableWidgets());
}
void DisplayOverlayController::RemoveDeleteEditShortcutWidget() {
if (delete_edit_shortcut_widget_) {
delete_edit_shortcut_widget_->Close();
delete_edit_shortcut_widget_.reset();
UpdateAccessibilityTree(GetTraversableWidgets());
}
}
void DisplayOverlayController::AddActionHighlightWidget(Action* action) {
auto* anchor_view = action->action_view();
if (!action_highlight_widget_) {
action_highlight_widget_ = CreateTransientWidget(
touch_injector_->window(), /*widget_name=*/kActionHighlight,
/*accept_events=*/false);
action_highlight_widget_->SetContentsView(
std::make_unique<ActionHighlight>(this, anchor_view));
}
if (auto* highlight = views::AsViewClass<ActionHighlight>(
action_highlight_widget_->GetContentsView())) {
highlight->UpdateAnchorView(anchor_view);
// Show `action_highlight_widget_` if it is hidden.
if (!action_highlight_widget_->IsVisible()) {
action_highlight_widget_->ShowInactive();
input_mapping_widget_->StackAboveWidget(action_highlight_widget_.get());
}
}
}
void DisplayOverlayController::RemoveActionHighlightWidget() {
if (action_highlight_widget_) {
action_highlight_widget_->Close();
action_highlight_widget_.reset();
}
}
void DisplayOverlayController::HideActionHighlightWidget() {
if (action_highlight_widget_) {
action_highlight_widget_->Hide();
}
}
void DisplayOverlayController::HideActionHighlightWidgetForAction(
Action* action) {
if (!action_highlight_widget_) {
return;
}
if (auto* highlight = views::AsViewClass<ActionHighlight>(
action_highlight_widget_->GetContentsView());
highlight && highlight->anchor_view() == action->action_view()) {
action_highlight_widget_->Hide();
}
}
void DisplayOverlayController::UpdateWidgetBoundsInRootWindow(
views::Widget* widget,
const gfx::Rect& bounds_in_root_window) {
auto root_bounds =
touch_injector_->window()->GetRootWindow()->GetBoundsInScreen();
auto bounds_in_screen = bounds_in_root_window;
bounds_in_screen.Offset(root_bounds.OffsetFromOrigin());
widget->SetBounds(bounds_in_screen);
}
ActionViewListItem* DisplayOverlayController::GetEditingListItemForAction(
Action* action) {
if (auto* editing_list = GetEditingList()) {
return editing_list->GetListItemForAction(action);
}
return nullptr;
}
void DisplayOverlayController::OnMouseEvent(ui::MouseEvent* event) {
if (display_mode_ == DisplayMode::kView ||
event->type() != ui::EventType::kMousePressed) {
return;
}
ProcessPressedEvent(*event);
}
void DisplayOverlayController::OnTouchEvent(ui::TouchEvent* event) {
if (display_mode_ == DisplayMode::kView ||
event->type() != ui::EventType::kTouchPressed) {
return;
}
ProcessPressedEvent(*event);
}
void DisplayOverlayController::OnKeyEvent(ui::KeyEvent* event) {
if (display_mode_ == DisplayMode::kEdit) {
MaybeChangeFocusWidget(*event);
}
}
void DisplayOverlayController::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
DCHECK_EQ(window, touch_injector_->window());
// Disregard the bounds from animation and only care final window bounds.
if (reason == ui::PropertyChangeReason::FROM_ANIMATION) {
return;
}
UpdateForBoundsChanged();
}
void DisplayOverlayController::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
DCHECK_EQ(window, touch_injector_->window());
if (key == chromeos::kImmersiveIsActive) {
bool is_immersive = window->GetProperty(chromeos::kImmersiveIsActive);
// This is to catch the corner case that when an app is launched as
// fullscreen/immersive mode, so it only cares when the window turns into
// immersive mode from non-immersive mode.
if (!is_immersive || is_immersive == static_cast<bool>(old)) {
return;
}
UpdateForBoundsChanged();
}
if (key == ash::kArcGameControlsFlagsKey) {
ash::ArcGameControlsFlag old_flags =
static_cast<ash::ArcGameControlsFlag>(old);
ash::ArcGameControlsFlag flags =
window->GetProperty(ash::kArcGameControlsFlagsKey);
if (flags != old_flags) {
UpdateDisplayMode();
SetTouchInjectorEnable(
IsFlagSet(flags, ash::ArcGameControlsFlag::kEnabled));
// `input_mapping_widget_` is always visible in edit mode.
SetInputMappingVisible(/*visible=*/
IsFlagSet(flags, ash::ArcGameControlsFlag::kEdit)
? true
: IsFlagSet(flags,
ash::ArcGameControlsFlag::kHint),
/*store_visible_state=*/true);
// Save the menu states upon menu closing.
if (IsFlagChanged(flags, old_flags, ash::ArcGameControlsFlag::kMenu) &&
!IsFlagSet(flags, ash::ArcGameControlsFlag::kMenu)) {
touch_injector_->OnInputMenuViewRemoved();
}
UpdateEventRewriteCapability();
// Record metrics.
const auto mapping_source = GetMappingSource();
if (IsFlagChanged(flags, old_flags, ash::ArcGameControlsFlag::kEnabled)) {
RecordToggleWithMappingSource(
GetPackageName(),
/*is_feature=*/true,
/*is_on=*/IsFlagSet(flags, ash::ArcGameControlsFlag::kEnabled),
mapping_source);
}
if (IsFlagChanged(flags, old_flags, ash::ArcGameControlsFlag::kHint)) {
RecordToggleWithMappingSource(
GetPackageName(),
/*is_feature=*/false,
/*is_on=*/IsFlagSet(flags, ash::ArcGameControlsFlag::kHint),
mapping_source);
}
}
}
}
void DisplayOverlayController::SetInputMappingVisible(
bool visible,
bool store_visible_state) {
// There is no `input_mapping_widget_` if there is no active action or the
// feature is disabled.
if (input_mapping_widget_ && input_mapping_widget_->IsVisible() != visible) {
if (visible) {
input_mapping_widget_->ShowInactive();
} else {
input_mapping_widget_->Hide();
}
}
if (store_visible_state) {
CHECK(touch_injector_);
touch_injector_->store_input_mapping_visible(visible);
}
}
bool DisplayOverlayController::GetInputMappingViewVisible() const {
DCHECK(touch_injector_);
if (!touch_injector_) {
return false;
}
return touch_injector_->input_mapping_visible();
}
void DisplayOverlayController::SetTouchInjectorEnable(bool enable) {
DCHECK(touch_injector_);
if (!touch_injector_) {
return;
}
touch_injector_->store_touch_injector_enable(enable);
}
bool DisplayOverlayController::GetTouchInjectorEnable() {
DCHECK(touch_injector_);
if (!touch_injector_) {
return false;
}
return touch_injector_->touch_injector_enable();
}
void DisplayOverlayController::ProcessPressedEvent(
const ui::LocatedEvent& event) {
if (auto* delete_edit_view = GetDeleteEditShortcut()) {
if (const auto bounds = delete_edit_view->GetBoundsInScreen();
!bounds.Contains(event.target()->GetScreenLocation(event))) {
RemoveDeleteEditShortcutWidget();
}
}
}
void DisplayOverlayController::EnsureTaskWindowToFrontForViewMode(
views::Widget* overlay_widget) {
DCHECK(overlay_widget);
DCHECK(overlay_widget->GetNativeWindow());
DCHECK_EQ(overlay_widget->GetNativeWindow()->event_targeting_policy(),
aura::EventTargetingPolicy::kNone);
auto* shell_surface_base =
exo::GetShellSurfaceBaseForWindow(touch_injector_->window());
DCHECK(shell_surface_base);
auto* host_window = shell_surface_base->host_window();
DCHECK(host_window);
if (const auto& children = host_window->children(); children.size() > 0u) {
// First child is the root ExoSurface window. Focus on the root surface
// window can bring the task window to the front of the task stack.
if (!children[0]->HasFocus()) {
children[0]->Focus();
}
} else {
host_window->Focus();
}
}
void DisplayOverlayController::UpdateForBoundsChanged() {
auto content_bounds = CalculateWindowContentBounds(touch_injector_->window());
if (content_bounds == touch_injector_->content_bounds_f()) {
return;
}
touch_injector_->UpdateForOverlayBoundsChanged(content_bounds);
UpdateInputMappingWidgetBounds();
UpdateEditingListWidgetBounds();
UpdateTargetWidgetBounds();
UpdateButtonOptionsMenuWidgetBounds();
}
void DisplayOverlayController::RemoveAllWidgets() {
RemoveRichNudge();
RemoveDeleteEditShortcutWidget();
RemoveTargetWidget();
RemoveButtonOptionsMenuWidget();
RemoveEditingListWidget();
RemoveActionHighlightWidget();
RemoveInputMappingWidget();
}
void DisplayOverlayController::AddInputMappingWidget() {
if (input_mapping_widget_) {
return;
}
input_mapping_widget_ = CreateTransientWidget(touch_injector_->window(),
/*widget_name=*/kInputMapping,
/*accept_events=*/false);
input_mapping_widget_->SetContentsView(
std::make_unique<InputMappingView>(this));
UpdateInputMappingWidgetBounds();
}
void DisplayOverlayController::RemoveInputMappingWidget() {
if (input_mapping_widget_) {
input_mapping_widget_->Close();
input_mapping_widget_.reset();
}
}
InputMappingView* DisplayOverlayController::GetInputMapping() {
if (!input_mapping_widget_) {
return nullptr;
}
return views::AsViewClass<InputMappingView>(
input_mapping_widget_->GetContentsView());
}
void DisplayOverlayController::StackInputMappingAtBottomForViewMode() {
if (!input_mapping_widget_ || display_mode_ != DisplayMode::kView) {
return;
}
input_mapping_widget_->Deactivate();
// ash::GameDashboardController::Get() is empty for the
// unit test.
if (auto* gd_controller = ash::GameDashboardController::Get()) {
gd_controller->MaybeStackAboveWidget(touch_injector_->window(),
input_mapping_widget_.get());
}
}
void DisplayOverlayController::AddEditingListWidget() {
if (editing_list_widget_) {
return;
}
editing_list_widget_ = CreateTransientWidget(
input_mapping_widget_->GetNativeWindow(), /*widget_name=*/kEditingList,
/*accept_events=*/true);
editing_list_widget_->SetContentsView(std::make_unique<EditingList>(this));
// Avoid active conflict with the game dashboard main menu.
editing_list_widget_->ShowInactive();
UpdateEditingListWidgetBounds();
editing_list_widget_->widget_delegate()->SetAccessibleTitle(
l10n_util::GetStringUTF16(IDS_INPUT_OVERLAY_EDITING_LIST_A11Y_LABEL));
UpdateAccessibilityTree(GetTraversableWidgets());
}
void DisplayOverlayController::RemoveEditingListWidget() {
if (editing_list_widget_) {
editing_list_widget_->Close();
editing_list_widget_.reset();
UpdateAccessibilityTree(GetTraversableWidgets());
UpdateFlagAndProperty(touch_injector_->window(),
ash::ArcGameControlsFlag::kEdit,
/*turn_on=*/false);
UpdateEventRewriteCapability();
}
}
void DisplayOverlayController::SetEditingListVisibility(bool visible) {
if (!editing_list_widget_ || editing_list_widget_->IsVisible() == visible) {
return;
}
if (visible) {
editing_list_widget_->Show();
} else {
editing_list_widget_->Hide();
}
UpdateAccessibilityTree(GetTraversableWidgets());
}
EditingList* DisplayOverlayController::GetEditingList() {
if (!editing_list_widget_) {
return nullptr;
}
return views::AsViewClass<EditingList>(
editing_list_widget_->GetContentsView());
}
ButtonOptionsMenu* DisplayOverlayController::GetButtonOptionsMenu() {
if (!button_options_widget_) {
return nullptr;
}
return views::AsViewClass<ButtonOptionsMenu>(
button_options_widget_->GetContentsView());
}
void DisplayOverlayController::EnterButtonPlaceMode(ActionType action_type) {
RemoveDeleteEditShortcutWidget();
SetEditingListVisibility(/*visible=*/false);
AddTargetWidget(action_type);
AddRichNudge();
}
void DisplayOverlayController::ExitButtonPlaceMode(bool is_action_added) {
RemoveRichNudge();
RemoveTargetWidget();
if (!is_action_added) {
SetEditingListVisibility(/*visible=*/true);
}
}
void DisplayOverlayController::UpdateButtonPlacementNudgeAnchorRect() {
auto* target_view = GetTargetView();
if (!target_view) {
return;
}
auto target_circle_bounds = target_view->GetTargetCircleBounds();
views::View::ConvertRectToScreen(target_view, &target_circle_bounds);
auto* rich_nudge = GetRichNudge();
if (rich_nudge && rich_nudge_widget_->GetWindowBoundsInScreen().Intersects(
target_circle_bounds)) {
rich_nudge->FlipPosition();
}
}
void DisplayOverlayController::AddTargetWidget(ActionType action_type) {
DCHECK(!target_widget_);
target_widget_ = CreateTransientWidget(touch_injector_->window(),
/*widget_name=*/kInputMapping,
/*accept_events=*/true);
target_widget_->SetContentsView(
std::make_unique<TargetView>(this, action_type));
target_widget_->ShowInactive();
}
void DisplayOverlayController::RemoveTargetWidget() {
if (target_widget_) {
target_widget_->Close();
target_widget_.reset();
}
}
TargetView* DisplayOverlayController::GetTargetView() const {
return target_widget_
? views::AsViewClass<TargetView>(target_widget_->GetContentsView())
: nullptr;
}
void DisplayOverlayController::AddRichNudge() {
if (GetTargetView()) {
rich_nudge_widget_ =
base::WrapUnique(views::BubbleDialogDelegateView::CreateBubble(
std::make_unique<RichNudge>(target_widget_->GetNativeWindow())));
rich_nudge_widget_->ShowInactive();
}
}
void DisplayOverlayController::RemoveRichNudge() {
if (rich_nudge_widget_) {
rich_nudge_widget_->Close();
rich_nudge_widget_.reset();
}
}
RichNudge* DisplayOverlayController::GetRichNudge() const {
return rich_nudge_widget_ ? views::AsViewClass<RichNudge>(
rich_nudge_widget_->widget_delegate()
->AsBubbleDialogDelegate()
->GetContentsView())
: nullptr;
}
DeleteEditShortcut* DisplayOverlayController::GetDeleteEditShortcut() const {
return delete_edit_shortcut_widget_
? views::AsViewClass<DeleteEditShortcut>(
delete_edit_shortcut_widget_->widget_delegate()
->AsBubbleDialogDelegate()
->GetContentsView())
: nullptr;
}
void DisplayOverlayController::UpdateButtonOptionsMenuWidgetBounds() {
// There is no `button_options_widget_` in view mode.
if (!button_options_widget_) {
return;
}
if (auto* menu = GetButtonOptionsMenu()) {
menu->UpdateWidget();
}
}
void DisplayOverlayController::UpdateInputMappingWidgetBounds() {
// There is no `input_mapping_widget_` if there is no active action or gio is
// disabled.
if (!input_mapping_widget_) {
return;
}
UpdateWidgetBoundsInRootWindow(input_mapping_widget_.get(),
touch_injector_->content_bounds());
StackInputMappingAtBottomForViewMode();
}
void DisplayOverlayController::UpdateEditingListWidgetBounds() {
// There is no `editing_list_widget_` in view mode.
if (!editing_list_widget_) {
return;
}
if (auto* editing_list = GetEditingList()) {
editing_list->UpdateWidget();
}
}
void DisplayOverlayController::UpdateTargetWidgetBounds() {
if (!target_widget_) {
return;
}
if (auto* target_view = GetTargetView()) {
target_view->UpdateWidgetBounds();
}
}
void DisplayOverlayController::UpdateEventRewriteCapability() {
ash::ArcGameControlsFlag flags =
touch_injector_->window()->GetProperty(ash::kArcGameControlsFlagsKey);
touch_injector_->set_can_rewrite_event(
IsFlagSet(flags, ash::ArcGameControlsFlag::kEnabled) &&
!IsFlagSet(flags, ash::ArcGameControlsFlag::kEmpty) &&
!IsFlagSet(flags, ash::ArcGameControlsFlag::kMenu) &&
!IsFlagSet(flags, ash::ArcGameControlsFlag::kEdit));
}
std::vector<views::Widget*> DisplayOverlayController::GetTraversableWidgets()
const {
std::vector<views::Widget*> widget_list;
if (input_mapping_widget_ && input_mapping_widget_->IsVisible()) {
widget_list.emplace_back(input_mapping_widget_.get());
}
if (editing_list_widget_ && editing_list_widget_->IsVisible()) {
widget_list.emplace_back(editing_list_widget_.get());
}
if (button_options_widget_ && button_options_widget_->IsVisible()) {
widget_list.emplace_back(button_options_widget_.get());
}
if (delete_edit_shortcut_widget_ &&
delete_edit_shortcut_widget_->IsVisible()) {
widget_list.emplace_back(delete_edit_shortcut_widget_.get());
}
return widget_list;
}
void DisplayOverlayController::MaybeChangeFocusWidget(ui::KeyEvent& event) {
// Only tab pressed is checked because the focus change is triggered by the
// tab pressed event. No need to change widget focus again on tab key
// released event.
if (event.type() == ui::EventType::kKeyReleased ||
!views::FocusManager::IsTabTraversalKeyEvent(event)) {
return;
}
const bool reverse = event.IsShiftDown();
auto* target_widget = views::Widget::GetWidgetForNativeWindow(
static_cast<aura::Window*>(event.target()));
auto* focus_manager = target_widget->GetFocusManager();
// Once there is next focusable view (dont_loop==true), it means the current
// focus is not the first or the last focusable view, so it doesn't need to
// change focus to the next widget.
if (focus_manager->GetNextFocusableView(
/*starting_view=*/focus_manager->GetFocusedView(),
/*starting_widget=*/target_widget, /*reverse=*/reverse,
/*dont_loop=*/true)) {
return;
}
// Change focus to the next widget.
if (auto* next_widget = GetNextWidgetToFocus(GetTraversableWidgets(),
target_widget, reverse)) {
next_widget->GetFocusManager()->AdvanceFocus(reverse);
// Change the event target.
ui::Event::DispatcherApi(&event).set_target(next_widget->GetNativeWindow());
}
}
} // namespace arc::input_overlay