OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "components/exo/shell_surface.h" | 5 #include "components/exo/shell_surface.h" |
6 | 6 |
7 #include "ash/aura/wm_window_aura.h" | 7 #include "ash/aura/wm_window_aura.h" |
8 #include "ash/common/shell_window_ids.h" | 8 #include "ash/common/shell_window_ids.h" |
9 #include "ash/common/wm/window_resizer.h" | 9 #include "ash/common/wm/window_resizer.h" |
10 #include "ash/common/wm/window_state.h" | 10 #include "ash/common/wm/window_state.h" |
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
154 //////////////////////////////////////////////////////////////////////////////// | 154 //////////////////////////////////////////////////////////////////////////////// |
155 // ShellSurface, public: | 155 // ShellSurface, public: |
156 | 156 |
157 DEFINE_LOCAL_WINDOW_PROPERTY_KEY(std::string*, kApplicationIdKey, nullptr) | 157 DEFINE_LOCAL_WINDOW_PROPERTY_KEY(std::string*, kApplicationIdKey, nullptr) |
158 DEFINE_LOCAL_WINDOW_PROPERTY_KEY(Surface*, kMainSurfaceKey, nullptr) | 158 DEFINE_LOCAL_WINDOW_PROPERTY_KEY(Surface*, kMainSurfaceKey, nullptr) |
159 | 159 |
160 ShellSurface::ShellSurface(Surface* surface, | 160 ShellSurface::ShellSurface(Surface* surface, |
161 ShellSurface* parent, | 161 ShellSurface* parent, |
162 const gfx::Rect& initial_bounds, | 162 const gfx::Rect& initial_bounds, |
163 bool activatable, | 163 bool activatable, |
164 bool resizeable, | |
165 int container) | 164 int container) |
166 : widget_(nullptr), | 165 : widget_(nullptr), |
167 surface_(surface), | 166 surface_(surface), |
168 parent_(parent ? parent->GetWidget()->GetNativeWindow() : nullptr), | 167 parent_(parent ? parent->GetWidget()->GetNativeWindow() : nullptr), |
169 initial_bounds_(initial_bounds), | 168 initial_bounds_(initial_bounds), |
170 activatable_(activatable), | 169 activatable_(activatable), |
171 resizeable_(resizeable), | |
172 container_(container), | 170 container_(container), |
| 171 pending_show_widget_(false), |
173 scale_(1.0), | 172 scale_(1.0), |
174 pending_scale_(1.0), | 173 pending_scale_(1.0), |
175 scoped_configure_(nullptr), | 174 scoped_configure_(nullptr), |
176 ignore_window_bounds_changes_(false), | 175 ignore_window_bounds_changes_(false), |
177 resize_component_(HTCAPTION), | 176 resize_component_(HTCAPTION), |
178 pending_resize_component_(HTCAPTION) { | 177 pending_resize_component_(HTCAPTION) { |
179 ash::Shell::GetInstance()->activation_client()->AddObserver(this); | 178 ash::Shell::GetInstance()->activation_client()->AddObserver(this); |
180 surface_->SetSurfaceDelegate(this); | 179 surface_->SetSurfaceDelegate(this); |
181 surface_->AddSurfaceObserver(this); | 180 surface_->AddSurfaceObserver(this); |
182 surface_->Show(); | 181 surface_->Show(); |
183 set_owned_by_client(); | 182 set_owned_by_client(); |
184 if (parent_) | 183 if (parent_) |
185 parent_->AddObserver(this); | 184 parent_->AddObserver(this); |
186 } | 185 } |
187 | 186 |
188 ShellSurface::ShellSurface(Surface* surface) | 187 ShellSurface::ShellSurface(Surface* surface) |
189 : ShellSurface(surface, | 188 : ShellSurface(surface, |
190 nullptr, | 189 nullptr, |
191 gfx::Rect(), | 190 gfx::Rect(), |
192 true, | 191 true, |
193 true, | |
194 ash::kShellWindowId_DefaultContainer) {} | 192 ash::kShellWindowId_DefaultContainer) {} |
195 | 193 |
196 ShellSurface::~ShellSurface() { | 194 ShellSurface::~ShellSurface() { |
197 DCHECK(!scoped_configure_); | 195 DCHECK(!scoped_configure_); |
198 ash::Shell::GetInstance()->activation_client()->RemoveObserver(this); | 196 ash::Shell::GetInstance()->activation_client()->RemoveObserver(this); |
199 if (surface_) { | 197 if (surface_) { |
200 if (scale_ != 1.0) | 198 if (scale_ != 1.0) |
201 surface_->SetTransform(gfx::Transform()); | 199 surface_->SetTransform(gfx::Transform()); |
202 surface_->SetSurfaceDelegate(nullptr); | 200 surface_->SetSurfaceDelegate(nullptr); |
203 surface_->RemoveSurfaceObserver(this); | 201 surface_->RemoveSurfaceObserver(this); |
(...skipping 59 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
263 | 261 |
264 if (!widget_) | 262 if (!widget_) |
265 CreateShellSurfaceWidget(ui::SHOW_STATE_MAXIMIZED); | 263 CreateShellSurfaceWidget(ui::SHOW_STATE_MAXIMIZED); |
266 | 264 |
267 // Note: This will ask client to configure its surface even if already | 265 // Note: This will ask client to configure its surface even if already |
268 // maximized. | 266 // maximized. |
269 ScopedConfigure scoped_configure(this, true); | 267 ScopedConfigure scoped_configure(this, true); |
270 widget_->Maximize(); | 268 widget_->Maximize(); |
271 } | 269 } |
272 | 270 |
| 271 void ShellSurface::Minimize() { |
| 272 TRACE_EVENT0("exo", "ShellSurface::Minimize"); |
| 273 |
| 274 if (!widget_) |
| 275 return; |
| 276 |
| 277 // Note: This will ask client to configure its surface even if already |
| 278 // minimized. |
| 279 ScopedConfigure scoped_configure(this, true); |
| 280 widget_->Minimize(); |
| 281 } |
| 282 |
273 void ShellSurface::Restore() { | 283 void ShellSurface::Restore() { |
274 TRACE_EVENT0("exo", "ShellSurface::Restore"); | 284 TRACE_EVENT0("exo", "ShellSurface::Restore"); |
275 | 285 |
276 if (!widget_) | 286 if (!widget_) |
277 return; | 287 return; |
278 | 288 |
279 // Note: This will ask client to configure its surface even if not already | 289 // Note: This will ask client to configure its surface even if not already |
280 // maximized. | 290 // maximized or minimized. |
281 ScopedConfigure scoped_configure(this, true); | 291 ScopedConfigure scoped_configure(this, true); |
282 widget_->Restore(); | 292 widget_->Restore(); |
283 } | 293 } |
284 | 294 |
285 void ShellSurface::SetFullscreen(bool fullscreen) { | 295 void ShellSurface::SetFullscreen(bool fullscreen) { |
286 TRACE_EVENT1("exo", "ShellSurface::SetFullscreen", "fullscreen", fullscreen); | 296 TRACE_EVENT1("exo", "ShellSurface::SetFullscreen", "fullscreen", fullscreen); |
287 | 297 |
288 if (!widget_) | 298 if (!widget_) |
289 CreateShellSurfaceWidget(ui::SHOW_STATE_FULLSCREEN); | 299 CreateShellSurfaceWidget(ui::SHOW_STATE_FULLSCREEN); |
290 | 300 |
(...skipping 27 matching lines...) Expand all Loading... |
318 void ShellSurface::SetApplicationId(const std::string& application_id) { | 328 void ShellSurface::SetApplicationId(const std::string& application_id) { |
319 TRACE_EVENT1("exo", "ShellSurface::SetApplicationId", "application_id", | 329 TRACE_EVENT1("exo", "ShellSurface::SetApplicationId", "application_id", |
320 application_id); | 330 application_id); |
321 | 331 |
322 application_id_ = application_id; | 332 application_id_ = application_id; |
323 } | 333 } |
324 | 334 |
325 void ShellSurface::Move() { | 335 void ShellSurface::Move() { |
326 TRACE_EVENT0("exo", "ShellSurface::Move"); | 336 TRACE_EVENT0("exo", "ShellSurface::Move"); |
327 | 337 |
328 if (widget_) | 338 if (widget_ && !widget_->movement_disabled()) |
329 AttemptToStartDrag(HTCAPTION); | 339 AttemptToStartDrag(HTCAPTION); |
330 } | 340 } |
331 | 341 |
332 void ShellSurface::Resize(int component) { | 342 void ShellSurface::Resize(int component) { |
333 TRACE_EVENT1("exo", "ShellSurface::Resize", "component", component); | 343 TRACE_EVENT1("exo", "ShellSurface::Resize", "component", component); |
334 | 344 |
335 if (widget_) | 345 if (widget_ && !widget_->movement_disabled()) |
336 AttemptToStartDrag(component); | 346 AttemptToStartDrag(component); |
337 } | 347 } |
338 | 348 |
339 void ShellSurface::Close() { | 349 void ShellSurface::Close() { |
340 if (!close_callback_.is_null()) | 350 if (!close_callback_.is_null()) |
341 close_callback_.Run(); | 351 close_callback_.Run(); |
342 } | 352 } |
343 | 353 |
344 void ShellSurface::SetGeometry(const gfx::Rect& geometry) { | 354 void ShellSurface::SetGeometry(const gfx::Rect& geometry) { |
345 TRACE_EVENT1("exo", "ShellSurface::SetGeometry", "geometry", | 355 TRACE_EVENT1("exo", "ShellSurface::SetGeometry", "geometry", |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
436 | 446 |
437 // Update surface scale. | 447 // Update surface scale. |
438 if (pending_scale_ != scale_) { | 448 if (pending_scale_ != scale_) { |
439 gfx::Transform transform; | 449 gfx::Transform transform; |
440 DCHECK_NE(pending_scale_, 0.0); | 450 DCHECK_NE(pending_scale_, 0.0); |
441 transform.Scale(1.0 / pending_scale_, 1.0 / pending_scale_); | 451 transform.Scale(1.0 / pending_scale_, 1.0 / pending_scale_); |
442 surface_->SetTransform(transform); | 452 surface_->SetTransform(transform); |
443 scale_ = pending_scale_; | 453 scale_ = pending_scale_; |
444 } | 454 } |
445 | 455 |
446 // Show widget if not already visible. | 456 // Show widget if needed. |
447 if (!widget_->IsClosed() && !widget_->IsVisible()) | 457 if (pending_show_widget_) { |
| 458 DCHECK(!widget_->IsClosed()); |
| 459 DCHECK(!widget_->IsVisible()); |
| 460 pending_show_widget_ = false; |
448 widget_->Show(); | 461 widget_->Show(); |
| 462 } |
449 } | 463 } |
450 } | 464 } |
451 | 465 |
452 bool ShellSurface::IsSurfaceSynchronized() const { | 466 bool ShellSurface::IsSurfaceSynchronized() const { |
453 // A shell surface is always desynchronized. | 467 // A shell surface is always desynchronized. |
454 return false; | 468 return false; |
455 } | 469 } |
456 | 470 |
457 //////////////////////////////////////////////////////////////////////////////// | 471 //////////////////////////////////////////////////////////////////////////////// |
458 // SurfaceObserver overrides: | 472 // SurfaceObserver overrides: |
(...skipping 14 matching lines...) Expand all Loading... |
473 // Note: In its use in the Wayland server implementation, the surface | 487 // Note: In its use in the Wayland server implementation, the surface |
474 // destroyed callback may destroy the ShellSurface instance. This call needs | 488 // destroyed callback may destroy the ShellSurface instance. This call needs |
475 // to be last so that the instance can be destroyed. | 489 // to be last so that the instance can be destroyed. |
476 if (!surface_destroyed_callback_.is_null()) | 490 if (!surface_destroyed_callback_.is_null()) |
477 surface_destroyed_callback_.Run(); | 491 surface_destroyed_callback_.Run(); |
478 } | 492 } |
479 | 493 |
480 //////////////////////////////////////////////////////////////////////////////// | 494 //////////////////////////////////////////////////////////////////////////////// |
481 // views::WidgetDelegate overrides: | 495 // views::WidgetDelegate overrides: |
482 | 496 |
483 bool ShellSurface::CanMaximize() const { | 497 bool ShellSurface::CanResize() const { |
484 return resizeable_; | 498 return initial_bounds_.IsEmpty(); |
485 } | 499 } |
486 | 500 |
487 bool ShellSurface::CanResize() const { | 501 bool ShellSurface::CanMaximize() const { |
488 return resizeable_; | 502 return true; |
| 503 } |
| 504 |
| 505 bool ShellSurface::CanMinimize() const { |
| 506 return true; |
489 } | 507 } |
490 | 508 |
491 base::string16 ShellSurface::GetWindowTitle() const { | 509 base::string16 ShellSurface::GetWindowTitle() const { |
492 return title_; | 510 return title_; |
493 } | 511 } |
494 | 512 |
495 void ShellSurface::WindowClosing() { | 513 void ShellSurface::WindowClosing() { |
496 if (resizer_) | 514 if (resizer_) |
497 EndDrag(true /* revert */); | 515 EndDrag(true /* revert */); |
498 SetEnabled(false); | 516 SetEnabled(false); |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
546 ash::wm::WindowStateType new_type = window_state->GetStateType(); | 564 ash::wm::WindowStateType new_type = window_state->GetStateType(); |
547 if (old_type == ash::wm::WINDOW_STATE_TYPE_MAXIMIZED || | 565 if (old_type == ash::wm::WINDOW_STATE_TYPE_MAXIMIZED || |
548 new_type == ash::wm::WINDOW_STATE_TYPE_MAXIMIZED || | 566 new_type == ash::wm::WINDOW_STATE_TYPE_MAXIMIZED || |
549 old_type == ash::wm::WINDOW_STATE_TYPE_FULLSCREEN || | 567 old_type == ash::wm::WINDOW_STATE_TYPE_FULLSCREEN || |
550 new_type == ash::wm::WINDOW_STATE_TYPE_FULLSCREEN) { | 568 new_type == ash::wm::WINDOW_STATE_TYPE_FULLSCREEN) { |
551 Configure(); | 569 Configure(); |
552 } | 570 } |
553 | 571 |
554 if (widget_) | 572 if (widget_) |
555 UpdateWidgetBounds(); | 573 UpdateWidgetBounds(); |
| 574 |
| 575 if (!state_changed_callback_.is_null()) |
| 576 state_changed_callback_.Run(old_type, new_type); |
556 } | 577 } |
557 | 578 |
558 //////////////////////////////////////////////////////////////////////////////// | 579 //////////////////////////////////////////////////////////////////////////////// |
559 // aura::WindowObserver overrides: | 580 // aura::WindowObserver overrides: |
560 | 581 |
561 void ShellSurface::OnWindowBoundsChanged(aura::Window* window, | 582 void ShellSurface::OnWindowBoundsChanged(aura::Window* window, |
562 const gfx::Rect& old_bounds, | 583 const gfx::Rect& old_bounds, |
563 const gfx::Rect& new_bounds) { | 584 const gfx::Rect& new_bounds) { |
564 if (!widget_ || !surface_ || ignore_window_bounds_changes_) | 585 if (!widget_ || !surface_ || ignore_window_bounds_changes_) |
565 return; | 586 return; |
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
683 | 704 |
684 views::Widget::InitParams params; | 705 views::Widget::InitParams params; |
685 params.type = views::Widget::InitParams::TYPE_WINDOW; | 706 params.type = views::Widget::InitParams::TYPE_WINDOW; |
686 params.ownership = views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET; | 707 params.ownership = views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET; |
687 params.delegate = this; | 708 params.delegate = this; |
688 params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; | 709 params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; |
689 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; | 710 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; |
690 params.show_state = show_state; | 711 params.show_state = show_state; |
691 params.parent = | 712 params.parent = |
692 ash::Shell::GetContainer(ash::Shell::GetPrimaryRootWindow(), container_); | 713 ash::Shell::GetContainer(ash::Shell::GetPrimaryRootWindow(), container_); |
693 if (!initial_bounds_.IsEmpty()) { | 714 params.bounds = initial_bounds_; |
694 params.bounds = initial_bounds_; | |
695 if (parent_) { | |
696 aura::Window::ConvertRectToTarget(GetMainSurface(parent_), params.parent, | |
697 ¶ms.bounds); | |
698 } | |
699 } | |
700 bool activatable = activatable_ && !surface_->GetHitTestBounds().IsEmpty(); | 715 bool activatable = activatable_ && !surface_->GetHitTestBounds().IsEmpty(); |
701 params.activatable = activatable ? views::Widget::InitParams::ACTIVATABLE_YES | 716 params.activatable = activatable ? views::Widget::InitParams::ACTIVATABLE_YES |
702 : views::Widget::InitParams::ACTIVATABLE_NO; | 717 : views::Widget::InitParams::ACTIVATABLE_NO; |
703 | 718 |
704 // Note: NativeWidget owns this widget. | 719 // Note: NativeWidget owns this widget. |
705 widget_ = new ShellSurfaceWidget(this); | 720 widget_ = new ShellSurfaceWidget(this); |
706 widget_->Init(params); | 721 widget_->Init(params); |
707 | 722 |
| 723 // Disable movement if initial bounds were specified. |
| 724 widget_->set_movement_disabled(!initial_bounds_.IsEmpty()); |
| 725 |
708 aura::Window* window = widget_->GetNativeWindow(); | 726 aura::Window* window = widget_->GetNativeWindow(); |
709 window->SetName("ExoShellSurface"); | 727 window->SetName("ExoShellSurface"); |
710 window->AddChild(surface_); | 728 window->AddChild(surface_); |
711 window->SetEventTargeter(base::WrapUnique(new CustomWindowTargeter)); | 729 window->SetEventTargeter(base::WrapUnique(new CustomWindowTargeter)); |
712 SetApplicationId(window, &application_id_); | 730 SetApplicationId(window, &application_id_); |
713 SetMainSurface(window, surface_); | 731 SetMainSurface(window, surface_); |
714 | 732 |
715 // Start tracking changes to window bounds and window state. | 733 // Start tracking changes to window bounds and window state. |
716 window->AddObserver(this); | 734 window->AddObserver(this); |
717 ash::wm::GetWindowState(window)->AddObserver(this); | 735 ash::wm::GetWindowState(window)->AddObserver(this); |
718 | 736 |
719 // Make shell surface a transient child if |parent_| has been set. | 737 // Make shell surface a transient child if |parent_| has been set. |
720 if (parent_) | 738 if (parent_) |
721 wm::AddTransientChild(parent_, window); | 739 wm::AddTransientChild(parent_, window); |
722 | 740 |
723 // Allow Ash to manage the position of a top-level shell surfaces if show | 741 // Allow Ash to manage the position of a top-level shell surfaces if show |
724 // state is one that allows auto positioning and |initial_bounds_| has | 742 // state is one that allows auto positioning and |initial_bounds_| has |
725 // not been set. | 743 // not been set. |
726 ash::wm::GetWindowState(window)->set_window_position_managed( | 744 ash::wm::GetWindowState(window)->set_window_position_managed( |
727 ash::wm::ToWindowShowState(ash::wm::WINDOW_STATE_TYPE_AUTO_POSITIONED) == | 745 ash::wm::ToWindowShowState(ash::wm::WINDOW_STATE_TYPE_AUTO_POSITIONED) == |
728 show_state && | 746 show_state && |
729 initial_bounds_.IsEmpty()); | 747 initial_bounds_.IsEmpty()); |
| 748 |
| 749 // Show widget next time Commit() is called. |
| 750 pending_show_widget_ = true; |
730 } | 751 } |
731 | 752 |
732 void ShellSurface::Configure() { | 753 void ShellSurface::Configure() { |
733 DCHECK(widget_); | 754 DCHECK(widget_); |
734 | 755 |
735 // Delay configure callback if |scoped_configure_| is set. | 756 // Delay configure callback if |scoped_configure_| is set. |
736 if (scoped_configure_) { | 757 if (scoped_configure_) { |
737 scoped_configure_->set_needs_configure(); | 758 scoped_configure_->set_needs_configure(); |
738 return; | 759 return; |
739 } | 760 } |
(...skipping 130 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
870 ash::WindowResizer::kBoundsChange_Resizes; | 891 ash::WindowResizer::kBoundsChange_Resizes; |
871 } | 892 } |
872 | 893 |
873 gfx::Rect ShellSurface::GetVisibleBounds() const { | 894 gfx::Rect ShellSurface::GetVisibleBounds() const { |
874 // Use |geometry_| if set, otherwise use the visual bounds of the surface. | 895 // Use |geometry_| if set, otherwise use the visual bounds of the surface. |
875 return geometry_.IsEmpty() ? gfx::Rect(surface_->layer()->size()) : geometry_; | 896 return geometry_.IsEmpty() ? gfx::Rect(surface_->layer()->size()) : geometry_; |
876 } | 897 } |
877 | 898 |
878 gfx::Point ShellSurface::GetSurfaceOrigin() const { | 899 gfx::Point ShellSurface::GetSurfaceOrigin() const { |
879 gfx::Rect window_bounds = widget_->GetWindowBoundsInScreen(); | 900 gfx::Rect window_bounds = widget_->GetWindowBoundsInScreen(); |
| 901 |
| 902 // If initial bounds were specified then surface origin is always relative |
| 903 // to those bounds. |
| 904 if (!initial_bounds_.IsEmpty()) |
| 905 return initial_bounds_.origin() - window_bounds.OffsetFromOrigin(); |
| 906 |
880 gfx::Rect visible_bounds = GetVisibleBounds(); | 907 gfx::Rect visible_bounds = GetVisibleBounds(); |
881 | |
882 switch (resize_component_) { | 908 switch (resize_component_) { |
883 case HTCAPTION: | 909 case HTCAPTION: |
884 return origin_ - visible_bounds.OffsetFromOrigin(); | 910 return origin_ - visible_bounds.OffsetFromOrigin(); |
885 case HTBOTTOM: | 911 case HTBOTTOM: |
886 case HTRIGHT: | 912 case HTRIGHT: |
887 case HTBOTTOMRIGHT: | 913 case HTBOTTOMRIGHT: |
888 return gfx::Point() - visible_bounds.OffsetFromOrigin(); | 914 return gfx::Point() - visible_bounds.OffsetFromOrigin(); |
889 case HTTOP: | 915 case HTTOP: |
890 case HTTOPRIGHT: | 916 case HTTOPRIGHT: |
891 return gfx::Point(0, window_bounds.height() - visible_bounds.height()) - | 917 return gfx::Point(0, window_bounds.height() - visible_bounds.height()) - |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
937 DCHECK(!ignore_window_bounds_changes_); | 963 DCHECK(!ignore_window_bounds_changes_); |
938 ignore_window_bounds_changes_ = true; | 964 ignore_window_bounds_changes_ = true; |
939 widget_->SetBounds(new_widget_bounds); | 965 widget_->SetBounds(new_widget_bounds); |
940 ignore_window_bounds_changes_ = false; | 966 ignore_window_bounds_changes_ = false; |
941 | 967 |
942 // A change to the widget size requires surface bounds to be re-adjusted. | 968 // A change to the widget size requires surface bounds to be re-adjusted. |
943 surface_->SetBounds(gfx::Rect(GetSurfaceOrigin(), surface_->layer()->size())); | 969 surface_->SetBounds(gfx::Rect(GetSurfaceOrigin(), surface_->layer()->size())); |
944 } | 970 } |
945 | 971 |
946 } // namespace exo | 972 } // namespace exo |
OLD | NEW |