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