blob: d819e218e936a0f4b0c5ab8d59d08aaae20d4ec1 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/arc/compat_mode/resize_toggle_menu.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/bind.h"
#include "base/check.h"
#include "base/notreached.h"
#include "components/arc/compat_mode/overlay_dialog.h"
#include "components/arc/vector_icons/vector_icons.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_dialog_delegate_view.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/widget/widget.h"
namespace arc {
namespace {
class RoundedCornerBubbleDialogDelegateView
: public views::BubbleDialogDelegateView {
public:
explicit RoundedCornerBubbleDialogDelegateView(int corner_radius)
: corner_radius_(corner_radius) {}
// views::View:
void AddedToWidget() override {
auto* const frame = GetBubbleFrameView();
if (frame)
frame->SetCornerRadius(corner_radius_);
}
private:
const int corner_radius_;
};
} // namespace
ResizeToggleMenu::MenuButtonView::MenuButtonView(PressedCallback callback,
const gfx::VectorIcon& icon,
int title_string_id)
: views::Button(std::move(callback)), icon_(icon) {
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kVertical)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
.SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
.SetInteriorMargin(gfx::Insets(16, 0, 14, 0))
.SetDefault(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kPreferred,
views::MaximumFlexSizeRule::kPreferred,
/*adjust_height_for_width=*/true));
AddChildView(views::Builder<views::ImageView>()
.CopyAddressTo(&icon_view_)
.SetImageSize(gfx::Size(20, 20))
.SetHorizontalAlignment(views::ImageView::Alignment::kCenter)
.SetVerticalAlignment(views::ImageView::Alignment::kCenter)
.SetProperty(views::kMarginsKey, gfx::Insets(0, 0, 8, 0))
.Build());
AddChildView(views::Builder<views::Label>()
.CopyAddressTo(&title_)
.SetBackgroundColor(SK_ColorTRANSPARENT)
.SetText(l10n_util::GetStringUTF16(title_string_id))
.SetVerticalAlignment(gfx::ALIGN_BOTTOM)
.SetLineHeight(16)
.SetMultiLine(true)
.SetAllowCharacterBreak(true)
.Build());
SetAccessibleName(l10n_util::GetStringUTF16(title_string_id));
GetViewAccessibility().OverrideRole(ax::mojom::Role::kMenuItem);
constexpr int kBorderThicknessDp = 1;
const auto radius = views::LayoutProvider::Get()->GetCornerRadiusMetric(
views::Emphasis::kMedium);
SetBorder(views::CreateRoundedRectBorder(kBorderThicknessDp, radius,
gfx::kPlaceholderColor));
SetBackground(
views::CreateRoundedRectBackground(gfx::kPlaceholderColor, radius));
SetFocusBehavior(FocusBehavior::ALWAYS);
SetInstallFocusRingOnFocus(true);
views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(), radius);
}
ResizeToggleMenu::MenuButtonView::~MenuButtonView() = default;
void ResizeToggleMenu::MenuButtonView::SetSelected(bool is_selected) {
is_selected_ = is_selected;
SetState(is_selected_ ? views::Button::ButtonState::STATE_DISABLED
: views::Button::ButtonState::STATE_NORMAL);
UpdateColors();
}
void ResizeToggleMenu::MenuButtonView::OnThemeChanged() {
views::Button::OnThemeChanged();
UpdateColors();
}
gfx::Size ResizeToggleMenu::MenuButtonView::CalculatePreferredSize() const {
constexpr int kWidth = 96;
return gfx::Size(kWidth, GetHeightForWidth(kWidth));
}
void ResizeToggleMenu::MenuButtonView::UpdateColors() {
if (!GetWidget())
return;
const auto* theme = GetNativeTheme();
const auto foreground_color = theme->GetSystemColor(
is_selected_ ? ui::NativeTheme::kColorId_TextOnProminentButtonColor
: ui::NativeTheme::kColorId_LabelEnabledColor);
icon_view_->SetImage(gfx::CreateVectorIcon(icon_, foreground_color));
title_->SetEnabledColor(foreground_color);
const auto background_color =
is_selected_ ? theme->GetSystemColor(
ui::NativeTheme::kColorId_ProminentButtonColor)
: SK_ColorTRANSPARENT;
background()->SetNativeControlColor(background_color);
const auto border_color =
is_selected_
? SK_ColorTRANSPARENT
: theme->GetSystemColor(ui::NativeTheme::kColorId_MenuBorderColor);
border()->set_color(border_color);
}
ResizeToggleMenu::ResizeToggleMenu(views::Widget* widget,
ArcResizeLockPrefDelegate* pref_delegate)
: widget_(widget), pref_delegate_(pref_delegate) {
bubble_widget_ =
views::BubbleDialogDelegateView::CreateBubble(MakeBubbleDelegateView(
widget_, GetAnchorRect(),
base::BindRepeating(&ResizeToggleMenu::ApplyResizeCompatMode,
base::Unretained(this))));
widget_observations_.AddObservation(widget_);
widget_observations_.AddObservation(bubble_widget_);
OverlayDialog::Show(widget_->GetNativeWindow(),
base::BindOnce(&ResizeToggleMenu::CloseBubble,
weak_ptr_factory_.GetWeakPtr()),
/*dialog_view=*/nullptr);
bubble_widget_->Show();
}
ResizeToggleMenu::~ResizeToggleMenu() {
CloseBubble();
}
void ResizeToggleMenu::OnWidgetClosing(views::Widget* widget) {
OverlayDialog::CloseIfAny(widget_->GetNativeWindow());
widget_observations_.RemoveAllObservations();
widget_ = nullptr;
bubble_widget_ = nullptr;
}
void ResizeToggleMenu::OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& new_bounds) {
if (widget != widget_)
return;
DCHECK(bubble_widget_);
bubble_widget_->widget_delegate()->AsBubbleDialogDelegate()->SetAnchorRect(
GetAnchorRect());
UpdateSelectedButton();
}
gfx::Rect ResizeToggleMenu::GetAnchorRect() const {
DCHECK(widget_);
const gfx::Rect client_view_rect =
widget_->client_view()->GetBoundsInScreen();
// Anchored to the top edge of the client_view with padding.
constexpr auto kMarginTopDp = 8;
return gfx::Rect(client_view_rect.x(), client_view_rect.y() + kMarginTopDp,
client_view_rect.width(), 0);
}
std::unique_ptr<views::BubbleDialogDelegateView>
ResizeToggleMenu::MakeBubbleDelegateView(
views::Widget* parent,
gfx::Rect anchor_rect,
base::RepeatingCallback<void(ResizeCompatMode)> command_handler) {
constexpr int kCornerRadius = 16;
auto delegate_view =
std::make_unique<RoundedCornerBubbleDialogDelegateView>(kCornerRadius);
// Setup delegate.
delegate_view->SetArrow(views::BubbleBorder::Arrow::TOP_CENTER);
delegate_view->SetButtons(ui::DIALOG_BUTTON_NONE);
delegate_view->set_parent_window(parent->GetNativeView());
delegate_view->set_title_margins(gfx::Insets());
delegate_view->set_margins(gfx::Insets());
delegate_view->SetAnchorRect(anchor_rect);
delegate_view->SetTitle(
l10n_util::GetStringUTF16(IDS_ARC_COMPAT_MODE_RESIZE_TOGGLE_MENU_TITLE));
delegate_view->SetShowTitle(false);
delegate_view->SetAccessibleRole(ax::mojom::Role::kMenu);
// Setup view.
auto* const provider = views::LayoutProvider::Get();
delegate_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal, gfx::Insets(16),
provider->GetDistanceMetric(views::DISTANCE_RELATED_BUTTON_HORIZONTAL)));
const auto add_menu_button = [&delegate_view, &command_handler](
ResizeCompatMode command_id,
const gfx::VectorIcon& icon, int string_id) {
return delegate_view->AddChildView(std::make_unique<MenuButtonView>(
base::BindRepeating(command_handler, command_id), icon, string_id));
};
phone_button_ =
add_menu_button(ResizeCompatMode::kPhone, ash::kSystemMenuPhoneIcon,
IDS_ARC_COMPAT_MODE_RESIZE_TOGGLE_MENU_PHONE);
tablet_button_ =
add_menu_button(ResizeCompatMode::kTablet, ash::kSystemMenuTabletIcon,
IDS_ARC_COMPAT_MODE_RESIZE_TOGGLE_MENU_TABLET);
resizable_button_ =
add_menu_button(ResizeCompatMode::kResizable, kResizableIcon,
IDS_ARC_COMPAT_MODE_RESIZE_TOGGLE_MENU_RESIZABLE);
UpdateSelectedButton();
return delegate_view;
}
void ResizeToggleMenu::UpdateSelectedButton() {
// No need to update the button states if the widget is (being) closed.
if (!widget_)
return;
const auto selected_mode = PredictCurrentMode(widget_, pref_delegate_);
phone_button_->SetSelected(selected_mode &&
*selected_mode == ResizeCompatMode::kPhone);
tablet_button_->SetSelected(selected_mode &&
*selected_mode == ResizeCompatMode::kTablet);
resizable_button_->SetSelected(
selected_mode && *selected_mode == ResizeCompatMode::kResizable);
}
void ResizeToggleMenu::ApplyResizeCompatMode(ResizeCompatMode mode) {
switch (mode) {
case ResizeCompatMode::kPhone:
ResizeLockToPhone(widget_, pref_delegate_);
break;
case ResizeCompatMode::kTablet:
ResizeLockToTablet(widget_, pref_delegate_);
break;
case ResizeCompatMode::kResizable:
EnableResizingWithConfirmationIfNeeded(widget_, pref_delegate_);
break;
}
// Enabling/disabling resizing might not trigger bounds change, so force to
// update selected button status here.
UpdateSelectedButton();
auto_close_closure_.Reset(base::BindOnce(&ResizeToggleMenu::CloseBubble,
weak_ptr_factory_.GetWeakPtr()));
constexpr auto kAutoCloseDelay = base::TimeDelta::FromSeconds(2);
base::SequencedTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE, auto_close_closure_.callback(), kAutoCloseDelay);
}
void ResizeToggleMenu::CloseBubble() {
if (!bubble_widget_ || bubble_widget_->IsClosed())
return;
bubble_widget_->CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
} // namespace arc