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