multitask_menu_nudge_controller.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. // Copyright 2022 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. #include "ash/wm/multitask_menu_nudge_controller.h"
  5. #include "ash/session/session_controller_impl.h"
  6. #include "ash/shell.h"
  7. #include "ash/strings/grit/ash_strings.h"
  8. #include "ash/style/system_toast_style.h"
  9. #include "ash/wm/tablet_mode/tablet_mode_controller.h"
  10. #include "ash/wm/window_util.h"
  11. #include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
  12. #include "chromeos/ui/frame/frame_header.h"
  13. #include "chromeos/ui/wm/features.h"
  14. #include "components/prefs/pref_registry_simple.h"
  15. #include "components/prefs/pref_service.h"
  16. #include "components/user_manager/user_type.h"
  17. #include "ui/aura/client/aura_constants.h"
  18. #include "ui/base/l10n/l10n_util.h"
  19. #include "ui/compositor/layer.h"
  20. #include "ui/gfx/geometry/transform_util.h"
  21. #include "ui/views/animation/animation_builder.h"
  22. #include "ui/views/widget/widget.h"
  23. #include "ui/views/window/frame_caption_button.h"
  24. #include "ui/wm/core/coordinate_conversion.h"
  25. namespace ash {
  26. constexpr base::TimeDelta kNudgeDismissTimeout = base::Seconds(6);
  27. // The name of an integer pref that counts the number of times we have shown the
  28. // multitask menu educational nudge.
  29. constexpr char kShownCountPrefName[] =
  30. "ash.wm_nudge.multitask_menu_nudge_count";
  31. // The name of a time pref that stores the time we last showed the multitask
  32. // menu education nudge.
  33. constexpr char kLastShownPrefName[] =
  34. "ash.wm_nudge.multitask_menu_nudge_last_shown";
  35. // The nudge will not be shown if it already been shown 3 times, or if 24 hours
  36. // have not yet passed since it was last shown.
  37. constexpr int kNudgeMaxShownCount = 3;
  38. constexpr base::TimeDelta kNudgeTimeBetweenShown = base::Hours(24);
  39. constexpr base::TimeDelta kFadeDuration = base::Milliseconds(50);
  40. constexpr int kNudgeDistanceFromAnchor = 8;
  41. // The max pulse size will be three times the size of the maximize/restore
  42. // button.
  43. constexpr float kPulseSizeMultiplier = 3.0f;
  44. constexpr base::TimeDelta kPulseDuration = base::Seconds(2);
  45. constexpr int kPulses = 3;
  46. // Clock that can be overridden for testing.
  47. base::Clock* g_clock_override = nullptr;
  48. base::Time GetTime() {
  49. return g_clock_override ? g_clock_override->Now() : base::Time::Now();
  50. }
  51. namespace {
  52. std::unique_ptr<views::Widget> CreateWidget(aura::Window* parent) {
  53. views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
  54. params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  55. params.name = "MultitaskNudgeWidget";
  56. params.accept_events = false;
  57. params.parent = parent;
  58. auto widget = std::make_unique<views::Widget>(std::move(params));
  59. widget->SetContentsView(std::make_unique<SystemToastStyle>(
  60. base::DoNothing(),
  61. l10n_util::GetStringUTF16(IDS_MULTITASK_MENU_NUDGE_TEXT),
  62. /*dismiss_text=*/u"", /*is_managed=*/false));
  63. return widget;
  64. }
  65. } // namespace
  66. MultitaskMenuNudgeController::MultitaskMenuNudgeController() = default;
  67. MultitaskMenuNudgeController::~MultitaskMenuNudgeController() {
  68. DismissNudgeInternal();
  69. }
  70. // static
  71. void MultitaskMenuNudgeController::RegisterProfilePrefs(
  72. PrefRegistrySimple* registry) {
  73. registry->RegisterIntegerPref(kShownCountPrefName, 0);
  74. registry->RegisterTimePref(kLastShownPrefName, base::Time());
  75. }
  76. void MultitaskMenuNudgeController::MaybeShowNudge(aura::Window* window) {
  77. if (!chromeos::wm::features::IsFloatWindowEnabled())
  78. return;
  79. // Nudge is already being shown, possibly on a different window.
  80. if (nudge_widget_)
  81. return;
  82. // TODO(sammiequon): Tablet mode nudge will be different as there is no title
  83. // bar with resize button.
  84. if (Shell::Get()->tablet_mode_controller()->InTabletMode())
  85. return;
  86. // Only regular users can see the nudge.
  87. auto* session_controller = Shell::Get()->session_controller();
  88. const absl::optional<user_manager::UserType> user_type =
  89. session_controller->GetUserType();
  90. if (!user_type || *user_type != user_manager::USER_TYPE_REGULAR)
  91. return;
  92. // TODO(sammiequon): Once the multitask menu has been opened once, we don't
  93. // need to show the nudge anymore.
  94. // Nudge has already been shown three times. No need to educate anymore.
  95. auto* pref_service = session_controller->GetActivePrefService();
  96. DCHECK(pref_service);
  97. const int shown_count = pref_service->GetInteger(kShownCountPrefName);
  98. if (shown_count >= kNudgeMaxShownCount)
  99. return;
  100. // Nudge has been shown within the last 24 hours already.
  101. const base::Time time = GetTime();
  102. if (time - pref_service->GetTime(kLastShownPrefName) <
  103. kNudgeTimeBetweenShown) {
  104. return;
  105. }
  106. auto* frame_header = chromeos::FrameHeader::Get(
  107. views::Widget::GetWidgetForNativeWindow(window));
  108. // Frame might not be in tests.
  109. if (!frame_header)
  110. return;
  111. // The anchor is the button on the header that serves as the maximize or
  112. // restore button (depending on the window state).
  113. auto* anchor_view = frame_header->caption_button_container()->size_button();
  114. DCHECK(anchor_view);
  115. // If either the window or anchor is not visible, do not show the nudge.
  116. if (!window->IsVisible() || !anchor_view->IsDrawn())
  117. return;
  118. window_ = window;
  119. window_observation_.Observe(window_);
  120. anchor_view_ = anchor_view;
  121. nudge_widget_ = CreateWidget(window_->parent());
  122. nudge_widget_->Show();
  123. // Create the layer which pulses on the maximize/restore button.
  124. pulse_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
  125. pulse_layer_->SetColor(SK_ColorGRAY);
  126. window_->parent()->layer()->Add(pulse_layer_.get());
  127. UpdateWidgetAndPulse();
  128. DCHECK(nudge_widget_);
  129. // Fade the educational nudge in.
  130. ui::Layer* layer = nudge_widget_->GetLayer();
  131. layer->SetOpacity(0.0f);
  132. views::AnimationBuilder()
  133. .SetPreemptionStrategy(
  134. ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
  135. .Once()
  136. .SetDuration(kFadeDuration)
  137. .SetOpacity(layer, 1.0f, gfx::Tween::LINEAR);
  138. PerformPulseAnimation(/*pulse_count=*/0);
  139. // Update the preferences.
  140. pref_service->SetInteger(kShownCountPrefName, shown_count + 1);
  141. pref_service->SetTime(kLastShownPrefName, time);
  142. nudge_dismiss_timer_.Start(
  143. FROM_HERE, kNudgeDismissTimeout, this,
  144. &MultitaskMenuNudgeController::OnDismissTimerEnded);
  145. }
  146. void MultitaskMenuNudgeController::OnWindowParentChanged(aura::Window* window,
  147. aura::Window* parent) {
  148. if (!parent)
  149. return;
  150. DCHECK_EQ(window_, window);
  151. UpdateWidgetAndPulse();
  152. }
  153. void MultitaskMenuNudgeController::OnWindowVisibilityChanged(
  154. aura::Window* window,
  155. bool visible) {
  156. if (window_ == window)
  157. UpdateWidgetAndPulse();
  158. }
  159. void MultitaskMenuNudgeController::OnWindowBoundsChanged(
  160. aura::Window* window,
  161. const gfx::Rect& old_bounds,
  162. const gfx::Rect& new_bounds,
  163. ui::PropertyChangeReason reason) {
  164. DCHECK_EQ(window_, window);
  165. UpdateWidgetAndPulse();
  166. }
  167. void MultitaskMenuNudgeController::OnWindowTargetTransformChanging(
  168. aura::Window* window,
  169. const gfx::Transform& new_transform) {
  170. DCHECK_EQ(window_, window);
  171. DismissNudgeInternal();
  172. }
  173. void MultitaskMenuNudgeController::OnWindowStackingChanged(
  174. aura::Window* window) {
  175. DCHECK_EQ(window_, window);
  176. DCHECK(nudge_widget_);
  177. // Ensure the `nudge_widget_` is always above `window_`. We dont worry about
  178. // the pulse layer since it is not a window, and won't get stacked on top of
  179. // during window activation for example.
  180. window_->parent()->StackChildAbove(nudge_widget_->GetNativeWindow(), window);
  181. }
  182. void MultitaskMenuNudgeController::OnWindowDestroying(aura::Window* window) {
  183. DCHECK_EQ(window_, window);
  184. DismissNudgeInternal();
  185. }
  186. // static
  187. void MultitaskMenuNudgeController::SetOverrideClockForTesting(
  188. base::Clock* test_clock) {
  189. g_clock_override = test_clock;
  190. }
  191. void MultitaskMenuNudgeController::OnDismissTimerEnded() {
  192. if (!nudge_widget_)
  193. return;
  194. ui::Layer* layer = nudge_widget_->GetLayer();
  195. layer->SetOpacity(1.0f);
  196. views::AnimationBuilder()
  197. .SetPreemptionStrategy(
  198. ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
  199. .OnEnded(
  200. base::BindOnce(&MultitaskMenuNudgeController::DismissNudgeInternal,
  201. base::Unretained(this)))
  202. .Once()
  203. .SetDuration(kFadeDuration)
  204. .SetOpacity(layer, 0.0f, gfx::Tween::LINEAR);
  205. }
  206. void MultitaskMenuNudgeController::DismissNudgeInternal() {
  207. nudge_dismiss_timer_.Stop();
  208. window_observation_.Reset();
  209. window_ = nullptr;
  210. anchor_view_ = nullptr;
  211. pulse_layer_.reset();
  212. if (nudge_widget_) {
  213. nudge_widget_->GetLayer()->GetAnimator()->AbortAllAnimations();
  214. nudge_widget_->CloseNow();
  215. }
  216. }
  217. void MultitaskMenuNudgeController::UpdateWidgetAndPulse() {
  218. DCHECK(nudge_widget_);
  219. DCHECK(pulse_layer_);
  220. DCHECK(window_);
  221. DCHECK(anchor_view_);
  222. // Dismiss the nudge if either of these are not visible, otherwise it will be
  223. // floating.
  224. if (!window_->IsVisible() || !anchor_view_->IsDrawn()) {
  225. DismissNudgeInternal();
  226. return;
  227. }
  228. const gfx::Rect anchor_bounds_in_screen = anchor_view_->GetBoundsInScreen();
  229. // Reparent the nudge and pulse if necessary.
  230. aura::Window* new_root =
  231. window_util::GetRootWindowMatching(anchor_bounds_in_screen);
  232. aura::Window* nudge_window = nudge_widget_->GetNativeWindow();
  233. if (new_root != nudge_window->GetRootWindow()) {
  234. const int parent_id = nudge_window->parent()->GetId();
  235. aura::Window* new_parent = new_root->GetChildById(parent_id);
  236. new_parent->AddChild(nudge_window);
  237. new_parent->layer()->Add(pulse_layer_.get());
  238. }
  239. // The nudge is placed right below the anchor.
  240. // TODO(crbug.com/1329233): Determine what to do if the nudge is offscreen.
  241. const gfx::Size size = nudge_widget_->GetContentsView()->GetPreferredSize();
  242. const gfx::Rect bounds_in_screen(
  243. anchor_bounds_in_screen.CenterPoint().x() - size.width() / 2,
  244. anchor_bounds_in_screen.bottom() + kNudgeDistanceFromAnchor, size.width(),
  245. size.height());
  246. nudge_widget_->SetBounds(bounds_in_screen);
  247. // The circular pulse should be a square that matches the smaller dimension of
  248. // `anchor_view_`. We use rounded corners to make it look like a circle.
  249. gfx::Rect pulse_layer_bounds = anchor_bounds_in_screen;
  250. wm::ConvertRectFromScreen(nudge_window->parent(), &pulse_layer_bounds);
  251. const int length =
  252. std::min(pulse_layer_bounds.width(), pulse_layer_bounds.height());
  253. pulse_layer_bounds.ClampToCenteredSize(gfx::Size(length, length));
  254. pulse_layer_->SetBounds(pulse_layer_bounds);
  255. pulse_layer_->SetRoundedCornerRadius(gfx::RoundedCornersF(length / 2.f));
  256. }
  257. void MultitaskMenuNudgeController::PerformPulseAnimation(int pulse_count) {
  258. if (pulse_count >= kPulses)
  259. return;
  260. DCHECK(pulse_layer_);
  261. // The pulse animation scales up and fades out on top of the maximize/restore
  262. // button until the nudge disappears.
  263. const gfx::Point pivot(
  264. gfx::Rect(pulse_layer_->GetTargetBounds().size()).CenterPoint());
  265. const gfx::Transform transform =
  266. gfx::GetScaleTransform(pivot, kPulseSizeMultiplier);
  267. pulse_layer_->SetOpacity(1.0f);
  268. pulse_layer_->SetTransform(gfx::Transform());
  269. // Note that `views::AnimationBuilder::Repeatedly` works here as well, but
  270. // causes tests to hang.
  271. views::AnimationBuilder builder;
  272. builder
  273. .SetPreemptionStrategy(
  274. ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
  275. .OnEnded(
  276. base::BindOnce(&MultitaskMenuNudgeController::PerformPulseAnimation,
  277. base::Unretained(this), pulse_count + 1))
  278. .Once()
  279. .SetDuration(kPulseDuration)
  280. .SetOpacity(pulse_layer_.get(), 0.0f, gfx::Tween::ACCEL_0_80_DECEL_80)
  281. .SetTransform(pulse_layer_.get(), transform,
  282. gfx::Tween::ACCEL_0_40_DECEL_100);
  283. }
  284. } // namespace ash