1 // Copyright (c) 2012 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.
5 #include "ash/system/tray/tray_background_view.h"
7 #include "ash/root_window_controller.h"
8 #include "ash/screen_util.h"
9 #include "ash/shelf/shelf_layout_manager.h"
10 #include "ash/shelf/shelf_widget.h"
11 #include "ash/shell.h"
12 #include "ash/shell_window_ids.h"
13 #include "ash/system/status_area_widget.h"
14 #include "ash/system/status_area_widget_delegate.h"
15 #include "ash/system/tray/system_tray.h"
16 #include "ash/system/tray/tray_constants.h"
17 #include "ash/system/tray/tray_event_filter.h"
18 #include "ash/wm/window_animations.h"
19 #include "base/command_line.h"
20 #include "grit/ash_resources.h"
21 #include "ui/accessibility/ax_view_state.h"
22 #include "ui/aura/window.h"
23 #include "ui/aura/window_event_dispatcher.h"
24 #include "ui/base/nine_image_painter_factory.h"
25 #include "ui/base/ui_base_switches_util.h"
26 #include "ui/compositor/layer.h"
27 #include "ui/compositor/layer_animation_element.h"
28 #include "ui/compositor/scoped_layer_animation_settings.h"
29 #include "ui/events/event_constants.h"
30 #include "ui/gfx/animation/tween.h"
31 #include "ui/gfx/canvas.h"
32 #include "ui/gfx/geometry/rect.h"
33 #include "ui/gfx/image/image_skia.h"
34 #include "ui/gfx/image/image_skia_operations.h"
35 #include "ui/gfx/nine_image_painter.h"
36 #include "ui/gfx/screen.h"
37 #include "ui/gfx/skia_util.h"
38 #include "ui/gfx/transform.h"
39 #include "ui/views/background.h"
40 #include "ui/views/layout/box_layout.h"
44 const int kTrayBackgroundAlpha
= 100;
45 const int kTrayBackgroundHoverAlpha
= 150;
46 const SkColor kTrayBackgroundPressedColor
= SkColorSetRGB(66, 129, 244);
48 const int kAnimationDurationForPopupMs
= 200;
50 // Duration of opacity animation for visibility changes.
51 const int kAnimationDurationForVisibilityMs
= 250;
53 // When becoming visible delay the animation so that StatusAreaWidgetDelegate
54 // can animate sibling views out of the position to be occuped by the
55 // TrayBackgroundView.
56 const int kShowAnimationDelayMs
= 100;
60 using views::TrayBubbleView
;
65 const char TrayBackgroundView::kViewClassName
[] = "tray/TrayBackgroundView";
67 // Used to track when the anchor widget changes position on screen so that the
68 // bubble position can be updated.
69 class TrayBackgroundView::TrayWidgetObserver
: public views::WidgetObserver
{
71 explicit TrayWidgetObserver(TrayBackgroundView
* host
)
75 void OnWidgetBoundsChanged(views::Widget
* widget
,
76 const gfx::Rect
& new_bounds
) override
{
77 host_
->AnchorUpdated();
80 void OnWidgetVisibilityChanged(views::Widget
* widget
, bool visible
) override
{
81 host_
->AnchorUpdated();
85 TrayBackgroundView
* host_
;
87 DISALLOW_COPY_AND_ASSIGN(TrayWidgetObserver
);
90 class TrayBackground
: public views::Background
{
92 const static int kImageTypeDefault
= 0;
93 const static int kImageTypeOnBlack
= 1;
94 const static int kImageTypePressed
= 2;
95 const static int kNumStates
= 3;
97 const static int kImageHorizontal
= 0;
98 const static int kImageVertical
= 1;
99 const static int kNumOrientations
= 2;
101 explicit TrayBackground(TrayBackgroundView
* tray_background_view
) :
102 tray_background_view_(tray_background_view
) {
103 set_alpha(kTrayBackgroundAlpha
);
106 ~TrayBackground() override
{}
108 SkColor
color() { return color_
; }
109 void set_color(SkColor color
) { color_
= color
; }
110 void set_alpha(int alpha
) { color_
= SkColorSetARGB(alpha
, 0, 0, 0); }
113 ShelfWidget
* GetShelfWidget() const {
114 return RootWindowController::ForWindow(tray_background_view_
->
115 status_area_widget()->GetNativeWindow())->shelf();
118 // Overridden from views::Background.
119 void Paint(gfx::Canvas
* canvas
, views::View
* view
) const override
{
120 const int kGridSizeForPainter
= 9;
121 const int kImages
[kNumOrientations
][kNumStates
][kGridSizeForPainter
] = {
123 IMAGE_GRID_HORIZONTAL(IDR_AURA_TRAY_BG_HORIZ
),
124 IMAGE_GRID_HORIZONTAL(IDR_AURA_TRAY_BG_HORIZ_ONBLACK
),
125 IMAGE_GRID_HORIZONTAL(IDR_AURA_TRAY_BG_HORIZ_PRESSED
),
128 IMAGE_GRID_VERTICAL(IDR_AURA_TRAY_BG_VERTICAL
),
129 IMAGE_GRID_VERTICAL(IDR_AURA_TRAY_BG_VERTICAL_ONBLACK
),
130 IMAGE_GRID_VERTICAL(IDR_AURA_TRAY_BG_VERTICAL_PRESSED
),
134 int orientation
= kImageHorizontal
;
135 ShelfWidget
* shelf_widget
= GetShelfWidget();
137 !shelf_widget
->shelf_layout_manager()->IsHorizontalAlignment())
138 orientation
= kImageVertical
;
140 int state
= kImageTypeDefault
;
141 if (tray_background_view_
->draw_background_as_active())
142 state
= kImageTypePressed
;
143 else if (shelf_widget
&& shelf_widget
->GetDimsShelf())
144 state
= kImageTypeOnBlack
;
146 state
= kImageTypeDefault
;
148 ui::CreateNineImagePainter(kImages
[orientation
][state
])
149 ->Paint(canvas
, view
->GetLocalBounds());
153 // Reference to the TrayBackgroundView for which this is a background.
154 TrayBackgroundView
* tray_background_view_
;
156 DISALLOW_COPY_AND_ASSIGN(TrayBackground
);
159 TrayBackgroundView::TrayContainer::TrayContainer(ShelfAlignment alignment
)
160 : alignment_(alignment
) {
164 void TrayBackgroundView::TrayContainer::SetAlignment(ShelfAlignment alignment
) {
165 if (alignment_
== alignment
)
167 alignment_
= alignment
;
171 gfx::Size
TrayBackgroundView::TrayContainer::GetPreferredSize() const {
173 return views::View::GetPreferredSize();
177 void TrayBackgroundView::TrayContainer::ChildPreferredSizeChanged(
178 views::View
* child
) {
179 PreferredSizeChanged();
182 void TrayBackgroundView::TrayContainer::ChildVisibilityChanged(View
* child
) {
183 PreferredSizeChanged();
186 void TrayBackgroundView::TrayContainer::ViewHierarchyChanged(
187 const ViewHierarchyChangedDetails
& details
) {
188 if (details
.parent
== this)
189 PreferredSizeChanged();
192 void TrayBackgroundView::TrayContainer::UpdateLayout() {
193 // Adjust the size of status tray dark background by adding additional
195 if (alignment_
== SHELF_ALIGNMENT_BOTTOM
||
196 alignment_
== SHELF_ALIGNMENT_TOP
) {
197 SetBorder(views::Border::CreateEmptyBorder(
198 kPaddingFromEdgeOfShelf
,
199 kPaddingFromEdgeOfShelf
,
200 kPaddingFromEdgeOfShelf
,
201 kPaddingFromEdgeOfShelf
));
203 views::BoxLayout
* layout
=
204 new views::BoxLayout(views::BoxLayout::kHorizontal
, 0, 0, 0);
205 layout
->SetDefaultFlex(1);
206 views::View::SetLayoutManager(layout
);
208 SetBorder(views::Border::CreateEmptyBorder(
209 kPaddingFromEdgeOfShelf
,
210 kPaddingFromEdgeOfShelf
,
211 kPaddingFromEdgeOfShelf
,
212 kPaddingFromEdgeOfShelf
));
214 views::BoxLayout
* layout
=
215 new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 0);
216 layout
->SetDefaultFlex(1);
217 views::View::SetLayoutManager(layout
);
219 PreferredSizeChanged();
222 ////////////////////////////////////////////////////////////////////////////////
223 // TrayBackgroundView
225 TrayBackgroundView::TrayBackgroundView(StatusAreaWidget
* status_area_widget
)
226 : status_area_widget_(status_area_widget
),
227 tray_container_(NULL
),
228 shelf_alignment_(SHELF_ALIGNMENT_BOTTOM
),
230 hide_background_animator_(this, 0, kTrayBackgroundAlpha
),
231 hover_background_animator_(
234 kTrayBackgroundHoverAlpha
- kTrayBackgroundAlpha
),
236 draw_background_as_active_(false),
237 widget_observer_(new TrayWidgetObserver(this)) {
238 set_notify_enter_exit_on_child(true);
240 // Initially we want to paint the background, but without the hover effect.
241 hide_background_animator_
.SetPaintsBackground(
242 true, BACKGROUND_CHANGE_IMMEDIATE
);
243 hover_background_animator_
.SetPaintsBackground(
244 false, BACKGROUND_CHANGE_IMMEDIATE
);
246 tray_container_
= new TrayContainer(shelf_alignment_
);
247 SetContents(tray_container_
);
248 tray_event_filter_
.reset(new TrayEventFilter
);
250 SetPaintToLayer(true);
251 SetFillsBoundsOpaquely(false);
252 // Start the tray items not visible, because visibility changes are animated.
253 views::View::SetVisible(false);
256 TrayBackgroundView::~TrayBackgroundView() {
258 GetWidget()->RemoveObserver(widget_observer_
.get());
259 StopObservingImplicitAnimations();
262 void TrayBackgroundView::Initialize() {
263 GetWidget()->AddObserver(widget_observer_
.get());
267 void TrayBackgroundView::SetVisible(bool visible
) {
268 if (visible
== layer()->GetTargetVisibility())
272 // The alignment of the shelf can change while the TrayBackgroundView is
273 // hidden. Reset the offscreen transform so that the animation to becoming
274 // visible reflects the current layout.
275 HideTransformation();
276 // SetVisible(false) is defered until the animation for hiding is done.
277 // Otherwise the view is immediately hidden and the animation does not
279 views::View::SetVisible(true);
280 // If SetVisible(true) is called while animating to not visible, then
281 // views::View::SetVisible(true) is a no-op. When the previous animation
282 // ends layer->SetVisible(false) is called. To prevent this
283 // layer->SetVisible(true) immediately interrupts the animation of this
284 // property, and keeps the layer visible.
285 layer()->SetVisible(true);
288 ui::ScopedLayerAnimationSettings
animation(layer()->GetAnimator());
289 animation
.SetTransitionDuration(base::TimeDelta::FromMilliseconds(
290 kAnimationDurationForVisibilityMs
));
291 animation
.SetPreemptionStrategy(
292 ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET
);
295 animation
.SetTweenType(gfx::Tween::EASE_OUT
);
296 // Show is delayed so as to allow time for other children of
297 // StatusAreaWidget to begin animating to their new positions.
298 layer()->GetAnimator()->SchedulePauseForProperties(
299 base::TimeDelta::FromMilliseconds(kShowAnimationDelayMs
),
300 ui::LayerAnimationElement::OPACITY
|
301 ui::LayerAnimationElement::TRANSFORM
);
302 layer()->SetOpacity(1.0f
);
303 gfx::Transform transform
;
304 transform
.Translate(0.0f
, 0.0f
);
305 layer()->SetTransform(transform
);
307 // Listen only to the hide animation. As we cannot turn off visibility
308 // until the animation is over.
309 animation
.AddObserver(this);
310 animation
.SetTweenType(gfx::Tween::EASE_IN
);
311 layer()->SetOpacity(0.0f
);
312 layer()->SetVisible(false);
313 HideTransformation();
317 const char* TrayBackgroundView::GetClassName() const {
318 return kViewClassName
;
321 void TrayBackgroundView::OnMouseEntered(const ui::MouseEvent
& event
) {
325 void TrayBackgroundView::OnMouseExited(const ui::MouseEvent
& event
) {
329 void TrayBackgroundView::ChildPreferredSizeChanged(views::View
* child
) {
330 PreferredSizeChanged();
333 void TrayBackgroundView::GetAccessibleState(ui::AXViewState
* state
) {
334 state
->role
= ui::AX_ROLE_BUTTON
;
335 state
->name
= GetAccessibleNameForTray();
338 void TrayBackgroundView::AboutToRequestFocusFromTabTraversal(bool reverse
) {
339 // Return focus to the login view. See crbug.com/120500.
340 views::View
* v
= GetNextFocusableView();
342 v
->AboutToRequestFocusFromTabTraversal(reverse
);
345 bool TrayBackgroundView::PerformAction(const ui::Event
& event
) {
349 gfx::Rect
TrayBackgroundView::GetFocusBounds() {
350 // The tray itself expands to the right and bottom edge of the screen to make
351 // sure clicking on the edges brings up the popup. However, the focus border
352 // should be only around the container.
353 return GetContentsBounds();
356 void TrayBackgroundView::OnGestureEvent(ui::GestureEvent
* event
) {
357 if (switches::IsTouchFeedbackEnabled()) {
358 if (event
->type() == ui::ET_GESTURE_TAP_DOWN
) {
359 SetDrawBackgroundAsActive(true);
360 } else if (event
->type() == ui::ET_GESTURE_SCROLL_BEGIN
||
361 event
->type() == ui::ET_GESTURE_TAP_CANCEL
) {
362 SetDrawBackgroundAsActive(false);
365 ActionableView::OnGestureEvent(event
);
368 void TrayBackgroundView::UpdateBackground(int alpha
) {
369 // The animator should never fire when the alternate shelf layout is used.
370 if (!background_
|| draw_background_as_active_
)
372 background_
->set_alpha(hide_background_animator_
.alpha() +
373 hover_background_animator_
.alpha());
377 void TrayBackgroundView::SetContents(views::View
* contents
) {
378 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical
, 0, 0, 0));
379 AddChildView(contents
);
382 void TrayBackgroundView::SetPaintsBackground(
383 bool value
, BackgroundAnimatorChangeType change_type
) {
384 hide_background_animator_
.SetPaintsBackground(value
, change_type
);
387 void TrayBackgroundView::SetContentsBackground() {
388 background_
= new TrayBackground(this);
389 tray_container_
->set_background(background_
);
392 ShelfLayoutManager
* TrayBackgroundView::GetShelfLayoutManager() {
393 return ShelfLayoutManager::ForShelf(GetWidget()->GetNativeView());
396 void TrayBackgroundView::SetShelfAlignment(ShelfAlignment alignment
) {
397 shelf_alignment_
= alignment
;
399 tray_container_
->SetAlignment(alignment
);
402 void TrayBackgroundView::SetTrayBorder() {
403 views::View
* parent
= status_area_widget_
->status_area_widget_delegate();
404 // Tray views are laid out right-to-left or bottom-to-top
405 bool on_edge
= (this == parent
->child_at(0));
406 int left_edge
, top_edge
, right_edge
, bottom_edge
;
407 if (shelf_alignment() == SHELF_ALIGNMENT_BOTTOM
) {
408 top_edge
= ShelfLayoutManager::kShelfItemInset
;
410 bottom_edge
= kShelfSize
-
411 ShelfLayoutManager::kShelfItemInset
- kShelfItemHeight
;
412 right_edge
= on_edge
? kPaddingFromEdgeOfShelf
: 0;
413 } else if (shelf_alignment() == SHELF_ALIGNMENT_LEFT
) {
415 left_edge
= kShelfSize
-
416 ShelfLayoutManager::kShelfItemInset
- kShelfItemHeight
;
417 bottom_edge
= on_edge
? kPaddingFromEdgeOfShelf
: 0;
418 right_edge
= ShelfLayoutManager::kShelfItemInset
;
419 } else { // SHELF_ALIGNMENT_RIGHT
421 left_edge
= ShelfLayoutManager::kShelfItemInset
;
422 bottom_edge
= on_edge
? kPaddingFromEdgeOfShelf
: 0;
423 right_edge
= kShelfSize
-
424 ShelfLayoutManager::kShelfItemInset
- kShelfItemHeight
;
426 SetBorder(views::Border::CreateEmptyBorder(
427 top_edge
, left_edge
, bottom_edge
, right_edge
));
430 void TrayBackgroundView::OnImplicitAnimationsCompleted() {
431 // If there is another animation in the queue, the reverse animation was
432 // triggered before the completion of animating to invisible. Do not turn off
433 // the visibility so that the next animation may render. The value of
434 // layer()->GetTargetVisibility() can be incorrect if the hide animation was
435 // aborted to schedule an animation to become visible. As the new animation
436 // is not yet added to the queue. crbug.com/374236
437 if(layer()->GetAnimator()->is_animating() ||
438 layer()->GetTargetVisibility())
440 views::View::SetVisible(false);
443 bool TrayBackgroundView::RequiresNotificationWhenAnimatorDestroyed() const {
444 // This is needed so that OnImplicitAnimationsCompleted() is called even upon
445 // destruction of the animator. This can occure when parallel animations
446 // caused by ScreenRotationAnimator end before the animations of
447 // TrayBackgroundView. This allows for a proper update to the visual state of
448 // the view. (crbug.com/476667)
452 void TrayBackgroundView::HideTransformation() {
453 gfx::Transform transform
;
454 if (shelf_alignment_
== SHELF_ALIGNMENT_BOTTOM
||
455 shelf_alignment_
== SHELF_ALIGNMENT_TOP
)
456 transform
.Translate(width(), 0.0f
);
458 transform
.Translate(0.0f
, height());
459 layer()->SetTransform(transform
);
462 void TrayBackgroundView::InitializeBubbleAnimations(
463 views::Widget
* bubble_widget
) {
464 wm::SetWindowVisibilityAnimationType(
465 bubble_widget
->GetNativeWindow(),
466 wm::WINDOW_VISIBILITY_ANIMATION_TYPE_FADE
);
467 wm::SetWindowVisibilityAnimationTransition(
468 bubble_widget
->GetNativeWindow(),
470 wm::SetWindowVisibilityAnimationDuration(
471 bubble_widget
->GetNativeWindow(),
472 base::TimeDelta::FromMilliseconds(kAnimationDurationForPopupMs
));
475 aura::Window
* TrayBackgroundView::GetBubbleWindowContainer() const {
476 return ash::Shell::GetContainer(
477 tray_container()->GetWidget()->GetNativeWindow()->GetRootWindow(),
478 ash::kShellWindowId_SettingBubbleContainer
);
481 gfx::Rect
TrayBackgroundView::GetBubbleAnchorRect(
482 views::Widget
* anchor_widget
,
483 TrayBubbleView::AnchorType anchor_type
,
484 TrayBubbleView::AnchorAlignment anchor_alignment
) const {
486 if (anchor_widget
&& anchor_widget
->IsVisible()) {
487 rect
= anchor_widget
->GetWindowBoundsInScreen();
488 if (anchor_type
== TrayBubbleView::ANCHOR_TYPE_TRAY
) {
489 if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM
) {
490 bool rtl
= base::i18n::IsRTL();
492 rtl
? kBubblePaddingHorizontalSide
: 0,
493 kBubblePaddingHorizontalBottom
,
494 rtl
? 0 : kBubblePaddingHorizontalSide
,
496 } else if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_LEFT
) {
497 rect
.Inset(0, 0, kBubblePaddingVerticalSide
+ 4,
498 kBubblePaddingVerticalBottom
);
499 } else if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_RIGHT
) {
500 rect
.Inset(kBubblePaddingVerticalSide
, 0, 0,
501 kBubblePaddingVerticalBottom
);
503 // TODO(bruthig) May need to handle other ANCHOR_ALIGNMENT_ values.
504 // ie. ANCHOR_ALIGNMENT_TOP
505 DCHECK(false) << "Unhandled anchor alignment.";
507 } else if (anchor_type
== TrayBubbleView::ANCHOR_TYPE_BUBBLE
) {
508 // Invert the offsets to align with the bubble below.
509 // Note that with the alternate shelf layout the tips are not shown and
510 // the offsets for left and right alignment do not need to be applied.
511 int vertical_alignment
= 0;
512 int horizontal_alignment
= kBubblePaddingVerticalBottom
;
513 if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_LEFT
)
514 rect
.Inset(vertical_alignment
, 0, 0, horizontal_alignment
);
515 else if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_RIGHT
)
516 rect
.Inset(0, 0, vertical_alignment
, horizontal_alignment
);
518 DCHECK(false) << "Unhandled anchor type.";
521 aura::Window
* target_root
= anchor_widget
?
522 anchor_widget
->GetNativeView()->GetRootWindow() :
523 Shell::GetPrimaryRootWindow();
524 rect
= target_root
->bounds();
525 if (anchor_type
== TrayBubbleView::ANCHOR_TYPE_TRAY
) {
526 if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM
) {
528 base::i18n::IsRTL() ?
529 kPaddingFromRightEdgeOfScreenBottomAlignment
:
530 rect
.width() - kPaddingFromRightEdgeOfScreenBottomAlignment
,
531 rect
.height() - kPaddingFromBottomOfScreenBottomAlignment
,
533 rect
= ScreenUtil::ConvertRectToScreen(target_root
, rect
);
534 } else if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_LEFT
) {
536 kPaddingFromRightEdgeOfScreenBottomAlignment
,
537 rect
.height() - kPaddingFromBottomOfScreenBottomAlignment
,
539 rect
= ScreenUtil::ConvertRectToScreen(target_root
, rect
);
540 } else if (anchor_alignment
== TrayBubbleView::ANCHOR_ALIGNMENT_RIGHT
) {
542 rect
.width() - kPaddingFromRightEdgeOfScreenBottomAlignment
,
543 rect
.height() - kPaddingFromBottomOfScreenBottomAlignment
,
545 rect
= ScreenUtil::ConvertRectToScreen(target_root
, rect
);
547 // TODO(bruthig) May need to handle other ANCHOR_ALIGNMENT_ values.
548 // ie. ANCHOR_ALIGNMENT_TOP
549 DCHECK(false) << "Unhandled anchor alignment.";
553 base::i18n::IsRTL() ?
554 kPaddingFromRightEdgeOfScreenBottomAlignment
:
555 rect
.width() - kPaddingFromRightEdgeOfScreenBottomAlignment
,
556 rect
.height() - kPaddingFromBottomOfScreenBottomAlignment
,
563 TrayBubbleView::AnchorAlignment
TrayBackgroundView::GetAnchorAlignment() const {
564 switch (shelf_alignment_
) {
565 case SHELF_ALIGNMENT_BOTTOM
:
566 return TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM
;
567 case SHELF_ALIGNMENT_LEFT
:
568 return TrayBubbleView::ANCHOR_ALIGNMENT_LEFT
;
569 case SHELF_ALIGNMENT_RIGHT
:
570 return TrayBubbleView::ANCHOR_ALIGNMENT_RIGHT
;
571 case SHELF_ALIGNMENT_TOP
:
572 return TrayBubbleView::ANCHOR_ALIGNMENT_TOP
;
575 return TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM
;
578 void TrayBackgroundView::SetDrawBackgroundAsActive(bool visible
) {
579 if (draw_background_as_active_
== visible
)
581 draw_background_as_active_
= visible
;
585 // Do not change gradually, changing color between grey and blue is weird.
586 if (draw_background_as_active_
)
587 background_
->set_color(kTrayBackgroundPressedColor
);
589 background_
->set_alpha(kTrayBackgroundHoverAlpha
);
591 background_
->set_alpha(kTrayBackgroundAlpha
);
595 void TrayBackgroundView::UpdateBubbleViewArrow(
596 views::TrayBubbleView
* bubble_view
) {
597 // Nothing to do here.