Commit 3593b6fa authored by Bailey Berro's avatar Bailey Berro Committed by Commit Bot

Introduce ResolutionChangeDialog

This change introduces a system modal dialog that is displayed when a
user changes the resolution of an external display. The majority of the
logic is encapsulated in a more generic DisplayChangeDialog so that
behavior can be reused for a future RefreshRateChangeDialog.

Mocks: go/cros-displays-split
Test: ash_unittests
Change-Id: Iaca64bbabbe4f831a3578012aac148fee95f3066
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1979137
Commit-Queue: Bailey Berro <baileyberro@chromium.org>
Reviewed-by: default avatarAhmed Fakhry <afakhry@chromium.org>
Reviewed-by: default avatarZentaro Kavanagh <zentaro@chromium.org>
Cr-Commit-Position: refs/heads/master@{#737053}
parent 2a085af2
......@@ -253,6 +253,8 @@ component("ash") {
"display/cursor_window_controller.h",
"display/display_animator.cc",
"display/display_animator.h",
"display/display_change_dialog.cc",
"display/display_change_dialog.h",
"display/display_color_manager.cc",
"display/display_color_manager.h",
"display/display_configuration_controller.cc",
......
......@@ -1251,8 +1251,17 @@ This file contains the strings for ash.
<message name="IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED" desc="The label used in the tray to notify that the display resolution settings has changed.">
<ph name="DISPLAY_NAME">$1<ex>Internal Display</ex></ph>: <ph name="RESOLUTION">$2<ex>2560x1600</ex></ph>
</message>
<message name="IDS_ASH_RESOLUTION_CHANGE_DIALOG_TITLE" desc="The title used in the dialog to notify that the display resolution settings has changed and asks the user to confirm the new resolution.">
Confirm Resolution
</message>
<message name="IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED" desc="The label used in the dialog to notify that the display resolution settings has changed.">
<ph name="DISPLAY_NAME">$1<ex>Google Monitor X</ex></ph> resolution changed to <ph name="RESOLUTION">$2<ex>2560x1600</ex></ph>. Click confirm to keep changes. The previous settings will be restored in <ph name="TIMEOUT_SECONDS">$3</ph>.
</message>
<message name="IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK" desc="The label used in the dialog to notify that the display resolution settings has changed to an unsupported resolution and the system falls back to another resolution.">
<ph name="DISPLAY_NAME">$1<ex>Google Monitor X</ex></ph> doesn't support <ph name="SPECIFIED_RESOLUTION">$2<ex>1920x1080</ex></ph>. The resolution was changed to <ph name="FALLBACK_RESOLUTION">$3<ex>1920x1200</ex></ph>. Click confirm to keep changes. The previous settings will be restored in <ph name="TIMEOUT_SECONDS">$4</ph>.
</message>
<message name="IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED_TO_UNSUPPORTED" desc="The label used in the tray to notify that the display resolution settings has changed to an unsupported resolution and the system falls back to another resolution.">
<ph name="DISPLAY_NAME">$1</ph> doesn't support <ph name="SPECIFIED_RESOLUTION">$2<ex>2560x1600</ex></ph>. The resolution was changed to <ph name="FALLBACK_RESOLUTION">$3<ex>1920x1200</ex></ph>
<ph name="DISPLAY_NAME">$1<ex>Google Monitor X</ex></ph> doesn't support <ph name="SPECIFIED_RESOLUTION">$2<ex>2560x1600</ex></ph>. The resolution was changed to <ph name="FALLBACK_RESOLUTION">$3<ex>1920x1200</ex></ph>.
</message>
<message name="IDS_ASH_STATUS_TRAY_DISPLAY_ROTATED" desc="The label used in the tray to notify that the display rotation settings has changed.">
<ph name="DISPLAY_NAME">$1</ph> was rotated to <ph name="ROTATION">$2</ph>
......@@ -1587,6 +1596,9 @@ This file contains the strings for ash.
<message name="IDS_ASH_SHUTDOWN_CONFIRMATION_CANCEL_BUTTON" desc="The text for the Cancel button in the shutdown confirmation dialog.">
Do it later
</message>
<message name="IDS_ASH_CONFIRM_BUTTON" desc="The text for confirm button on display change confirmation dialogs.">
Confirm
</message>
<message name="IDS_ASH_AUTO_NIGHT_LIGHT_NOTIFY_TITLE" desc="The title of the notification shown when Night Light turns on automatically when the auto-night-light feature is enabled.">
Night Light turns on automatically at sunset
......
// Copyright 2019 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 "ash/display/display_change_dialog.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/bind.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/base/ui_base_types.h"
#include "ui/views/border.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/widget/widget.h"
namespace ash {
DisplayChangeDialog::DisplayChangeDialog(
base::string16 window_title,
base::string16 timeout_message_with_placeholder,
base::OnceClosure on_accept_callback,
CancelCallback on_cancel_callback)
: window_title_(std::move(window_title)),
timeout_message_with_placeholder_(
std::move(timeout_message_with_placeholder)),
on_cancel_callback_(std::move(on_cancel_callback)) {
DialogDelegate::set_button_label(
ui::DIALOG_BUTTON_OK, l10n_util::GetStringUTF16(IDS_ASH_CONFIRM_BUTTON));
DialogDelegate::set_accept_callback(std::move(on_accept_callback));
DialogDelegate::set_cancel_callback(base::BindOnce(
&DisplayChangeDialog::OnCancelButtonClicked, base::Unretained(this)));
SetLayoutManager(std::make_unique<views::FillLayout>());
SetBorder(views::CreateEmptyBorder(
views::LayoutProvider::Get()->GetDialogInsetsForContentType(
views::TEXT, views::TEXT)));
label_ =
AddChildView(std::make_unique<views::Label>(GetRevertTimeoutString()));
label_->SetMultiLine(true);
views::Widget* widget = CreateDialogWidget(
this, nullptr,
Shell::GetContainer(Shell::GetPrimaryRootWindow(),
kShellWindowId_SystemModalContainer));
widget->Show();
timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), this,
&DisplayChangeDialog::OnTimerTick);
}
DisplayChangeDialog::~DisplayChangeDialog() = default;
void DisplayChangeDialog::OnCancelButtonClicked() {
timer_.Stop();
std::move(on_cancel_callback_).Run(/*display_was_removed=*/false);
}
ui::ModalType DisplayChangeDialog::GetModalType() const {
return ui::MODAL_TYPE_SYSTEM;
}
base::string16 DisplayChangeDialog::GetWindowTitle() const {
return window_title_;
}
gfx::Size DisplayChangeDialog::CalculatePreferredSize() const {
return gfx::Size(350, 100);
}
void DisplayChangeDialog::OnTimerTick() {
if (--timeout_count_ == 0) {
CancelDialog();
return;
}
label_->SetText(GetRevertTimeoutString());
}
base::string16 DisplayChangeDialog::GetRevertTimeoutString() const {
const base::string16 timer = ui::TimeFormat::Simple(
ui::TimeFormat::FORMAT_DURATION, ui::TimeFormat::LENGTH_LONG,
base::TimeDelta::FromSeconds(timeout_count_));
return base::ReplaceStringPlaceholders(timeout_message_with_placeholder_,
timer, /*offset=*/nullptr);
}
base::WeakPtr<DisplayChangeDialog> DisplayChangeDialog::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
} // namespace ash
// Copyright 2019 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.
#ifndef ASH_DISPLAY_DISPLAY_CHANGE_DIALOG_H_
#define ASH_DISPLAY_DISPLAY_CHANGE_DIALOG_H_
#include "ash/ash_export.h"
#include "base/callback.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string16.h"
#include "base/timer/timer.h"
#include "ui/views/window/dialog_delegate.h"
namespace views {
class Label;
} // namespace views
namespace ash {
// Modal system dialog that is displayed when a user changes the configuration
// of an external display.
class ASH_EXPORT DisplayChangeDialog : public views::DialogDelegateView {
public:
using CancelCallback = base::OnceCallback<void(bool display_was_removed)>;
DisplayChangeDialog(base::string16 window_title,
base::string16 timeout_message_with_placeholder,
base::OnceClosure on_accept_callback,
CancelCallback on_cancel_callback);
~DisplayChangeDialog() override;
DisplayChangeDialog(const DisplayChangeDialog&) = delete;
DisplayChangeDialog& operator=(const DisplayChangeDialog&) = delete;
ui::ModalType GetModalType() const override;
base::string16 GetWindowTitle() const override;
// views::View:
gfx::Size CalculatePreferredSize() const override;
base::WeakPtr<DisplayChangeDialog> GetWeakPtr();
private:
friend class ResolutionNotificationControllerTest;
FRIEND_TEST_ALL_PREFIXES(ResolutionNotificationControllerTest, Timeout);
static constexpr uint16_t kDefaultTimeoutInSeconds = 10;
void OnCancelButtonClicked();
void OnTimerTick();
// Returns the string displayed as a message in the dialog which includes a
// countdown timer.
base::string16 GetRevertTimeoutString() const;
// The remaining timeout in seconds.
uint16_t timeout_count_ = kDefaultTimeoutInSeconds;
const base::string16 window_title_;
const base::string16 timeout_message_with_placeholder_;
views::Label* label_ = nullptr; // Not owned.
CancelCallback on_cancel_callback_;
// The timer to invoke OnTimerTick() every second. This cannot be
// OneShotTimer since the message contains text "automatically closed in xx
// seconds..." which has to be updated every second.
base::RepeatingTimer timer_;
base::WeakPtrFactory<DisplayChangeDialog> weak_factory_{this};
};
} // namespace ash
#endif // ASH_DISPLAY_DISPLAY_CHANGE_DIALOG_H_
......@@ -6,6 +6,8 @@
#include <utility>
#include "ash/display/display_change_dialog.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
......@@ -179,8 +181,57 @@ void ResolutionNotificationController::Click(
RevertResolutionChange(false /* display_was_removed */);
}
void ResolutionNotificationController::CreateOrReplaceModalDialog() {
DCHECK(features::IsDisplayChangeModalEnabled());
if (confirmation_dialog_)
confirmation_dialog_->GetWidget()->CloseNow();
if (!change_info_)
return;
const base::string16 display_name =
base::UTF8ToUTF16(Shell::Get()->display_manager()->GetDisplayNameForId(
change_info_->display_id));
const base::string16 actual_display_size =
base::UTF8ToUTF16(change_info_->current_resolution.size().ToString());
const base::string16 requested_display_size =
base::UTF8ToUTF16(change_info_->new_resolution.size().ToString());
base::string16 dialog_title =
l10n_util::GetStringUTF16(IDS_ASH_RESOLUTION_CHANGE_DIALOG_TITLE);
// Construct the timeout message, leaving a placeholder for the countdown
// timer so that the string does not need to be completely rebuilt every
// timer tick.
constexpr char kTimeoutPlaceHolder[] = "$1";
base::string16 timeout_message_with_placeholder =
actual_display_size == requested_display_size
? l10n_util::GetStringFUTF16(IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED,
display_name, actual_display_size,
base::UTF8ToUTF16(kTimeoutPlaceHolder))
: l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK, display_name,
requested_display_size, actual_display_size,
base::UTF8ToUTF16(kTimeoutPlaceHolder));
DisplayChangeDialog* dialog = new DisplayChangeDialog(
std::move(dialog_title), std::move(timeout_message_with_placeholder),
base::BindOnce(&ResolutionNotificationController::AcceptResolutionChange,
weak_factory_.GetWeakPtr(), /*close_notification=*/false),
base::BindOnce(&ResolutionNotificationController::RevertResolutionChange,
weak_factory_.GetWeakPtr()));
confirmation_dialog_ = dialog->GetWeakPtr();
}
void ResolutionNotificationController::CreateOrUpdateNotification(
bool enable_spoken_feedback) {
if (features::IsDisplayChangeModalEnabled()) {
CreateOrReplaceModalDialog();
return;
}
message_center::MessageCenter* message_center =
message_center::MessageCenter::Get();
if (!change_info_) {
......@@ -287,8 +338,11 @@ void ResolutionNotificationController::OnTimerTick() {
void ResolutionNotificationController::OnDisplayRemoved(
const display::Display& old_display) {
if (change_info_ && change_info_->display_id == old_display.id())
if (change_info_ && change_info_->display_id == old_display.id()) {
if (confirmation_dialog_)
confirmation_dialog_->GetWidget()->CloseNow();
RevertResolutionChange(true /* display_was_removed */);
}
}
void ResolutionNotificationController::OnDisplayConfigurationChanged() {
......
......@@ -8,6 +8,7 @@
#include <stdint.h>
#include "ash/ash_export.h"
#include "ash/display/display_change_dialog.h"
#include "ash/display/window_tree_host_manager.h"
#include "ash/public/mojom/cros_display_config.mojom.h"
#include "base/callback.h"
......@@ -74,6 +75,10 @@ class ASH_EXPORT ResolutionNotificationController
void Click(const base::Optional<int>& button_index,
const base::Optional<base::string16>& reply) override;
DisplayChangeDialog* dialog_for_testing() const {
return confirmation_dialog_.get();
}
private:
friend class ResolutionNotificationControllerTest;
FRIEND_TEST_ALL_PREFIXES(ResolutionNotificationControllerTest, Timeout);
......@@ -91,6 +96,9 @@ class ASH_EXPORT ResolutionNotificationController
// feedback.
void CreateOrUpdateNotification(bool enable_spoken_feedback);
// Create a new modal dialog, or replace the dialog if it already exists.
void CreateOrReplaceModalDialog();
// Called when the user accepts the display resolution change. Set
// |close_notification| to true when the notification should be removed.
void AcceptResolutionChange(bool close_notification);
......@@ -111,6 +119,8 @@ class ASH_EXPORT ResolutionNotificationController
std::unique_ptr<ResolutionChangeInfo> change_info_;
base::WeakPtr<DisplayChangeDialog> confirmation_dialog_;
base::WeakPtrFactory<ResolutionNotificationController> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(ResolutionNotificationController);
......
......@@ -4,6 +4,7 @@
#include "ash/display/resolution_notification_controller.h"
#include "ash/display/display_change_dialog.h"
#include "ash/public/cpp/ash_features.h"
#include "ash/screen_util.h"
#include "ash/session/session_controller_impl.h"
......@@ -16,11 +17,13 @@
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#include "ui/display/manager/display_manager.h"
#include "ui/gfx/geometry/size.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/notification_list.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/views/controls/label.h"
namespace ash {
......@@ -34,6 +37,15 @@ class ResolutionNotificationControllerTest
base::string16 ExpectedNotificationMessage(int64_t display_id,
const gfx::Size& new_resolution) {
if (features::IsDisplayChangeModalEnabled()) {
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_CHANGED,
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id)),
base::UTF8ToUTF16(new_resolution.ToString()),
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION,
ui::TimeFormat::LENGTH_LONG,
base::TimeDelta::FromSeconds(10)));
}
return l10n_util::GetStringFUTF16(
IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED,
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id)),
......@@ -44,6 +56,16 @@ class ResolutionNotificationControllerTest
int64_t display_id,
const gfx::Size& specified_resolution,
const gfx::Size& fallback_resolution) {
if (features::IsDisplayChangeModalEnabled()) {
return l10n_util::GetStringFUTF16(
IDS_ASH_RESOLUTION_CHANGE_DIALOG_FALLBACK,
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id)),
base::UTF8ToUTF16(specified_resolution.ToString()),
base::UTF8ToUTF16(fallback_resolution.ToString()),
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_DURATION,
ui::TimeFormat::LENGTH_LONG,
base::TimeDelta::FromSeconds(10)));
}
return l10n_util::GetStringFUTF16(
IDS_ASH_STATUS_TRAY_DISPLAY_RESOLUTION_CHANGED_TO_UNSUPPORTED,
base::UTF8ToUTF16(display_manager()->GetDisplayNameForId(display_id)),
......@@ -107,6 +129,9 @@ class ResolutionNotificationControllerTest
}
static base::string16 GetNotificationMessage() {
if (features::IsDisplayChangeModalEnabled())
return controller()->dialog_for_testing()->label_->GetText();
const message_center::NotificationList::Notifications& notifications =
message_center::MessageCenter::Get()->GetVisibleNotifications();
for (message_center::NotificationList::Notifications::const_iterator iter =
......@@ -120,6 +145,10 @@ class ResolutionNotificationControllerTest
}
static void ClickOnNotification() {
if (features::IsDisplayChangeModalEnabled()) {
controller()->dialog_for_testing()->AcceptDialog();
return;
}
message_center::MessageCenter::Get()->ClickOnNotification(
ResolutionNotificationController::kNotificationId);
}
......@@ -130,11 +159,18 @@ class ResolutionNotificationControllerTest
}
static void CloseNotification() {
if (features::IsDisplayChangeModalEnabled()) {
controller()->dialog_for_testing()->AcceptDialog();
return;
}
message_center::MessageCenter::Get()->RemoveNotification(
ResolutionNotificationController::kNotificationId, true /* by_user */);
}
static bool IsNotificationVisible() {
if (features::IsDisplayChangeModalEnabled())
return controller()->dialog_for_testing() != nullptr;
return message_center::MessageCenter::Get()->FindVisibleNotificationById(
ResolutionNotificationController::kNotificationId);
}
......@@ -144,12 +180,25 @@ class ResolutionNotificationControllerTest
ScreenLayoutObserver::kNotificationId);
}
static void TickTimer() { controller()->OnTimerTick(); }
static void TickTimer() {
if (features::IsDisplayChangeModalEnabled()) {
controller()->dialog_for_testing()->OnTimerTick();
return;
}
controller()->OnTimerTick();
}
static ResolutionNotificationController* controller() {
return Shell::Get()->resolution_notification_controller();
}
static void CancelNotification() {
if (features::IsDisplayChangeModalEnabled())
controller()->dialog_for_testing()->CancelDialog();
else
ClickOnNotificationButton(0);
}
int accept_count() const { return accept_count_; }
private:
......@@ -186,7 +235,7 @@ TEST_P(ResolutionNotificationControllerTest, Basic) {
EXPECT_EQ(60.0, mode.refresh_rate());
// Click the revert button, which reverts to the best resolution.
ClickOnNotificationButton(0);
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
......@@ -232,7 +281,6 @@ TEST_P(ResolutionNotificationControllerTest, ClickMeansAccept) {
EXPECT_EQ("200x200", mode.size().ToString());
EXPECT_EQ(60.0, mode.refresh_rate());
// Click the revert button, which reverts the resolution.
ClickOnNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
......@@ -251,10 +299,15 @@ TEST_P(ResolutionNotificationControllerTest, AcceptButton) {
EXPECT_TRUE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
// If there's a single display only, it will have timeout and the first button
// becomes accept.
if (features::IsDisplayChangeModalEnabled()) {
controller()->dialog_for_testing()->AcceptDialog();
base::RunLoop().RunUntilIdle();
} else {
// If there's a single display only, it will have timeout and the first
// button becomes accept.
EXPECT_TRUE(controller()->DoesNotificationTimeout());
ClickOnNotificationButton(0);
}
EXPECT_FALSE(IsNotificationVisible());
EXPECT_EQ(1, accept_count());
......@@ -271,8 +324,13 @@ TEST_P(ResolutionNotificationControllerTest, AcceptButton) {
EXPECT_TRUE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
if (features::IsDisplayChangeModalEnabled()) {
controller()->dialog_for_testing()->CancelDialog();
base::RunLoop().RunUntilIdle();
} else {
EXPECT_TRUE(controller()->DoesNotificationTimeout());
ClickOnNotificationButton(1);
}
EXPECT_FALSE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
EXPECT_EQ(1, accept_count());
......@@ -315,7 +373,11 @@ TEST_P(ResolutionNotificationControllerTest, Timeout) {
display::Screen::GetScreen()->GetPrimaryDisplay();
SetDisplayResolutionAndNotify(display, gfx::Size(200, 200));
for (int i = 0; i < ResolutionNotificationController::kTimeoutInSec; ++i) {
const int timeout_in_seconds =
features::IsDisplayChangeModalEnabled()
? DisplayChangeDialog::kDefaultTimeoutInSeconds
: ResolutionNotificationController::kTimeoutInSec;
for (int i = 0; i < timeout_in_seconds; ++i) {
EXPECT_TRUE(IsNotificationVisible()) << "notification is closed after " << i
<< "-th timer tick";
TickTimer();
......@@ -381,7 +443,7 @@ TEST_P(ResolutionNotificationControllerTest, MultipleResolutionChange) {
// Then, click the revert button. Although |old_resolution| for the second
// SetDisplayResolutionAndNotify is 200x200, it should revert to the original
// size 250x250.
ClickOnNotificationButton(0);
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
......@@ -415,7 +477,7 @@ TEST_P(ResolutionNotificationControllerTest, Fallback) {
EXPECT_EQ(60.0f, mode.refresh_rate());
// Click the revert button, which reverts to the best resolution.
ClickOnNotificationButton(0);
CancelNotification();
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(IsNotificationVisible());
EXPECT_FALSE(IsScreenLayoutObserverNotificationVisible());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment