1 // Copyright 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/wm/desks/desk_mini_view.h"
6 
7 #include <algorithm>
8 
9 #include "ash/accessibility/accessibility_controller_impl.h"
10 #include "ash/shell.h"
11 #include "ash/strings/grit/ash_strings.h"
12 #include "ash/style/ash_color_provider.h"
13 #include "ash/wm/desks/close_desk_button.h"
14 #include "ash/wm/desks/desk.h"
15 #include "ash/wm/desks/desk_name_view.h"
16 #include "ash/wm/desks/desk_preview_view.h"
17 #include "ash/wm/desks/desks_bar_view.h"
18 #include "ash/wm/desks/desks_controller.h"
19 #include "ash/wm/desks/desks_restore_util.h"
20 #include "base/strings/string_util.h"
21 #include "ui/accessibility/ax_enums.mojom.h"
22 #include "ui/accessibility/ax_node_data.h"
23 #include "ui/aura/window.h"
24 #include "ui/base/l10n/l10n_util.h"
25 #include "ui/gfx/canvas.h"
26 #include "ui/views/widget/widget.h"
27 #include "ui/wm/core/coordinate_conversion.h"
28 
29 namespace ash {
30 
31 namespace {
32 
33 constexpr int kLabelPreviewSpacing = 8;
34 
35 constexpr int kCloseButtonMargin = 8;
36 
37 constexpr int kMinDeskNameViewWidth = 20;
38 
39 // Returns the width of the desk preview based on its |preview_height| and the
40 // aspect ratio of the root window taken from |root_window_size|.
GetPreviewWidth(const gfx::Size & root_window_size,int preview_height)41 int GetPreviewWidth(const gfx::Size& root_window_size, int preview_height) {
42   return preview_height * root_window_size.width() / root_window_size.height();
43 }
44 
45 // The desk preview bounds are proportional to the bounds of the display on
46 // which it resides, and whether the |compact| layout is used.
GetDeskPreviewBounds(aura::Window * root_window,bool compact)47 gfx::Rect GetDeskPreviewBounds(aura::Window* root_window, bool compact) {
48   const int preview_height = DeskPreviewView::GetHeight(root_window, compact);
49   const auto root_size = root_window->bounds().size();
50   return gfx::Rect(GetPreviewWidth(root_size, preview_height), preview_height);
51 }
52 
53 }  // namespace
54 
55 // -----------------------------------------------------------------------------
56 // DeskMiniView
57 
DeskMiniView(DesksBarView * owner_bar,aura::Window * root_window,Desk * desk)58 DeskMiniView::DeskMiniView(DesksBarView* owner_bar,
59                            aura::Window* root_window,
60                            Desk* desk)
61     : owner_bar_(owner_bar), root_window_(root_window), desk_(desk) {
62   DCHECK(root_window_);
63   DCHECK(root_window_->IsRootWindow());
64 
65   desk_->AddObserver(this);
66 
67   auto desk_name_view = std::make_unique<DeskNameView>();
68   desk_name_view->AddObserver(this);
69   desk_name_view->set_controller(this);
70   desk_name_view->SetText(desk_->name());
71 
72   SetPaintToLayer();
73   layer()->SetFillsBoundsOpaquely(false);
74 
75   // TODO(afakhry): Tooltips.
76 
77   desk_preview_ = AddChildView(std::make_unique<DeskPreviewView>(
78       base::BindRepeating(&DeskMiniView::OnDeskPreviewPressed,
79                           base::Unretained(this)),
80       this));
81   desk_name_view_ = AddChildView(std::move(desk_name_view));
82   close_desk_button_ =
83       AddChildView(std::make_unique<CloseDeskButton>(base::BindRepeating(
84           &DeskMiniView::OnCloseButtonPressed, base::Unretained(this))));
85 
86   UpdateCloseButtonVisibility();
87   UpdateBorderColor();
88 }
89 
~DeskMiniView()90 DeskMiniView::~DeskMiniView() {
91   desk_name_view_->RemoveObserver(this);
92   // In tests, where animations are disabled, the mini_view maybe destroyed
93   // before the desk.
94   if (desk_)
95     desk_->RemoveObserver(this);
96 }
97 
GetDeskContainer() const98 aura::Window* DeskMiniView::GetDeskContainer() const {
99   DCHECK(desk_);
100   return desk_->GetDeskContainerForRoot(root_window_);
101 }
102 
IsDeskNameBeingModified() const103 bool DeskMiniView::IsDeskNameBeingModified() const {
104   return desk_name_view_->HasFocus();
105 }
106 
UpdateCloseButtonVisibility()107 void DeskMiniView::UpdateCloseButtonVisibility() {
108   // Don't show the close button when hovered while the dragged window is on
109   // the DesksBarView.
110   // For switch access, setting the close button to visible allows users to
111   // navigate to it.
112   close_desk_button_->SetVisible(
113       DesksController::Get()->CanRemoveDesks() &&
114       !owner_bar_->dragged_item_over_bar() &&
115       (IsMouseHovered() || force_show_close_button_ ||
116        Shell::Get()->accessibility_controller()->IsSwitchAccessRunning()));
117 }
118 
OnWidgetGestureTap(const gfx::Rect & screen_rect,bool is_long_gesture)119 void DeskMiniView::OnWidgetGestureTap(const gfx::Rect& screen_rect,
120                                       bool is_long_gesture) {
121   const bool old_force_show_close_button = force_show_close_button_;
122   // Note that we don't want to hide the close button if it's a single tap
123   // within the bounds of an already visible button, which will later be handled
124   // as a press event on that close button that will result in closing the desk.
125   force_show_close_button_ =
126       (is_long_gesture && IsPointOnMiniView(screen_rect.CenterPoint())) ||
127       (!is_long_gesture && close_desk_button_->GetVisible() &&
128        close_desk_button_->DoesIntersectScreenRect(screen_rect));
129   if (old_force_show_close_button != force_show_close_button_)
130     UpdateCloseButtonVisibility();
131 }
132 
UpdateBorderColor()133 void DeskMiniView::UpdateBorderColor() {
134   DCHECK(desk_);
135   auto* color_provider = AshColorProvider::Get();
136   if ((owner_bar_->dragged_item_over_bar() &&
137        IsPointOnMiniView(owner_bar_->last_dragged_item_screen_location())) ||
138       IsViewHighlighted()) {
139     desk_preview_->SetBorderColor(color_provider->GetControlsLayerColor(
140         AshColorProvider::ControlsLayerType::kFocusRingColor));
141   } else if (!desk_->is_active()) {
142     desk_preview_->SetBorderColor(SK_ColorTRANSPARENT);
143   } else {
144     desk_preview_->SetBorderColor(color_provider->GetContentLayerColor(
145         AshColorProvider::ContentLayerType::kCurrentDeskColor));
146   }
147 }
148 
GetClassName() const149 const char* DeskMiniView::GetClassName() const {
150   return "DeskMiniView";
151 }
152 
Layout()153 void DeskMiniView::Layout() {
154   const bool compact = owner_bar_->UsesCompactLayout();
155   const gfx::Rect preview_bounds = GetDeskPreviewBounds(root_window_, compact);
156   desk_preview_->SetBoundsRect(preview_bounds);
157 
158   desk_name_view_->SetVisible(!compact);
159 
160   if (!compact)
161     LayoutDeskNameView(preview_bounds);
162 
163   close_desk_button_->SetBounds(
164       preview_bounds.right() - CloseDeskButton::kCloseButtonSize -
165           kCloseButtonMargin,
166       kCloseButtonMargin, CloseDeskButton::kCloseButtonSize,
167       CloseDeskButton::kCloseButtonSize);
168 }
169 
CalculatePreferredSize() const170 gfx::Size DeskMiniView::CalculatePreferredSize() const {
171   const bool compact = owner_bar_->UsesCompactLayout();
172   const gfx::Rect preview_bounds = GetDeskPreviewBounds(root_window_, compact);
173   if (compact)
174     return preview_bounds.size();
175 
176   // The preferred size takes into account only the width of the preview
177   // view.
178   return gfx::Size{preview_bounds.width(),
179                    preview_bounds.height() + 2 * kLabelPreviewSpacing +
180                        desk_name_view_->GetPreferredSize().height()};
181 }
182 
GetAccessibleNodeData(ui::AXNodeData * node_data)183 void DeskMiniView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
184   desk_preview_->GetAccessibleNodeData(node_data);
185 
186   // Note that the desk may have already been destroyed.
187   if (desk_) {
188     // Announce desk name.
189     node_data->AddStringAttribute(
190         ax::mojom::StringAttribute::kName,
191         l10n_util::GetStringFUTF8(IDS_ASH_DESKS_DESK_ACCESSIBLE_NAME,
192                                   desk_->name()));
193 
194     node_data->AddStringAttribute(
195         ax::mojom::StringAttribute::kValue,
196         l10n_util::GetStringUTF8(
197             desk_->is_active()
198                 ? IDS_ASH_DESKS_ACTIVE_DESK_MINIVIEW_A11Y_EXTRA_TIP
199                 : IDS_ASH_DESKS_INACTIVE_DESK_MINIVIEW_A11Y_EXTRA_TIP));
200   }
201 
202   if (DesksController::Get()->CanRemoveDesks()) {
203     node_data->AddStringAttribute(
204         ax::mojom::StringAttribute::kDescription,
205         l10n_util::GetStringUTF8(
206             IDS_ASH_OVERVIEW_CLOSABLE_HIGHLIGHT_ITEM_A11Y_EXTRA_TIP));
207   }
208 }
209 
OnThemeChanged()210 void DeskMiniView::OnThemeChanged() {
211   views::View::OnThemeChanged();
212   UpdateBorderColor();
213 }
214 
OnContentChanged()215 void DeskMiniView::OnContentChanged() {
216   desk_preview_->RecreateDeskContentsMirrorLayers();
217 }
218 
OnDeskDestroyed(const Desk * desk)219 void DeskMiniView::OnDeskDestroyed(const Desk* desk) {
220   // Note that the mini_view outlives the desk (which will be removed after all
221   // DeskController's observers have been notified of its removal) because of
222   // the animation.
223   // Note that we can't make it the other way around (i.e. make the desk outlive
224   // the mini_view). The desk's existence (or lack thereof) is more important
225   // than the existence of the mini_view, since it determines whether we can
226   // create new desks or remove existing ones. This determines whether the close
227   // button will show on hover, and whether the new_desk_button is enabled. We
228   // shouldn't allow that state to be wrong while the mini_views perform the
229   // desk removal animation.
230   // TODO(afakhry): Consider detaching the layer and destroying the mini_view
231   // directly.
232 
233   DCHECK_EQ(desk_, desk);
234   desk_ = nullptr;
235 
236   // No need to remove `this` as an observer; it's done automatically.
237 }
238 
OnDeskNameChanged(const base::string16 & new_name)239 void DeskMiniView::OnDeskNameChanged(const base::string16& new_name) {
240   if (is_desk_name_being_modified_)
241     return;
242 
243   desk_name_view_->SetTextAndElideIfNeeded(new_name);
244   desk_preview_->SetAccessibleName(new_name);
245 
246   Layout();
247 }
248 
GetView()249 views::View* DeskMiniView::GetView() {
250   return this;
251 }
252 
MaybeActivateHighlightedView()253 void DeskMiniView::MaybeActivateHighlightedView() {
254   DesksController::Get()->ActivateDesk(desk(),
255                                        DesksSwitchSource::kMiniViewButton);
256 }
257 
MaybeCloseHighlightedView()258 void DeskMiniView::MaybeCloseHighlightedView() {
259   OnCloseButtonPressed();
260 }
261 
OnViewHighlighted()262 void DeskMiniView::OnViewHighlighted() {
263   UpdateBorderColor();
264 }
265 
OnViewUnhighlighted()266 void DeskMiniView::OnViewUnhighlighted() {
267   UpdateBorderColor();
268 }
269 
ContentsChanged(views::Textfield * sender,const base::string16 & new_contents)270 void DeskMiniView::ContentsChanged(views::Textfield* sender,
271                                    const base::string16& new_contents) {
272   DCHECK_EQ(sender, desk_name_view_);
273   DCHECK(is_desk_name_being_modified_);
274   if (!desk_)
275     return;
276 
277   // Avoid copying new_contents if we don't need to trim it below.
278   const base::string16* new_text = &new_contents;
279 
280   // To avoid potential security and memory issues, we don't allow desk names to
281   // have an unbounded length. Therefore we trim if needed at kMaxLength UTF-16
282   // boundary. Note that we don't care about code point boundaries in this case.
283   base::string16 trimmed_new_contents;
284   if (new_contents.size() > DeskNameView::kMaxLength) {
285     trimmed_new_contents = new_contents;
286     trimmed_new_contents.resize(DeskNameView::kMaxLength);
287     new_text = &trimmed_new_contents;
288     desk_name_view_->SetText(trimmed_new_contents);
289   }
290 
291   desk_->SetName(
292       base::CollapseWhitespace(*new_text,
293                                /*trim_sequences_with_line_breaks=*/false),
294       /*set_by_user=*/true);
295 
296   Layout();
297 }
298 
HandleKeyEvent(views::Textfield * sender,const ui::KeyEvent & key_event)299 bool DeskMiniView::HandleKeyEvent(views::Textfield* sender,
300                                   const ui::KeyEvent& key_event) {
301   DCHECK_EQ(sender, desk_name_view_);
302   DCHECK(is_desk_name_being_modified_);
303 
304   // Pressing enter or escape should blur the focus away from DeskNameView so
305   // that editing the desk's name ends.
306   if (key_event.type() != ui::ET_KEY_PRESSED)
307     return false;
308 
309   if (key_event.key_code() != ui::VKEY_RETURN &&
310       key_event.key_code() != ui::VKEY_ESCAPE) {
311     return false;
312   }
313 
314   DeskNameView::CommitChanges(GetWidget());
315 
316   Shell::Get()
317       ->accessibility_controller()
318       ->TriggerAccessibilityAlertWithMessage(l10n_util::GetStringFUTF8(
319           IDS_ASH_DESKS_DESK_NAME_COMMIT, desk_->name()));
320   return true;
321 }
322 
HandleMouseEvent(views::Textfield * sender,const ui::MouseEvent & mouse_event)323 bool DeskMiniView::HandleMouseEvent(views::Textfield* sender,
324                                     const ui::MouseEvent& mouse_event) {
325   DCHECK_EQ(sender, desk_name_view_);
326 
327   switch (mouse_event.type()) {
328     case ui::ET_MOUSE_PRESSED:
329       // If this is the first mouse press on the DeskNameView, then it's not
330       // focused yet. OnViewFocused() should not select all text, since it will
331       // be undone by the mouse release event. Instead we defer it until we get
332       // the mouse release event.
333       if (!is_desk_name_being_modified_)
334         defer_select_all_ = true;
335       break;
336 
337     case ui::ET_MOUSE_RELEASED:
338       if (defer_select_all_) {
339         defer_select_all_ = false;
340         // The user may have already clicked and dragged to select some range
341         // other than all the text. In this case, don't mess with an existing
342         // selection.
343         if (!desk_name_view_->HasSelection())
344           desk_name_view_->SelectAll(false);
345         return true;
346       }
347       break;
348 
349     default:
350       break;
351   }
352 
353   return false;
354 }
355 
OnViewFocused(views::View * observed_view)356 void DeskMiniView::OnViewFocused(views::View* observed_view) {
357   DCHECK_EQ(observed_view, desk_name_view_);
358   is_desk_name_being_modified_ = true;
359   desk_name_view_->UpdateViewAppearance();
360 
361   // Set the unelided desk name so that the full name shows up for the user to
362   // be able to change it.
363   desk_name_view_->SetText(desk_->name());
364 
365   if (!defer_select_all_)
366     desk_name_view_->SelectAll(false);
367 }
368 
OnViewBlurred(views::View * observed_view)369 void DeskMiniView::OnViewBlurred(views::View* observed_view) {
370   DCHECK_EQ(observed_view, desk_name_view_);
371   is_desk_name_being_modified_ = false;
372   defer_select_all_ = false;
373   desk_name_view_->UpdateViewAppearance();
374 
375   // When committing the name, do not allow an empty desk name. Revert back to
376   // the default name.
377   // TODO(afakhry): Make this more robust. What if user renames a previously
378   // user-modified desk name, say from "code" to "Desk 2", and that desk
379   // happened to be in the second position. Since the new name matches the
380   // default one for this position, should we revert it (i.e. consider it
381   // `set_by_user = false`?
382   if (desk_->name().empty()) {
383     DesksController::Get()->RevertDeskNameToDefault(desk_);
384     return;
385   }
386 
387   OnDeskNameChanged(desk_->name());
388 
389   // Only when the new desk name has been committed is when we can update the
390   // desks restore prefs.
391   desks_restore_util::UpdatePrimaryUserDesksPrefs();
392 }
393 
IsPointOnMiniView(const gfx::Point & screen_location) const394 bool DeskMiniView::IsPointOnMiniView(const gfx::Point& screen_location) const {
395   gfx::Point point_in_view = screen_location;
396   ConvertPointFromScreen(this, &point_in_view);
397   return HitTestPoint(point_in_view);
398 }
399 
GetMinWidthForDefaultLayout() const400 int DeskMiniView::GetMinWidthForDefaultLayout() const {
401   const auto& root_size = root_window_->bounds().size();
402   return GetPreviewWidth(root_size,
403                          DeskPreviewView::GetHeight(root_window_,
404                                                     /*compact=*/false));
405 }
406 
IsDeskNameViewVisibleForTesting() const407 bool DeskMiniView::IsDeskNameViewVisibleForTesting() const {
408   return desk_name_view_->GetVisible();
409 }
410 
OnCloseButtonPressed()411 void DeskMiniView::OnCloseButtonPressed() {
412   auto* controller = DesksController::Get();
413   if (!controller->CanRemoveDesks())
414     return;
415 
416   // Hide the close button so it can no longer be pressed.
417   close_desk_button_->SetVisible(false);
418 
419   desk_preview_->OnRemovingDesk();
420 
421   controller->RemoveDesk(desk_, DesksCreationRemovalSource::kButton);
422 }
423 
OnDeskPreviewPressed()424 void DeskMiniView::OnDeskPreviewPressed() {
425   DesksController::Get()->ActivateDesk(desk_,
426                                        DesksSwitchSource::kMiniViewButton);
427 }
428 
LayoutDeskNameView(const gfx::Rect & preview_bounds)429 void DeskMiniView::LayoutDeskNameView(const gfx::Rect& preview_bounds) {
430   const int previous_width = desk_name_view_->width();
431   const gfx::Size desk_name_view_size = desk_name_view_->GetPreferredSize();
432 
433   const int text_width =
434       base::ClampToRange(desk_name_view_size.width(), kMinDeskNameViewWidth,
435                          preview_bounds.width());
436 
437   const int desk_name_view_x =
438       preview_bounds.x() + (preview_bounds.width() - text_width) / 2;
439   gfx::Rect desk_name_view_bounds{
440       desk_name_view_x, preview_bounds.bottom() + kLabelPreviewSpacing,
441       text_width, desk_name_view_size.height()};
442   desk_name_view_->SetBoundsRect(desk_name_view_bounds);
443 
444   // A change in the DeskNameView's width might mean the need
445   // to elide the text differently.
446   if (previous_width != desk_name_view_bounds.width())
447     OnDeskNameChanged(desk_->name());
448 }
449 
450 }  // namespace ash
451