blob: 0703b959d182a0a4812cfef38a4044548428f6f0 [file] [log] [blame]
// 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/shelf/default_shelf_view.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/shelf/app_list_button.h"
#include "ash/shelf/back_button.h"
#include "ash/shelf/overflow_button.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_constants.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/model/virtual_keyboard_model.h"
#include "ash/system/status_area_widget.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/separator.h"
#include "ui/views/view.h"
#include "ui/views/view_model.h"
namespace ash {
namespace {
// White with ~20% opacity.
constexpr SkColor kSeparatorColor = SkColorSetARGB(0x32, 0xFF, 0xFF, 0xFF);
// The dimensions, in pixels, of the separator between pinned and unpinned
// items.
constexpr int kSeparatorSize = 20;
constexpr int kSeparatorThickness = 1;
// The margin between app icons and the overflow button. This is bigger than
// between app icons because the overflow button is a little smaller.
constexpr int kMarginBetweenAppsAndOverflow = 16;
// The margin on either side of the group of app icons (including the overflow
// button).
constexpr int kAppIconGroupMargin = 16;
// Helper to check if tablet mode is enabled.
bool IsTabletModeEnabled() {
// This check is needed, because tablet mode controller is destroyed before
// shelf widget. See https://crbug.com/967149 for more details.
return Shell::Get()->tablet_mode_controller() &&
Shell::Get()->tablet_mode_controller()->InTabletMode();
}
// Returns the size occupied by |count| app icons. If |with_overflow| is
// true, returns the size of |count| app icons followed by an overflow
// button.
int GetSizeOfAppIcons(int count, bool with_overflow) {
if (count == 0)
return with_overflow ? kShelfControlSize : 0;
const int app_size = count * kShelfButtonSize;
int overflow_size = 0;
int total_padding = ShelfConstants::button_spacing() * (count - 1);
if (with_overflow) {
overflow_size += kShelfControlSize;
total_padding += kMarginBetweenAppsAndOverflow;
}
return app_size + total_padding + overflow_size;
}
} // namespace
DefaultShelfView::DefaultShelfView(ShelfModel* model,
Shelf* shelf,
ShelfWidget* shelf_widget)
: ShelfView(model, shelf, shelf_widget) {}
DefaultShelfView::~DefaultShelfView() = default;
void DefaultShelfView::CalculateBackAndHomeButtonsIdealBounds() {
if (is_overflow_mode())
return;
const int button_spacing = ShelfConstants::button_spacing();
int x = shelf()->PrimaryAxisValue(button_spacing, 0);
int y = shelf()->PrimaryAxisValue(0, button_spacing);
GetBackButton()->set_ideal_bounds(gfx::Rect(
x, y, shelf()->PrimaryAxisValue(kShelfControlSize, kShelfButtonSize),
shelf()->PrimaryAxisValue(kShelfButtonSize, kShelfControlSize)));
// If we are not in tablet mode, the home button will get placed on top of
// the (invisible) back button.
if (IsTabletModeEnabled()) {
x = shelf()->PrimaryAxisValue(x + kShelfControlSize + button_spacing, x);
y = shelf()->PrimaryAxisValue(y, y + kShelfControlSize + button_spacing);
}
GetAppListButton()->set_ideal_bounds(gfx::Rect(
x, y, shelf()->PrimaryAxisValue(kShelfControlSize, kShelfButtonSize),
shelf()->PrimaryAxisValue(kShelfButtonSize, kShelfControlSize)));
}
void DefaultShelfView::Init() {
// Add the background view behind the app list and back buttons first, so
// that other views will appear above it.
back_and_app_list_background_ = new views::View();
back_and_app_list_background_->set_can_process_events_within_subtree(false);
back_and_app_list_background_->SetBackground(
CreateBackgroundFromPainter(views::Painter::CreateSolidRoundRectPainter(
kShelfControlPermanentHighlightBackground,
ShelfConstants::control_border_radius())));
ConfigureChildView(back_and_app_list_background_);
AddChildView(back_and_app_list_background_);
separator_ = new views::Separator();
separator_->SetColor(kSeparatorColor);
separator_->SetPreferredHeight(kSeparatorSize);
separator_->SetVisible(false);
ConfigureChildView(separator_);
AddChildView(separator_);
// Calling Init() after adding background view, so control buttons are added
// after background.
ShelfView::Init();
}
void DefaultShelfView::CalculateIdealBounds() {
DCHECK(model()->item_count() == view_model()->view_size());
const int button_spacing = ShelfConstants::button_spacing();
const int available_size = shelf()->PrimaryAxisValue(width(), height());
const int separator_index = GetSeparatorIndex();
const AppCenteringStrategy app_centering_strategy =
CalculateAppCenteringStrategy();
// At this point we know that |last_visible_index_| is up to date.
const bool virtual_keyboard_visible =
Shell::Get()->system_tray_model()->virtual_keyboard()->visible();
// Don't show the separator if it isn't needed, or would appear after all
// visible items.
separator_->SetVisible(separator_index != -1 &&
separator_index < last_visible_index() &&
!virtual_keyboard_visible);
int x = 0;
int y = 0;
if (!is_overflow_mode()) {
// Calculate the bounds for the back and home buttons separately.
CalculateBackAndHomeButtonsIdealBounds();
int app_list_button_position =
shelf()->PrimaryAxisValue(GetAppListButton()->ideal_bounds().right(),
GetAppListButton()->ideal_bounds().bottom());
x = shelf()->PrimaryAxisValue(app_list_button_position, 0);
y = shelf()->PrimaryAxisValue(0, app_list_button_position);
// A larger minimum padding after the app list button is required:
// increment with the necessary extra amount.
x += shelf()->PrimaryAxisValue(kAppIconGroupMargin - button_spacing, 0);
y += shelf()->PrimaryAxisValue(0, kAppIconGroupMargin - button_spacing);
// Now add the necessary padding to center app icons.
StatusAreaWidget* status_widget = shelf_widget()->status_area_widget();
const int status_widget_size =
status_widget ? shelf()->PrimaryAxisValue(
status_widget->GetWindowBoundsInScreen().width(),
status_widget->GetWindowBoundsInScreen().height())
: 0;
const int screen_size = available_size + status_widget_size;
const int available_size_for_app_icons = GetAvailableSpaceForAppIcons();
const int icons_size = GetSizeOfAppIcons(number_of_visible_apps(),
app_centering_strategy.overflow);
int padding_for_centering = 0;
if (app_centering_strategy.center_on_screen) {
padding_for_centering = (screen_size - icons_size) / 2;
} else {
padding_for_centering =
kShelfButtonSpacing +
(IsTabletModeEnabled() ? 2 : 1) * kShelfControlSize +
kAppIconGroupMargin + (available_size_for_app_icons - icons_size) / 2;
}
if (padding_for_centering >
app_list_button_position + kAppIconGroupMargin) {
// Only shift buttons to the right, never let them interfere with the
// left-aligned system buttons.
x = shelf()->PrimaryAxisValue(padding_for_centering, 0);
y = shelf()->PrimaryAxisValue(0, padding_for_centering);
}
}
for (int i = 0; i < view_model()->view_size(); ++i) {
if (is_overflow_mode() && i < first_visible_index()) {
view_model()->set_ideal_bounds(i, gfx::Rect(x, y, 0, 0));
continue;
}
view_model()->set_ideal_bounds(
i, gfx::Rect(x, y, kShelfButtonSize, kShelfButtonSize));
x = shelf()->PrimaryAxisValue(x + kShelfButtonSize + button_spacing, x);
y = shelf()->PrimaryAxisValue(y, y + kShelfButtonSize + button_spacing);
if (i == separator_index) {
// Place the separator halfway between the two icons it separates,
// vertically centered.
int half_space = button_spacing / 2;
int secondary_offset =
(ShelfConstants::shelf_size() - kSeparatorSize) / 2;
x -= shelf()->PrimaryAxisValue(half_space, 0);
y -= shelf()->PrimaryAxisValue(0, half_space);
separator_->SetBounds(
x + shelf()->PrimaryAxisValue(0, secondary_offset),
y + shelf()->PrimaryAxisValue(secondary_offset, 0),
shelf()->PrimaryAxisValue(kSeparatorThickness, kSeparatorSize),
shelf()->PrimaryAxisValue(kSeparatorSize, kSeparatorThickness));
x += shelf()->PrimaryAxisValue(half_space, 0);
y += shelf()->PrimaryAxisValue(0, half_space);
}
}
if (is_overflow_mode()) {
UpdateAllButtonsVisibilityInOverflowMode();
return;
}
// In the main shelf, the first visible index is either the first app, or -1
// if there are no apps.
set_first_visible_index(view_model()->view_size() == 0 ? -1 : 0);
for (int i = 0; i < view_model()->view_size(); ++i) {
// To receive drag event continuously from |drag_view_| during the dragging
// off from the shelf, don't make |drag_view_| invisible. It will be
// eventually invisible and removed from the |view_model_| by
// FinalizeRipOffDrag().
if (dragged_off_shelf() && view_model()->view_at(i) == drag_view())
continue;
// If the virtual keyboard is visible, do not show any apps.
view_model()->view_at(i)->SetVisible(i <= last_visible_index() &&
!virtual_keyboard_visible);
}
overflow_button()->SetVisible(app_centering_strategy.overflow);
if (app_centering_strategy.overflow) {
if (overflow_bubble() && overflow_bubble()->IsShowing())
UpdateOverflowRange(overflow_bubble()->bubble_view()->shelf_view());
} else {
if (overflow_bubble())
overflow_bubble()->Hide();
}
}
void DefaultShelfView::LayoutAppListAndBackButtonHighlight() {
// Don't show anything if this is the overflow menu.
if (is_overflow_mode()) {
back_and_app_list_background_->SetVisible(false);
return;
}
const int button_spacing = ShelfConstants::button_spacing();
// "Secondary" as in "orthogonal to the shelf primary axis".
const int control_secondary_padding =
(ShelfConstants::shelf_size() - kShelfControlSize) / 2;
const int back_and_app_list_background_size =
kShelfControlSize +
(IsTabletModeEnabled() ? kShelfControlSize + button_spacing : 0);
back_and_app_list_background_->SetBounds(
shelf()->PrimaryAxisValue(button_spacing, control_secondary_padding),
shelf()->PrimaryAxisValue(control_secondary_padding, button_spacing),
shelf()->PrimaryAxisValue(back_and_app_list_background_size,
kShelfControlSize),
shelf()->PrimaryAxisValue(kShelfControlSize,
back_and_app_list_background_size));
}
std::unique_ptr<BackButton> DefaultShelfView::CreateBackButton() {
return std::make_unique<BackButton>(this);
}
std::unique_ptr<AppListButton> DefaultShelfView::CreateHomeButton() {
return std::make_unique<AppListButton>(this, shelf());
}
int DefaultShelfView::GetAvailableSpaceForAppIcons() const {
// Subtract space already allocated to the app list button, and the back
// button if applicable.
return shelf()->PrimaryAxisValue(width(), height()) - kShelfButtonSpacing -
(IsTabletModeEnabled() ? 2 : 1) * kShelfControlSize -
2 * kAppIconGroupMargin;
}
int DefaultShelfView::GetSeparatorIndex() const {
for (int i = 0; i < model()->item_count() - 1; ++i) {
if (IsItemPinned(model()->items()[i]) &&
model()->items()[i + 1].type == TYPE_APP) {
return i;
}
}
return -1;
}
DefaultShelfView::AppCenteringStrategy
DefaultShelfView::CalculateAppCenteringStrategy() {
// There are two possibilities. Either all the apps fit when centered
// on the whole screen width, in which case we do that. Or, when space
// becomes a little tight (which happens especially when the status area
// is wider because of extra panels), we center apps on the available space.
AppCenteringStrategy strategy;
// This is only relevant for the main shelf.
if (is_overflow_mode())
return strategy;
const int total_available_size = shelf()->PrimaryAxisValue(width(), height());
StatusAreaWidget* status_widget = shelf_widget()->status_area_widget();
const int status_widget_size =
status_widget ? shelf()->PrimaryAxisValue(
status_widget->GetWindowBoundsInScreen().width(),
status_widget->GetWindowBoundsInScreen().height())
: 0;
const int screen_size = total_available_size + status_widget_size;
// An easy way to check whether the apps fit at the exact center of the
// screen is to imagine that we have another status widget on the other
// side (the status widget is always bigger than the app list button plus
// the back button if applicable) and see if the apps can fit in the middle.
int available_space_for_screen_centering =
screen_size - 2 * (status_widget_size + kAppIconGroupMargin);
if (GetSizeOfAppIcons(view_model()->view_size(), false) <
available_space_for_screen_centering) {
// Everything fits in the center of the screen.
set_last_visible_index(view_model()->view_size() - 1);
strategy.center_on_screen = true;
return strategy;
}
const int available_size_for_app_icons = GetAvailableSpaceForAppIcons();
set_last_visible_index(-1);
// We know that replacing the last app that fits with the overflow button
// will not change the outcome, so we ignore that case for now.
while (last_visible_index() < view_model()->view_size() - 1) {
if (GetSizeOfAppIcons(last_visible_index() + 2, false) <=
available_size_for_app_icons) {
set_last_visible_index(last_visible_index() + 1);
} else {
strategy.overflow = true;
// Make space for the overflow button by showing one fewer app icon.
set_last_visible_index(last_visible_index() - 1);
break;
}
}
return strategy;
}
void DefaultShelfView::UpdateAllButtonsVisibilityInOverflowMode() {
// The overflow button is not shown in overflow mode.
overflow_button()->SetVisible(false);
DCHECK_LT(last_visible_index(), view_model()->view_size());
for (int i = 0; i < view_model()->view_size(); ++i) {
bool visible = i >= first_visible_index() && i <= last_visible_index();
// To track the dragging of |drag_view_| continuously, its visibility
// should be always true regardless of its position.
if (dragged_to_another_shelf() && view_model()->view_at(i) == drag_view())
view_model()->view_at(i)->SetVisible(true);
else
view_model()->view_at(i)->SetVisible(visible);
}
}
} // namespace ash