1 // Copyright (c) 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/public/cpp/shelf_config.h"
6
7 #include "ash/accessibility/accessibility_controller_impl.h"
8 #include "ash/accessibility/accessibility_observer.h"
9 #include "ash/app_list/app_list_controller_impl.h"
10 #include "ash/public/cpp/ash_features.h"
11 #include "ash/session/session_controller_impl.h"
12 #include "ash/shell.h"
13 #include "ash/style/ash_color_provider.h"
14 #include "ash/system/model/system_tray_model.h"
15 #include "ash/wallpaper/wallpaper_controller_impl.h"
16 #include "ash/wm/overview/overview_controller.h"
17 #include "ash/wm/tablet_mode/tablet_mode_controller.h"
18 #include "base/metrics/histogram_functions.h"
19 #include "base/scoped_observer.h"
20 #include "ui/gfx/color_analysis.h"
21 #include "ui/gfx/color_palette.h"
22 #include "ui/gfx/color_utils.h"
23
24 namespace ash {
25
26 namespace {
27
28 // Used in as a value in histogram to record the reason shelf navigation buttons
29 // are shown in tablet mode.
30 // The values assigned to enum items should not be changed/reassigned.
31 constexpr int kControlButtonsShownForShelfNavigationButtonsSetting = 1;
32 constexpr int kControlButtonsShownForSpokenFeedback = 1 << 1;
33 constexpr int kControlButtonsShownForSwitchAccess = 1 << 2;
34 constexpr int kControlButtonsShownForAutoclick = 1 << 3;
35 constexpr int kControlButtonsShownReasonCount = 1 << 4;
36
37 // When any edge of the primary display is less than or equal to this threshold,
38 // dense shelf will be active.
39 constexpr int kDenseShelfScreenSizeThreshold = 600;
40
41 // Drags on the shelf that are greater than this number times the shelf size
42 // will trigger shelf visibility changes.
43 constexpr float kDragHideRatioThreshold = 0.4f;
44
45 // Records the histogram value tracking the reason shelf control buttons are
46 // shown in tablet mode.
RecordReasonForShowingShelfControls()47 void RecordReasonForShowingShelfControls() {
48 AccessibilityControllerImpl* accessibility_controller =
49 Shell::Get()->accessibility_controller();
50 int buttons_shown_reason_mask = 0;
51
52 if (accessibility_controller
53 ->tablet_mode_shelf_navigation_buttons_enabled()) {
54 buttons_shown_reason_mask |=
55 kControlButtonsShownForShelfNavigationButtonsSetting;
56 }
57
58 if (accessibility_controller->spoken_feedback().enabled())
59 buttons_shown_reason_mask |= kControlButtonsShownForSpokenFeedback;
60
61 if (accessibility_controller->switch_access().enabled())
62 buttons_shown_reason_mask |= kControlButtonsShownForSwitchAccess;
63
64 if (accessibility_controller->autoclick().enabled())
65 buttons_shown_reason_mask |= kControlButtonsShownForAutoclick;
66
67 base::UmaHistogramExactLinear(
68 "Ash.Shelf.NavigationButtonsInTabletMode.ReasonShown",
69 buttons_shown_reason_mask, kControlButtonsShownReasonCount);
70 }
71
72 } // namespace
73
74 class ShelfConfig::ShelfAccessibilityObserver : public AccessibilityObserver {
75 public:
ShelfAccessibilityObserver(const base::RepeatingClosure & accessibility_state_changed_callback)76 ShelfAccessibilityObserver(
77 const base::RepeatingClosure& accessibility_state_changed_callback)
78 : accessibility_state_changed_callback_(
79 accessibility_state_changed_callback) {
80 observer_.Add(Shell::Get()->accessibility_controller());
81 }
82
83 ShelfAccessibilityObserver(const ShelfAccessibilityObserver& other) = delete;
84 ShelfAccessibilityObserver& operator=(
85 const ShelfAccessibilityObserver& other) = delete;
86
87 ~ShelfAccessibilityObserver() override = default;
88
89 // AccessibilityObserver:
OnAccessibilityStatusChanged()90 void OnAccessibilityStatusChanged() override {
91 accessibility_state_changed_callback_.Run();
92 }
OnAccessibilityControllerShutdown()93 void OnAccessibilityControllerShutdown() override { observer_.RemoveAll(); }
94
95 private:
96 base::RepeatingClosure accessibility_state_changed_callback_;
97
98 ScopedObserver<AccessibilityControllerImpl, AccessibilityObserver> observer_{
99 this};
100 };
101
ShelfConfig()102 ShelfConfig::ShelfConfig()
103 : use_in_app_shelf_in_overview_(false),
104 overview_mode_(false),
105 in_tablet_mode_(false),
106 is_dense_(false),
107 shelf_controls_shown_(true),
108 is_virtual_keyboard_shown_(false),
109 is_app_list_visible_(false),
110 shelf_button_icon_size_(44),
111 shelf_button_icon_size_median_(40),
112 shelf_button_icon_size_dense_(36),
113 shelf_button_size_(56),
114 shelf_button_size_median_(52),
115 shelf_button_size_dense_(48),
116 shelf_button_spacing_(8),
117 shelf_status_area_hit_region_padding_(4),
118 shelf_status_area_hit_region_padding_dense_(2),
119 app_icon_group_margin_tablet_(16),
120 app_icon_group_margin_clamshell_(12),
121 shelf_focus_border_color_(gfx::kGoogleBlue300),
122 workspace_area_visible_inset_(2),
123 workspace_area_auto_hide_inset_(5),
124 hidden_shelf_in_screen_portion_(3),
125 status_indicator_offset_from_shelf_edge_(1),
126 scrollable_shelf_ripple_padding_(2),
127 shelf_tooltip_preview_height_(128),
128 shelf_tooltip_preview_max_width_(192),
129 shelf_tooltip_preview_max_ratio_(1.5), // = 3/2
130 shelf_tooltip_preview_min_ratio_(0.666), // = 2/3
131 shelf_blur_radius_(30),
132 mousewheel_scroll_offset_threshold_(20),
133 in_app_control_button_height_inset_(4),
134 app_icon_end_padding_(4) {
135 accessibility_observer_ = std::make_unique<ShelfAccessibilityObserver>(
136 base::BindRepeating(&ShelfConfig::UpdateConfigForAccessibilityState,
137 base::Unretained(this)));
138 }
139
140 ShelfConfig::~ShelfConfig() = default;
141
142 // static
Get()143 ShelfConfig* ShelfConfig::Get() {
144 return Shell::Get()->shelf_config();
145 }
146
AddObserver(Observer * observer)147 void ShelfConfig::AddObserver(Observer* observer) {
148 observers_.AddObserver(observer);
149 }
150
RemoveObserver(Observer * observer)151 void ShelfConfig::RemoveObserver(Observer* observer) {
152 observers_.RemoveObserver(observer);
153 }
154
Init()155 void ShelfConfig::Init() {
156 Shell* const shell = Shell::Get();
157
158 shell->app_list_controller()->AddObserver(this);
159 display::Screen::GetScreen()->AddObserver(this);
160 shell->system_tray_model()->virtual_keyboard()->AddObserver(this);
161 shell->overview_controller()->AddObserver(this);
162
163 shell->tablet_mode_controller()->AddObserver(this);
164 in_tablet_mode_ = shell->IsInTabletMode();
165 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
166 }
167
Shutdown()168 void ShelfConfig::Shutdown() {
169 Shell* const shell = Shell::Get();
170 shell->tablet_mode_controller()->RemoveObserver(this);
171
172 shell->overview_controller()->RemoveObserver(this);
173 shell->system_tray_model()->virtual_keyboard()->RemoveObserver(this);
174 display::Screen::GetScreen()->RemoveObserver(this);
175 shell->app_list_controller()->RemoveObserver(this);
176 }
177
OnOverviewModeWillStart()178 void ShelfConfig::OnOverviewModeWillStart() {
179 DCHECK(!overview_mode_);
180 use_in_app_shelf_in_overview_ = is_in_app();
181 overview_mode_ = true;
182 }
183
OnOverviewModeEnding(OverviewSession * overview_session)184 void ShelfConfig::OnOverviewModeEnding(OverviewSession* overview_session) {
185 overview_mode_ = false;
186 use_in_app_shelf_in_overview_ = false;
187 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
188 }
189
OnTabletModeStarting()190 void ShelfConfig::OnTabletModeStarting() {
191 // Update the shelf config at the "starting" stage of the tablet mode
192 // transition, so that the shelf bounds are set and remains stable during the
193 // transition animation. Otherwise, updating the shelf bounds during the
194 // animation will lead to work-area bounds changes which lead to many
195 // re-layouts, hurting the animation's smoothness. https://crbug.com/1044316.
196 DCHECK(!in_tablet_mode_);
197 in_tablet_mode_ = true;
198
199 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/true);
200 }
201
OnTabletModeEnding()202 void ShelfConfig::OnTabletModeEnding() {
203 // Many events can lead to UpdateConfig being called as a result of
204 // OnTabletModeEnded(), therefore we need to listen to the "ending" stage
205 // rather than the "ended", so |in_tablet_mode_| gets updated correctly, and
206 // the shelf bounds are stabilized early so as not to have multiple
207 // unnecessary work-area bounds changes.
208 in_tablet_mode_ = false;
209
210 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/true);
211 }
212
OnDisplayMetricsChanged(const display::Display & display,uint32_t changed_metrics)213 void ShelfConfig::OnDisplayMetricsChanged(const display::Display& display,
214 uint32_t changed_metrics) {
215 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
216 }
217
OnVirtualKeyboardVisibilityChanged()218 void ShelfConfig::OnVirtualKeyboardVisibilityChanged() {
219 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
220 }
221
OnAppListVisibilityWillChange(bool shown,int64_t display_id)222 void ShelfConfig::OnAppListVisibilityWillChange(bool shown,
223 int64_t display_id) {
224 // Let's check that the app visibility mechanism isn't mis-firing, which
225 // would lead to a lot of extraneous relayout work.
226 DCHECK_NE(is_app_list_visible_, shown);
227
228 UpdateConfig(/*new_is_app_list_visible=*/shown,
229 /*tablet_mode_changed=*/false);
230 }
231
ShelfControlsForcedShownForAccessibility() const232 bool ShelfConfig::ShelfControlsForcedShownForAccessibility() const {
233 auto* accessibility_controller = Shell::Get()->accessibility_controller();
234 return accessibility_controller->spoken_feedback().enabled() ||
235 accessibility_controller->autoclick().enabled() ||
236 accessibility_controller->switch_access().enabled() ||
237 accessibility_controller
238 ->tablet_mode_shelf_navigation_buttons_enabled();
239 }
240
GetShelfButtonSize(HotseatDensity density) const241 int ShelfConfig::GetShelfButtonSize(HotseatDensity density) const {
242 if (is_dense_)
243 return shelf_button_size_dense_;
244
245 switch (density) {
246 case HotseatDensity::kNormal:
247 return shelf_button_size_;
248 case HotseatDensity::kSemiDense:
249 return shelf_button_size_median_;
250 case HotseatDensity::kDense:
251 return shelf_button_size_dense_;
252 }
253 }
254
GetShelfButtonIconSize(HotseatDensity density) const255 int ShelfConfig::GetShelfButtonIconSize(HotseatDensity density) const {
256 if (is_dense_)
257 return shelf_button_icon_size_dense_;
258
259 switch (density) {
260 case HotseatDensity::kNormal:
261 return shelf_button_icon_size_;
262 case HotseatDensity::kSemiDense:
263 return shelf_button_icon_size_median_;
264 case HotseatDensity::kDense:
265 return shelf_button_icon_size_dense_;
266 }
267 }
268
GetHotseatSize(HotseatDensity density) const269 int ShelfConfig::GetHotseatSize(HotseatDensity density) const {
270 if (!in_tablet_mode_)
271 return shelf_size();
272
273 return GetShelfButtonSize(density);
274 }
275
shelf_size() const276 int ShelfConfig::shelf_size() const {
277 return GetShelfSize(false /*ignore_in_app_state*/);
278 }
279
in_app_shelf_size() const280 int ShelfConfig::in_app_shelf_size() const {
281 return is_dense_ ? 36 : 40;
282 }
283
system_shelf_size() const284 int ShelfConfig::system_shelf_size() const {
285 return GetShelfSize(true /*ignore_in_app_state*/);
286 }
287
shelf_drag_handle_centering_size() const288 int ShelfConfig::shelf_drag_handle_centering_size() const {
289 const session_manager::SessionState session_state =
290 Shell::Get()->session_controller()->GetSessionState();
291 return session_state == session_manager::SessionState::ACTIVE
292 ? in_app_shelf_size()
293 : 28;
294 }
295
hotseat_bottom_padding() const296 int ShelfConfig::hotseat_bottom_padding() const {
297 return 8;
298 }
299
button_spacing() const300 int ShelfConfig::button_spacing() const {
301 return shelf_button_spacing_;
302 }
303
control_size() const304 int ShelfConfig::control_size() const {
305 if (!in_tablet_mode_)
306 return 36;
307
308 return is_dense_ ? 36 : 40;
309 }
310
control_border_radius() const311 int ShelfConfig::control_border_radius() const {
312 return (is_in_app() && in_tablet_mode_)
313 ? control_size() / 2 - in_app_control_button_height_inset_
314 : control_size() / 2;
315 }
316
control_button_edge_spacing(bool is_primary_axis_edge) const317 int ShelfConfig::control_button_edge_spacing(bool is_primary_axis_edge) const {
318 if (is_primary_axis_edge)
319 return in_tablet_mode_ ? 8 : 6;
320
321 return (shelf_size() - control_size()) / 2;
322 }
323
hotseat_background_animation_duration() const324 base::TimeDelta ShelfConfig::hotseat_background_animation_duration() const {
325 // This matches the duration of the maximize/minimize animation.
326 return base::TimeDelta::FromMilliseconds(300);
327 }
328
shelf_animation_duration() const329 base::TimeDelta ShelfConfig::shelf_animation_duration() const {
330 return hotseat_background_animation_duration();
331 }
332
status_area_hit_region_padding() const333 int ShelfConfig::status_area_hit_region_padding() const {
334 return is_dense_ ? shelf_status_area_hit_region_padding_dense_
335 : shelf_status_area_hit_region_padding_;
336 }
337
is_in_app() const338 bool ShelfConfig::is_in_app() const {
339 Shell* shell = Shell::Get();
340 const auto* session = shell->session_controller();
341 if (!session ||
342 session->GetSessionState() != session_manager::SessionState::ACTIVE) {
343 return false;
344 }
345 if (is_virtual_keyboard_shown_)
346 return true;
347 if (is_app_list_visible_)
348 return false;
349 if (overview_mode_)
350 return use_in_app_shelf_in_overview_;
351 return true;
352 }
353
drag_hide_ratio_threshold() const354 float ShelfConfig::drag_hide_ratio_threshold() const {
355 return kDragHideRatioThreshold;
356 }
357
UpdateConfig(bool new_is_app_list_visible,bool tablet_mode_changed)358 void ShelfConfig::UpdateConfig(bool new_is_app_list_visible,
359 bool tablet_mode_changed) {
360 const gfx::Rect screen_size =
361 display::Screen::GetScreen()->GetPrimaryDisplay().bounds();
362
363 const bool new_is_dense =
364 !in_tablet_mode_ ||
365 (screen_size.width() <= kDenseShelfScreenSizeThreshold ||
366 screen_size.height() <= kDenseShelfScreenSizeThreshold);
367
368 const bool can_hide_shelf_controls =
369 in_tablet_mode_ && features::IsHideShelfControlsInTabletModeEnabled();
370 const bool new_shelf_controls_shown =
371 !can_hide_shelf_controls || ShelfControlsForcedShownForAccessibility();
372 // Record reason to show shelf control buttons only if tablet mode changes, or
373 // if the buttons visibility state changes
374 if (can_hide_shelf_controls && new_shelf_controls_shown &&
375 (tablet_mode_changed || !shelf_controls_shown_)) {
376 RecordReasonForShowingShelfControls();
377 }
378
379 // TODO(https://crbug.com/1058205): Test this behavior.
380 // If the virtual keyboard is shown, the back button and in-app shelf should
381 // be shown so users can exit the keyboard. SystemTrayModel may be null in
382 // tests.
383 const bool new_is_virtual_keyboard_shown =
384 Shell::Get()->system_tray_model()
385 ? Shell::Get()->system_tray_model()->virtual_keyboard()->visible()
386 : false;
387
388 if (!tablet_mode_changed && is_dense_ == new_is_dense &&
389 shelf_controls_shown_ == new_shelf_controls_shown &&
390 is_virtual_keyboard_shown_ == new_is_virtual_keyboard_shown &&
391 is_app_list_visible_ == new_is_app_list_visible) {
392 return;
393 }
394
395 is_dense_ = new_is_dense;
396 shelf_controls_shown_ = new_shelf_controls_shown;
397 is_virtual_keyboard_shown_ = new_is_virtual_keyboard_shown;
398 is_app_list_visible_ = new_is_app_list_visible;
399
400 OnShelfConfigUpdated();
401 }
402
GetShelfSize(bool ignore_in_app_state) const403 int ShelfConfig::GetShelfSize(bool ignore_in_app_state) const {
404 // In clamshell mode, the shelf always has the same size.
405 if (!in_tablet_mode_)
406 return 48;
407
408 if (!ignore_in_app_state && is_in_app())
409 return in_app_shelf_size();
410
411 return is_dense_ ? 48 : 56;
412 }
413
GetShelfControlButtonColor() const414 SkColor ShelfConfig::GetShelfControlButtonColor() const {
415 const session_manager::SessionState session_state =
416 Shell::Get()->session_controller()->GetSessionState();
417
418 if (in_tablet_mode_ &&
419 session_state == session_manager::SessionState::ACTIVE) {
420 return is_in_app() ? SK_ColorTRANSPARENT : GetDefaultShelfColor();
421 } else if (session_state == session_manager::SessionState::OOBE) {
422 return SkColorSetA(SK_ColorBLACK, 16); // 6% opacity
423 }
424 return AshColorProvider::Get()->GetControlsLayerColor(
425 AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive);
426 }
427
GetShelfWithAppListColor() const428 SkColor ShelfConfig::GetShelfWithAppListColor() const {
429 return SkColorSetA(SK_ColorBLACK, 20); // 8% opacity
430 }
431
GetMaximizedShelfColor() const432 SkColor ShelfConfig::GetMaximizedShelfColor() const {
433 return SkColorSetA(GetDefaultShelfColor(), 0xFF); // 100% opacity
434 }
435
GetShelfBaseLayerType() const436 AshColorProvider::BaseLayerType ShelfConfig::GetShelfBaseLayerType() const {
437 if (!in_tablet_mode_)
438 return AshColorProvider::BaseLayerType::kTransparent80;
439
440 if (!is_in_app())
441 return AshColorProvider::BaseLayerType::kTransparent60;
442
443 return AshColorProvider::Get()->IsDarkModeEnabled()
444 ? AshColorProvider::BaseLayerType::kTransparent90
445 : AshColorProvider::BaseLayerType::kOpaque;
446 }
447
GetDefaultShelfColor() const448 SkColor ShelfConfig::GetDefaultShelfColor() const {
449 if (!features::IsBackgroundBlurEnabled()) {
450 return AshColorProvider::Get()->GetBaseLayerColor(
451 AshColorProvider::BaseLayerType::kTransparent90);
452 }
453
454 AshColorProvider::BaseLayerType layer_type = GetShelfBaseLayerType();
455
456 return AshColorProvider::Get()->GetBaseLayerColor(layer_type);
457 }
458
GetShelfControlButtonBlurRadius() const459 int ShelfConfig::GetShelfControlButtonBlurRadius() const {
460 if (features::IsBackgroundBlurEnabled() && in_tablet_mode_ && !is_in_app())
461 return shelf_blur_radius_;
462 return 0;
463 }
464
GetAppIconEndPadding() const465 int ShelfConfig::GetAppIconEndPadding() const {
466 return app_icon_end_padding_;
467 }
468
GetAppIconGroupMargin() const469 int ShelfConfig::GetAppIconGroupMargin() const {
470 return in_tablet_mode_ ? app_icon_group_margin_tablet_
471 : app_icon_group_margin_clamshell_;
472 }
473
DimAnimationDuration() const474 base::TimeDelta ShelfConfig::DimAnimationDuration() const {
475 return base::TimeDelta::FromMilliseconds(1000);
476 }
477
DimAnimationTween() const478 gfx::Tween::Type ShelfConfig::DimAnimationTween() const {
479 return gfx::Tween::LINEAR;
480 }
481
DragHandleSize() const482 gfx::Size ShelfConfig::DragHandleSize() const {
483 const session_manager::SessionState session_state =
484 Shell::Get()->session_controller()->GetSessionState();
485 return session_state == session_manager::SessionState::ACTIVE
486 ? gfx::Size(80, 4)
487 : gfx::Size(120, 4);
488 }
489
UpdateConfigForAccessibilityState()490 void ShelfConfig::UpdateConfigForAccessibilityState() {
491 UpdateConfig(is_app_list_visible_, /*tablet_mode_changed=*/false);
492 }
493
OnShelfConfigUpdated()494 void ShelfConfig::OnShelfConfigUpdated() {
495 for (auto& observer : observers_)
496 observer.OnShelfConfigUpdated();
497 }
498
499 } // namespace ash
500