Add "Add new task" button to glanceables TasksBubbleView
In this iteration it shows only when selected task list is empty and opens Tasks Web UI on press. This CL also indroduces separate `TasksBubbleViewTest` to avoid polluting more generic `DateTrayTest` and test `TasksBubbleView` independently. Also moves some unnecessary test-only methods from production to test code. Bug: b/293305425 Test: ash_unittests --gtest_filter="TasksBubbleViewTest.*" Change-Id: I2bb0e36b0a6b58c27f4304c75ae6ddbe2741085e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4727013 Reviewed-by: James Cook <jamescook@chromium.org> Commit-Queue: Artsiom Mitrokhin <amitrokhin@chromium.org> Reviewed-by: Toni Barzic <tbarzic@chromium.org> Cr-Commit-Position: refs/heads/main@{#1177261}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
4f1a9c09a8
commit
cfc4fa5244
ash
@ -3551,6 +3551,7 @@ test("ash_unittests") {
|
||||
"system/unified/quick_settings_view_unittest.cc",
|
||||
"system/unified/quiet_mode_feature_pod_controller_unittest.cc",
|
||||
"system/unified/screen_capture_tray_item_view_unittest.cc",
|
||||
"system/unified/tasks_bubble_view_unittest.cc",
|
||||
"system/unified/top_shortcuts_view_unittest.cc",
|
||||
"system/unified/unified_system_info_view_unittest.cc",
|
||||
"system/unified/unified_system_tray_controller_unittest.cc",
|
||||
|
@ -6752,6 +6752,9 @@ New install
|
||||
<message name="IDS_GLANCEABLES_LIST_FOOTER_ACTION_BUTTON_LABEL" desc="The glanceable displays classroom or tasks items fetched from Google Classroom or Google Tasks API. Glanceables UI renders only limited number of items, the rest of them can be seen by clicking this button, which will open a corresponding website.">
|
||||
See all
|
||||
</message>
|
||||
<message name="IDS_GLANCEABLES_TASKS_ADD_NEW_TASK_BUTTON_LABEL" desc="Tasks glanceable displays non-completed tasks fetched from Google Tasks API. This text in displayed on the button, which allows creating new tasks (currently it redirects to Tasks Web UI).">
|
||||
Add new task
|
||||
</message>
|
||||
|
||||
<!-- Do not disturb notification -->
|
||||
<message name="IDS_ASH_DO_NOT_DISTURB_NOTIFICATION_TITLE" desc="Label used for the notification that shows up when the 'Do not disturb' feature is enabled.">
|
||||
|
@ -0,0 +1 @@
|
||||
48a625783a776e25e535e4e3032f547f821e2d7c
|
@ -27,6 +27,12 @@ enum class GlanceablesViewId {
|
||||
kClassroomItemDueDateLabel,
|
||||
kClassroomItemDueTimeLabel,
|
||||
kClassroomItemTurnedInAndGradedLabel,
|
||||
|
||||
// `TasksBubbleView`.
|
||||
kTasksBubbleComboBox,
|
||||
kTasksBubbleListContainer,
|
||||
kTasksBubbleAddNewButton,
|
||||
kTasksBubbleListFooter,
|
||||
};
|
||||
|
||||
} // namespace ash
|
||||
|
@ -53,6 +53,8 @@ void FakeGlanceablesTasksClient::PopulateTasks() {
|
||||
"TaskListID1", "Task List 1 Title", base::Time::Now()));
|
||||
task_lists_->Add(std::make_unique<GlanceablesTaskList>(
|
||||
"TaskListID2", "Task List 2 Title", base::Time::Now()));
|
||||
task_lists_->Add(std::make_unique<GlanceablesTaskList>(
|
||||
"TaskListID3", "Task List 3 Title (empty)", base::Time::Now()));
|
||||
}
|
||||
|
||||
void FakeGlanceablesTasksClient::PopulateTaskLists() {
|
||||
@ -82,6 +84,8 @@ void FakeGlanceablesTasksClient::PopulateTaskLists() {
|
||||
/*has_subtasks=*/false, /*has_email_link=*/false));
|
||||
tasks_in_task_lists_.emplace("TaskListID1", std::move(task_list_1));
|
||||
tasks_in_task_lists_.emplace("TaskListID2", std::move(task_list_2));
|
||||
tasks_in_task_lists_.emplace(
|
||||
"TaskListID3", std::make_unique<ui::ListModel<GlanceablesTask>>());
|
||||
}
|
||||
|
||||
} // namespace ash
|
||||
|
@ -127,6 +127,7 @@ aggregate_vector_icons("ash_vector_icons") {
|
||||
"glanceables_classroom_assignment.icon",
|
||||
"glanceables_subtask.icon",
|
||||
"glanceables_tasks.icon",
|
||||
"glanceables_tasks_add_new_task.icon",
|
||||
"glanceables_tasks_due_date.icon",
|
||||
"global_media_controls.icon",
|
||||
"hide_closed_caption.icon",
|
||||
|
@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
CANVAS_DIMENSIONS, 20,
|
||||
MOVE_TO, 10, 16.4f,
|
||||
CUBIC_TO, 9.12f, 16.4f, 8.29f, 16.23f, 7.52f, 15.9f,
|
||||
CUBIC_TO, 6.74f, 15.57f, 6.06f, 15.11f, 5.47f, 14.53f,
|
||||
CUBIC_TO, 4.89f, 13.94f, 4.43f, 13.26f, 4.1f, 12.48f,
|
||||
CUBIC_TO, 3.77f, 11.71f, 3.6f, 10.88f, 3.6f, 10,
|
||||
CUBIC_TO, 3.6f, 9.11f, 3.77f, 8.28f, 4.1f, 7.52f,
|
||||
CUBIC_TO, 4.43f, 6.74f, 4.89f, 6.06f, 5.47f, 5.48f,
|
||||
CUBIC_TO, 6.06f, 4.89f, 6.74f, 4.43f, 7.52f, 4.1f,
|
||||
CUBIC_TO, 8.29f, 3.77f, 9.12f, 3.6f, 10, 3.6f,
|
||||
CUBIC_TO, 10.7f, 3.6f, 11.37f, 3.71f, 12, 3.92f,
|
||||
CUBIC_TO, 12.63f, 4.13f, 13.22f, 4.43f, 13.75f, 4.82f,
|
||||
LINE_TO, 12.88f, 5.68f,
|
||||
CUBIC_TO, 12.47f, 5.39f, 12.02f, 5.18f, 11.53f, 5.03f,
|
||||
CUBIC_TO, 11.04f, 4.88f, 10.53f, 4.8f, 10, 4.8f,
|
||||
CUBIC_TO, 8.56f, 4.8f, 7.33f, 5.31f, 6.32f, 6.32f,
|
||||
CUBIC_TO, 5.31f, 7.33f, 4.8f, 8.56f, 4.8f, 10,
|
||||
CUBIC_TO, 4.8f, 11.44f, 5.31f, 12.67f, 6.32f, 13.68f,
|
||||
CUBIC_TO, 7.33f, 14.69f, 8.56f, 15.2f, 10, 15.2f,
|
||||
CUBIC_TO, 10.41f, 15.2f, 10.81f, 15.16f, 11.18f, 15.07f,
|
||||
CUBIC_TO, 11.57f, 14.98f, 11.94f, 14.85f, 12.28f, 14.68f,
|
||||
LINE_TO, 13.17f, 15.57f,
|
||||
CUBIC_TO, 12.7f, 15.83f, 12.2f, 16.04f, 11.67f, 16.18f,
|
||||
CUBIC_TO, 11.13f, 16.33f, 10.58f, 16.4f, 10, 16.4f,
|
||||
CLOSE,
|
||||
MOVE_TO, 14.4f, 15.2f,
|
||||
V_LINE_TO, 13.2f,
|
||||
H_LINE_TO, 12.4f,
|
||||
V_LINE_TO, 12,
|
||||
H_LINE_TO, 14.4f,
|
||||
V_LINE_TO, 10,
|
||||
H_LINE_TO, 15.6f,
|
||||
V_LINE_TO, 12,
|
||||
H_LINE_TO, 17.6f,
|
||||
V_LINE_TO, 13.2f,
|
||||
H_LINE_TO, 15.6f,
|
||||
V_LINE_TO, 15.2f,
|
||||
H_LINE_TO, 14.4f,
|
||||
CLOSE,
|
||||
MOVE_TO, 9.08f, 12.88f,
|
||||
LINE_TO, 6.4f, 10.2f,
|
||||
LINE_TO, 7.27f, 9.33f,
|
||||
LINE_TO, 9.1f, 11.17f,
|
||||
LINE_TO, 15.55f, 4.72f,
|
||||
LINE_TO, 16.4f, 5.57f,
|
||||
LINE_TO, 9.08f, 12.88f,
|
||||
CLOSE
|
@ -14,9 +14,7 @@
|
||||
#include "ash/glanceables/common/glanceables_view_id.h"
|
||||
#include "ash/glanceables/glanceables_v2_controller.h"
|
||||
#include "ash/glanceables/tasks/fake_glanceables_tasks_client.h"
|
||||
#include "ash/glanceables/tasks/glanceables_task_view.h"
|
||||
#include "ash/public/cpp/test/shell_test_api.h"
|
||||
#include "ash/public/cpp/test/test_new_window_delegate.h"
|
||||
#include "ash/shell.h"
|
||||
#include "ash/system/status_area_widget.h"
|
||||
#include "ash/system/status_area_widget_test_helper.h"
|
||||
@ -37,7 +35,6 @@
|
||||
#include "base/time/time_override.h"
|
||||
#include "base/types/cxx23_to_underlying.h"
|
||||
#include "components/account_id/account_id.h"
|
||||
#include "testing/gmock/include/gmock/gmock.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "ui/events/keycodes/keyboard_codes_posix.h"
|
||||
#include "ui/views/controls/combobox/combobox.h"
|
||||
@ -187,15 +184,6 @@ class TestGlanceablesClassroomClient : public GlanceablesClassroomClient {
|
||||
int bubble_closed_count_ = 0;
|
||||
};
|
||||
|
||||
class MockNewWindowDelegate : public testing::NiceMock<TestNewWindowDelegate> {
|
||||
public:
|
||||
// TestNewWindowDelegate:
|
||||
MOCK_METHOD(void,
|
||||
OpenUrl,
|
||||
(const GURL& url, OpenUrlFrom from, Disposition disposition),
|
||||
(override));
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class DateTrayTest
|
||||
@ -206,12 +194,6 @@ class DateTrayTest
|
||||
DateTrayTest() {
|
||||
scoped_feature_list_.InitWithFeatureState(features::kGlanceablesV2,
|
||||
GetParam());
|
||||
|
||||
auto delegate = std::make_unique<MockNewWindowDelegate>();
|
||||
new_window_delegate_ = delegate.get();
|
||||
window_delegate_provider_ =
|
||||
std::make_unique<ash::TestNewWindowDelegateProvider>(
|
||||
std::move(delegate));
|
||||
}
|
||||
|
||||
DateTrayTest(const DateTrayTest&) = delete;
|
||||
@ -260,8 +242,7 @@ class DateTrayTest
|
||||
|
||||
void TearDown() override {
|
||||
if (AreGlanceablesV2Enabled()) {
|
||||
Shell::Get()->glanceables_v2_controller()->UpdateClientsRegistration(
|
||||
account_id_, GlanceablesV2Controller::ClientsRegistration{});
|
||||
RemoveGlanceablesClients();
|
||||
}
|
||||
|
||||
widget_.reset();
|
||||
@ -333,8 +314,6 @@ class DateTrayTest
|
||||
return fake_glanceables_tasks_client_.get();
|
||||
}
|
||||
|
||||
MockNewWindowDelegate* new_window_delegate() { return new_window_delegate_; }
|
||||
|
||||
void RemoveGlanceablesClients() {
|
||||
Shell::Get()->glanceables_v2_controller()->UpdateClientsRegistration(
|
||||
account_id_, GlanceablesV2Controller::ClientsRegistration{
|
||||
@ -342,6 +321,7 @@ class DateTrayTest
|
||||
}
|
||||
|
||||
private:
|
||||
base::test::ScopedFeatureList scoped_feature_list_;
|
||||
std::unique_ptr<views::Widget> widget_;
|
||||
AccountId account_id_ =
|
||||
AccountId::FromUserEmailGaiaId("test_user@gmail.com", "123456");
|
||||
@ -353,12 +333,6 @@ class DateTrayTest
|
||||
raw_ptr<DateTray, ExperimentalAsh> date_tray_ = nullptr;
|
||||
|
||||
raw_ptr<UnifiedSystemTray, ExperimentalAsh> unified_system_tray_ = nullptr;
|
||||
|
||||
base::test::ScopedFeatureList scoped_feature_list_;
|
||||
|
||||
std::unique_ptr<ash::TestNewWindowDelegateProvider> window_delegate_provider_;
|
||||
raw_ptr<MockNewWindowDelegate, DanglingUntriaged> new_window_delegate_ =
|
||||
nullptr;
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(GlanceablesV2, DateTrayTest, testing::Bool());
|
||||
@ -394,100 +368,6 @@ TEST_P(DateTrayTest, InitialState) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DateTrayTest, ShowTasksComboModel) {
|
||||
LeftClickOn(GetDateTray());
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_TRUE(IsBubbleShown());
|
||||
EXPECT_TRUE(AreContentsViewShown());
|
||||
|
||||
if (!AreGlanceablesV2Enabled()) {
|
||||
EXPECT_EQ(GetGlanceableTrayBubble(), nullptr);
|
||||
} else {
|
||||
auto* tasks_view = GetGlanceableTrayBubble()->GetTasksView();
|
||||
EXPECT_TRUE(tasks_view->GetVisible());
|
||||
EXPECT_FALSE(tasks_view->IsMenuRunning());
|
||||
EXPECT_TRUE(tasks_view->task_list_combo_box_view()->GetVisible());
|
||||
tasks_view->GetWidget()->LayoutRootViewIfNecessary();
|
||||
|
||||
EXPECT_EQ(tasks_view->task_items_container_view()->children().size(), 2u);
|
||||
|
||||
tasks_view->task_list_combo_box_view()->ScrollViewToVisible();
|
||||
tasks_view->GetWidget()->LayoutRootViewIfNecessary();
|
||||
|
||||
// Verify that tapping on combobox opens the selection menu.
|
||||
GestureTapOn(tasks_view->task_list_combo_box_view());
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_TRUE(tasks_view->IsMenuRunning());
|
||||
|
||||
// Select the next task list using keyboard navigation.
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
|
||||
|
||||
// Verify the number of items in task_items_container_view()->children().
|
||||
EXPECT_EQ(tasks_view->task_items_container_view()->children().size(), 3u);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DateTrayTest, MarkTaskAsComplete) {
|
||||
LeftClickOn(GetDateTray());
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_TRUE(IsBubbleShown());
|
||||
EXPECT_TRUE(AreContentsViewShown());
|
||||
|
||||
if (!AreGlanceablesV2Enabled()) {
|
||||
EXPECT_EQ(GetGlanceableTrayBubble(), nullptr);
|
||||
} else {
|
||||
EXPECT_TRUE(GetGlanceableTrayBubble()->GetTasksView()->GetVisible());
|
||||
EXPECT_FALSE(GetGlanceableTrayBubble()->GetTasksView()->IsMenuRunning());
|
||||
EXPECT_TRUE(GetGlanceableTrayBubble()
|
||||
->GetTasksView()
|
||||
->task_list_combo_box_view()
|
||||
->GetVisible());
|
||||
EXPECT_EQ(GetGlanceableTrayBubble()
|
||||
->GetTasksView()
|
||||
->task_items_container_view()
|
||||
->children()
|
||||
.size(),
|
||||
2u);
|
||||
|
||||
// Verify that tapping on combobox opens the selection menu.
|
||||
GlanceablesTaskView* task_view = views::AsViewClass<GlanceablesTaskView>(
|
||||
GetGlanceableTrayBubble()
|
||||
->GetTasksView()
|
||||
->task_items_container_view()
|
||||
->children()[0]);
|
||||
|
||||
ASSERT_TRUE(task_view);
|
||||
task_view->GetWidget()->LayoutRootViewIfNecessary();
|
||||
ASSERT_FALSE(task_view->GetCompletedForTest());
|
||||
ASSERT_EQ(0u, fake_glanceables_tasks_client()->completed_tasks().size());
|
||||
GestureTapOn(task_view->GetButtonForTest());
|
||||
ASSERT_TRUE(task_view->GetCompletedForTest());
|
||||
ASSERT_EQ(1u, fake_glanceables_tasks_client()->completed_tasks().size());
|
||||
ASSERT_EQ("TaskListID1:TaskListItem1",
|
||||
fake_glanceables_tasks_client()->completed_tasks().front());
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that tapping the tasks glanceable action button opens a browser page.
|
||||
TEST_P(DateTrayTest, ShowTasksWebUI) {
|
||||
LeftClickOn(GetDateTray());
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_TRUE(IsBubbleShown());
|
||||
EXPECT_TRUE(AreContentsViewShown());
|
||||
|
||||
if (!AreGlanceablesV2Enabled()) {
|
||||
EXPECT_EQ(GetGlanceableTrayBubble(), nullptr);
|
||||
} else {
|
||||
const auto* const see_all_button = views::AsViewClass<views::LabelButton>(
|
||||
GetGlanceableTrayBubble()->GetTasksView()->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kListFooterSeeAllButton)));
|
||||
EXPECT_CALL(*new_window_delegate(), OpenUrl).Times(1);
|
||||
GestureTapOn(see_all_button);
|
||||
}
|
||||
}
|
||||
|
||||
// Tests clicking/tapping the DateTray shows/closes the calendar bubble.
|
||||
TEST_P(DateTrayTest, ShowCalendarBubble) {
|
||||
base::HistogramTester histogram_tester;
|
||||
|
@ -8,21 +8,28 @@
|
||||
#include <memory>
|
||||
|
||||
#include "ash/glanceables/common/glanceables_list_footer_view.h"
|
||||
#include "ash/glanceables/common/glanceables_view_id.h"
|
||||
#include "ash/glanceables/glanceables_v2_controller.h"
|
||||
#include "ash/glanceables/tasks/glanceables_task_view.h"
|
||||
#include "ash/glanceables/tasks/glanceables_tasks_client.h"
|
||||
#include "ash/public/cpp/new_window_delegate.h"
|
||||
#include "ash/resources/vector_icons/vector_icons.h"
|
||||
#include "ash/shell.h"
|
||||
#include "ash/strings/grit/ash_strings.h"
|
||||
#include "ash/style/ash_color_id.h"
|
||||
#include "ash/system/tray/tray_constants.h"
|
||||
#include "ash/system/unified/glanceable_tray_child_bubble.h"
|
||||
#include "ash/system/unified/tasks_combobox_model.h"
|
||||
#include "base/types/cxx23_to_underlying.h"
|
||||
#include "chromeos/constants/chromeos_features.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
#include "ui/base/metadata/metadata_impl_macros.h"
|
||||
#include "ui/base/models/image_model.h"
|
||||
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
|
||||
#include "ui/compositor/layer.h"
|
||||
#include "ui/gfx/text_constants.h"
|
||||
#include "ui/views/accessibility/view_accessibility.h"
|
||||
#include "ui/views/background.h"
|
||||
#include "ui/views/controls/button/button.h"
|
||||
#include "ui/views/controls/button/label_button.h"
|
||||
#include "ui/views/controls/combobox/combobox.h"
|
||||
#include "ui/views/controls/image_view.h"
|
||||
@ -34,8 +41,9 @@ namespace {
|
||||
constexpr int kMaximumTasks = 5;
|
||||
constexpr int kTasksIconRightPadding = 4;
|
||||
constexpr int kTasksIconViewSize = 32;
|
||||
constexpr int kSpacingAboveListContainerView = 16;
|
||||
constexpr int kInteriorGlanceableBubbleMargin = 16;
|
||||
constexpr auto kHeaderViewMargins = gfx::Insets::TLBR(0, 0, 16, 0);
|
||||
constexpr auto kAddNewTaskButtonMargins = gfx::Insets::TLBR(0, 0, 16, 0);
|
||||
|
||||
constexpr char kTasksManagementPage[] =
|
||||
"https://calendar.google.com/calendar/u/0/r/week?opentasks=1";
|
||||
@ -57,11 +65,6 @@ TasksBubbleView::TasksBubbleView(DetailedViewDelegate* delegate)
|
||||
|
||||
TasksBubbleView::~TasksBubbleView() = default;
|
||||
|
||||
bool TasksBubbleView::IsMenuRunning() {
|
||||
return task_list_combo_box_view_ &&
|
||||
task_list_combo_box_view_->IsMenuRunning();
|
||||
}
|
||||
|
||||
void TasksBubbleView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
|
||||
// TODO(b:277268122): Implement accessibility behavior.
|
||||
if (!GetVisible()) {
|
||||
@ -94,11 +97,11 @@ void TasksBubbleView::InitViews(ui::ListModel<GlanceablesTaskList>* task_list) {
|
||||
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
|
||||
views::MaximumFlexSizeRule::kPreferred)
|
||||
.WithOrder(1));
|
||||
tasks_header_view_->SetProperty(views::kMarginsKey, kHeaderViewMargins);
|
||||
|
||||
task_items_container_view_ = AddChildView(std::make_unique<views::View>());
|
||||
task_items_container_view_->SetProperty(
|
||||
views::kMarginsKey,
|
||||
gfx::Insets::TLBR(kSpacingAboveListContainerView, 0, 0, 0));
|
||||
task_items_container_view_->SetID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleListContainer));
|
||||
task_items_container_view_->SetPaintToLayer();
|
||||
task_items_container_view_->layer()->SetFillsBoundsOpaquely(false);
|
||||
task_items_container_view_->layer()->SetRoundedCornerRadius(
|
||||
@ -108,6 +111,27 @@ void TasksBubbleView::InitViews(ui::ListModel<GlanceablesTaskList>* task_list) {
|
||||
views::BoxLayout::Orientation::kVertical));
|
||||
layout->set_between_child_spacing(2);
|
||||
|
||||
add_new_task_button_ = AddChildView(std::make_unique<views::LabelButton>(
|
||||
base::BindRepeating(&TasksBubbleView::ActionButtonPressed,
|
||||
base::Unretained(this)),
|
||||
l10n_util::GetStringUTF16(
|
||||
IDS_GLANCEABLES_TASKS_ADD_NEW_TASK_BUTTON_LABEL)));
|
||||
add_new_task_button_->SetID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleAddNewButton));
|
||||
add_new_task_button_->SetImageModel(
|
||||
views::Button::ButtonState::STATE_NORMAL,
|
||||
ui::ImageModel::FromVectorIcon(kGlanceablesTasksAddNewTaskIcon,
|
||||
cros_tokens::kCrosSysOnSurface));
|
||||
add_new_task_button_->SetHorizontalAlignment(
|
||||
gfx::HorizontalAlignment::ALIGN_CENTER);
|
||||
add_new_task_button_->SetImageLabelSpacing(8);
|
||||
add_new_task_button_->SetBackground(views::CreateThemedRoundedRectBackground(
|
||||
cros_tokens::kCrosSysSystemOnBase, 16));
|
||||
add_new_task_button_->SetTextColorId(views::Button::ButtonState::STATE_NORMAL,
|
||||
cros_tokens::kCrosSysOnSurface);
|
||||
add_new_task_button_->SetProperty(views::kMarginsKey,
|
||||
kAddNewTaskButtonMargins);
|
||||
|
||||
task_icon_view_ =
|
||||
tasks_header_view_->AddChildView(std::make_unique<views::ImageView>());
|
||||
task_icon_view_->SetPreferredSize(
|
||||
@ -127,6 +151,8 @@ void TasksBubbleView::InitViews(ui::ListModel<GlanceablesTaskList>* task_list) {
|
||||
tasks_combobox_model_ = std::make_unique<TasksComboboxModel>(task_list);
|
||||
task_list_combo_box_view_ = tasks_header_view_->AddChildView(
|
||||
std::make_unique<views::Combobox>(tasks_combobox_model_.get()));
|
||||
task_list_combo_box_view_->SetID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleComboBox));
|
||||
task_list_combo_box_view_->SetSizeToLargestLabel(false);
|
||||
|
||||
// TODO(b:277268122): Implement accessibility behavior.
|
||||
@ -139,8 +165,10 @@ void TasksBubbleView::InitViews(ui::ListModel<GlanceablesTaskList>* task_list) {
|
||||
list_footer_view_ = AddChildView(
|
||||
std::make_unique<GlanceablesListFooterView>(base::BindRepeating(
|
||||
&TasksBubbleView::ActionButtonPressed, base::Unretained(this))));
|
||||
list_footer_view_->SetID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleListFooter));
|
||||
|
||||
ScheduleUpdateTasksList();
|
||||
SelectedTasksListChanged();
|
||||
}
|
||||
|
||||
void TasksBubbleView::ActionButtonPressed() {
|
||||
@ -152,6 +180,9 @@ void TasksBubbleView::ActionButtonPressed() {
|
||||
|
||||
void TasksBubbleView::SelectedTasksListChanged() {
|
||||
task_items_container_view_->RemoveAllChildViews();
|
||||
task_items_container_view_->SetVisible(false);
|
||||
list_footer_view_->SetVisible(false);
|
||||
add_new_task_button_->SetVisible(true);
|
||||
ScheduleUpdateTasksList();
|
||||
}
|
||||
|
||||
@ -172,7 +203,7 @@ void TasksBubbleView::UpdateTasksList(const std::string& task_list_id,
|
||||
ui::ListModel<GlanceablesTask>* tasks) {
|
||||
const int old_tasks_shown = num_tasks_shown_;
|
||||
num_tasks_shown_ = 0;
|
||||
int num_tasks_ = 0;
|
||||
int num_tasks = 0;
|
||||
for (const auto& task : *tasks) {
|
||||
if (task->completed) {
|
||||
continue;
|
||||
@ -185,10 +216,13 @@ void TasksBubbleView::UpdateTasksList(const std::string& task_list_id,
|
||||
view->SetOrientation(views::LayoutOrientation::kHorizontal);
|
||||
++num_tasks_shown_;
|
||||
}
|
||||
++num_tasks_;
|
||||
++num_tasks;
|
||||
}
|
||||
task_items_container_view_->SetVisible(num_tasks_shown_ > 0);
|
||||
add_new_task_button_->SetVisible(num_tasks_shown_ == 0);
|
||||
|
||||
list_footer_view_->UpdateItemsCount(num_tasks_shown_, num_tasks_);
|
||||
list_footer_view_->UpdateItemsCount(num_tasks_shown_, num_tasks);
|
||||
list_footer_view_->SetVisible(num_tasks_shown_ > 0);
|
||||
|
||||
if (old_tasks_shown != num_tasks_shown_) {
|
||||
PreferredSizeChanged();
|
||||
|
@ -16,6 +16,7 @@
|
||||
namespace views {
|
||||
class Combobox;
|
||||
class ImageView;
|
||||
class LabelButton;
|
||||
} // namespace views
|
||||
|
||||
namespace ash {
|
||||
@ -34,6 +35,9 @@ class TasksComboboxModel;
|
||||
// | |'task_items_container_view_' | |
|
||||
// | +-----------------------------------------------------------+ |
|
||||
// | +-----------------------------------------------------------+ |
|
||||
// | |'add_new_task_button_' | |
|
||||
// | +-----------------------------------------------------------+ |
|
||||
// | +-----------------------------------------------------------+ |
|
||||
// | |'tasks_footer_view_' | |
|
||||
// | +-----------------------------------------------------------+ |
|
||||
// +---------------------------------------------------------------+
|
||||
@ -68,25 +72,15 @@ class ASH_EXPORT TasksBubbleView : public GlanceableTrayChildBubble {
|
||||
TasksBubbleView& operator=(const TasksBubbleView&) = delete;
|
||||
~TasksBubbleView() override;
|
||||
|
||||
bool IsMenuRunning();
|
||||
|
||||
views::Combobox* task_list_combo_box_view() const {
|
||||
return task_list_combo_box_view_;
|
||||
}
|
||||
|
||||
views::View* task_items_container_view() const {
|
||||
return task_items_container_view_;
|
||||
}
|
||||
|
||||
// views::View:
|
||||
void GetAccessibleNodeData(ui::AXNodeData* node_data) override;
|
||||
|
||||
private:
|
||||
friend class DateTrayTest;
|
||||
// Setup child views.
|
||||
void InitViews(ui::ListModel<GlanceablesTaskList>* task_list);
|
||||
|
||||
// Handles on-click behavior for the "See all" button in `list_footer_view_`.
|
||||
// Handles press behavior for the "See all" button in `list_footer_view_` and
|
||||
// `add_new_task_button_`.
|
||||
void ActionButtonPressed();
|
||||
|
||||
// Handles switching between tasks lists.
|
||||
@ -107,6 +101,7 @@ class ASH_EXPORT TasksBubbleView : public GlanceableTrayChildBubble {
|
||||
raw_ptr<views::Combobox, ExperimentalAsh> task_list_combo_box_view_ = nullptr;
|
||||
raw_ptr<views::FlexLayoutView, ExperimentalAsh> button_container_ = nullptr;
|
||||
raw_ptr<views::View, ExperimentalAsh> task_items_container_view_ = nullptr;
|
||||
raw_ptr<views::LabelButton, ExperimentalAsh> add_new_task_button_ = nullptr;
|
||||
raw_ptr<GlanceablesListFooterView, ExperimentalAsh> list_footer_view_ =
|
||||
nullptr;
|
||||
|
||||
|
192
ash/system/unified/tasks_bubble_view_unittest.cc
Normal file
192
ash/system/unified/tasks_bubble_view_unittest.cc
Normal file
@ -0,0 +1,192 @@
|
||||
// Copyright 2023 The Chromium Authors
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
#include "ash/constants/ash_features.h"
|
||||
#include "ash/glanceables/common/glanceables_list_footer_view.h"
|
||||
#include "ash/glanceables/common/glanceables_view_id.h"
|
||||
#include "ash/glanceables/glanceables_v2_controller.h"
|
||||
#include "ash/glanceables/tasks/fake_glanceables_tasks_client.h"
|
||||
#include "ash/glanceables/tasks/glanceables_task_view.h"
|
||||
#include "ash/public/cpp/test/test_new_window_delegate.h"
|
||||
#include "ash/shell.h"
|
||||
#include "ash/system/tray/detailed_view_delegate.h"
|
||||
#include "ash/system/unified/tasks_bubble_view.h"
|
||||
#include "ash/test/ash_test_base.h"
|
||||
#include "base/allocator/partition_allocator/pointers/raw_ptr.h"
|
||||
#include "base/run_loop.h"
|
||||
#include "base/test/scoped_feature_list.h"
|
||||
#include "base/types/cxx23_to_underlying.h"
|
||||
#include "components/account_id/account_id.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "ui/events/keycodes/keyboard_codes_posix.h"
|
||||
#include "ui/views/controls/button/label_button.h"
|
||||
#include "ui/views/controls/combobox/combobox.h"
|
||||
#include "ui/views/view.h"
|
||||
#include "ui/views/view_utils.h"
|
||||
#include "ui/views/widget/widget.h"
|
||||
#include "url/gurl.h"
|
||||
|
||||
namespace ash {
|
||||
namespace {
|
||||
|
||||
class TestNewWindowDelegateImpl : public TestNewWindowDelegate {
|
||||
public:
|
||||
// TestNewWindowDelegate:
|
||||
void OpenUrl(const GURL& url,
|
||||
OpenUrlFrom from,
|
||||
Disposition disposition) override {
|
||||
last_opened_url_ = url;
|
||||
}
|
||||
|
||||
GURL last_opened_url() const { return last_opened_url_; }
|
||||
|
||||
private:
|
||||
GURL last_opened_url_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class TasksBubbleViewTest : public AshTestBase {
|
||||
public:
|
||||
TasksBubbleViewTest() {
|
||||
auto new_window_delegate = std::make_unique<TestNewWindowDelegateImpl>();
|
||||
new_window_delegate_ = new_window_delegate.get();
|
||||
new_window_delegate_provider_ =
|
||||
std::make_unique<TestNewWindowDelegateProvider>(
|
||||
std::move(new_window_delegate));
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
AshTestBase::SetUp();
|
||||
SimulateUserLogin(account_id_);
|
||||
Shell::Get()->glanceables_v2_controller()->UpdateClientsRegistration(
|
||||
account_id_, GlanceablesV2Controller::ClientsRegistration{
|
||||
.tasks_client = &tasks_client_});
|
||||
ASSERT_TRUE(Shell::Get()->glanceables_v2_controller()->GetTasksClient());
|
||||
|
||||
widget_ = CreateFramelessTestWidget();
|
||||
widget_->SetFullscreen(true);
|
||||
|
||||
view_ = widget_->SetContentsView(
|
||||
std::make_unique<TasksBubbleView>(&detailed_view_delegate_));
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
// Destroy `widget_` first, before destroying `LayoutProvider` (needed in
|
||||
// the `views::Combobox`'s destruction chain).
|
||||
view_ = nullptr;
|
||||
widget_.reset();
|
||||
AshTestBase::TearDown();
|
||||
}
|
||||
|
||||
views::Combobox* GetComboBoxView() const {
|
||||
return views::AsViewClass<views::Combobox>(view_->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleComboBox)));
|
||||
}
|
||||
|
||||
bool IsMenuRunning() const {
|
||||
const auto* const combo_box = GetComboBoxView();
|
||||
return combo_box && combo_box->IsMenuRunning();
|
||||
}
|
||||
|
||||
const views::View* GetTaskItemsContainerView() const {
|
||||
return views::AsViewClass<views::View>(view_->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleListContainer)));
|
||||
}
|
||||
|
||||
const views::LabelButton* GetAddNewTaskButton() const {
|
||||
return views::AsViewClass<views::LabelButton>(view_->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleAddNewButton)));
|
||||
}
|
||||
|
||||
const GlanceablesListFooterView* GetListFooterView() const {
|
||||
return views::AsViewClass<GlanceablesListFooterView>(view_->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kTasksBubbleListFooter)));
|
||||
}
|
||||
|
||||
const FakeGlanceablesTasksClient* tasks_client() const {
|
||||
return &tasks_client_;
|
||||
}
|
||||
|
||||
const TestNewWindowDelegateImpl* new_window_delegate() const {
|
||||
return new_window_delegate_;
|
||||
}
|
||||
|
||||
private:
|
||||
base::test::ScopedFeatureList feature_list_{features::kGlanceablesV2};
|
||||
AccountId account_id_ = AccountId::FromUserEmail("test_user@gmail.com");
|
||||
FakeGlanceablesTasksClient tasks_client_;
|
||||
std::unique_ptr<ash::TestNewWindowDelegateProvider>
|
||||
new_window_delegate_provider_;
|
||||
raw_ptr<TestNewWindowDelegateImpl> new_window_delegate_;
|
||||
DetailedViewDelegate detailed_view_delegate_{nullptr};
|
||||
raw_ptr<TasksBubbleView> view_;
|
||||
std::unique_ptr<views::Widget> widget_;
|
||||
};
|
||||
|
||||
TEST_F(TasksBubbleViewTest, ShowTasksComboModel) {
|
||||
EXPECT_FALSE(IsMenuRunning());
|
||||
EXPECT_TRUE(GetComboBoxView()->GetVisible());
|
||||
|
||||
EXPECT_EQ(GetTaskItemsContainerView()->children().size(), 2u);
|
||||
|
||||
// Verify that tapping on combobox opens the selection menu.
|
||||
GestureTapOn(GetComboBoxView());
|
||||
base::RunLoop().RunUntilIdle();
|
||||
EXPECT_TRUE(IsMenuRunning());
|
||||
|
||||
// Select the next task list using keyboard navigation.
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_DOWN);
|
||||
PressAndReleaseKey(ui::KeyboardCode::VKEY_RETURN);
|
||||
|
||||
// Verify the number of items in task_items_container_view()->children().
|
||||
EXPECT_EQ(GetTaskItemsContainerView()->children().size(), 3u);
|
||||
}
|
||||
|
||||
TEST_F(TasksBubbleViewTest, MarkTaskAsComplete) {
|
||||
EXPECT_EQ(GetTaskItemsContainerView()->children().size(), 2u);
|
||||
|
||||
auto* const task_view = views::AsViewClass<GlanceablesTaskView>(
|
||||
GetTaskItemsContainerView()->children()[0]);
|
||||
ASSERT_TRUE(task_view);
|
||||
ASSERT_FALSE(task_view->GetCompletedForTest());
|
||||
ASSERT_TRUE(tasks_client()->completed_tasks().empty());
|
||||
|
||||
GestureTapOn(task_view->GetButtonForTest());
|
||||
ASSERT_TRUE(task_view->GetCompletedForTest());
|
||||
ASSERT_EQ(tasks_client()->completed_tasks().size(), 1u);
|
||||
EXPECT_EQ(tasks_client()->completed_tasks().front(),
|
||||
"TaskListID1:TaskListItem1");
|
||||
}
|
||||
|
||||
TEST_F(TasksBubbleViewTest, ShowTasksWebUI) {
|
||||
const auto* const see_all_button =
|
||||
views::AsViewClass<views::LabelButton>(GetListFooterView()->GetViewByID(
|
||||
base::to_underlying(GlanceablesViewId::kListFooterSeeAllButton)));
|
||||
GestureTapOn(see_all_button);
|
||||
EXPECT_EQ(new_window_delegate()->last_opened_url(),
|
||||
"https://calendar.google.com/calendar/u/0/r/week?opentasks=1");
|
||||
}
|
||||
|
||||
TEST_F(TasksBubbleViewTest, ShowsAndHidesAddNewButton) {
|
||||
// Shows items from the first / default task list.
|
||||
EXPECT_TRUE(GetTaskItemsContainerView()->GetVisible());
|
||||
EXPECT_EQ(GetTaskItemsContainerView()->children().size(), 2u);
|
||||
EXPECT_FALSE(GetAddNewTaskButton()->GetVisible());
|
||||
EXPECT_TRUE(GetListFooterView()->GetVisible());
|
||||
|
||||
// Switch to the empty task list.
|
||||
ASSERT_EQ(GetComboBoxView()->GetTextForRow(2), u"Task List 3 Title (empty)");
|
||||
GetComboBoxView()->MenuSelectionAt(2);
|
||||
EXPECT_FALSE(GetTaskItemsContainerView()->GetVisible());
|
||||
EXPECT_TRUE(GetTaskItemsContainerView()->children().empty());
|
||||
EXPECT_TRUE(GetAddNewTaskButton()->GetVisible());
|
||||
EXPECT_FALSE(GetListFooterView()->GetVisible());
|
||||
}
|
||||
|
||||
} // namespace ash
|
Reference in New Issue
Block a user