Commit 2af0583b authored by Xiaoqian Dai's avatar Xiaoqian Dai Committed by Commit Bot

capture mode: window capture change.

In window capture mode, the user should be able to interactively select
a window to capture. when the to-be-captured window's bounds or state is
changed, we should update the capture region to reflect the change as
well.

Other changes that is worth mentioning in this CL:
- Window stacking order or activation order will not be changed by the
capture window selection change
- If current selected is closed/minimized or becomes invisible, we
automatically select another window under mouse/touch event location.
and if there is no such window, do nothing.
- If another window is activated when a window is selected, if the
activated window is under mouse/touch event location, it will be focused
instead.
- When a mouse moved over a selectable window, change its cursor to
either capture icon or recording icon

Bug: 1127526
Change-Id: Icfdbed419dadfe0301b60da693f01010f4a2da2c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2421090Reviewed-by: default avatarAhmed Fakhry <afakhry@chromium.org>
Reviewed-by: default avatarAlex Newcomer <newcomer@chromium.org>
Commit-Queue: Xiaoqian Dai <xdai@chromium.org>
Cr-Commit-Position: refs/heads/master@{#812976}
parent c35c69ae
...@@ -275,6 +275,8 @@ component("ash") { ...@@ -275,6 +275,8 @@ component("ash") {
"capture_mode/capture_mode_type_view.cc", "capture_mode/capture_mode_type_view.cc",
"capture_mode/capture_mode_type_view.h", "capture_mode/capture_mode_type_view.h",
"capture_mode/capture_mode_types.h", "capture_mode/capture_mode_types.h",
"capture_mode/capture_window_observer.cc",
"capture_mode/capture_window_observer.h",
"capture_mode/stop_recording_button_tray.cc", "capture_mode/stop_recording_button_tray.cc",
"capture_mode/stop_recording_button_tray.h", "capture_mode/stop_recording_button_tray.h",
"capture_mode/view_with_ink_drop.h", "capture_mode/view_with_ink_drop.h",
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "ash/capture_mode/capture_mode_bar_view.h" #include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_controller.h" #include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_window_observer.h"
#include "ash/display/mouse_cursor_event_filter.h" #include "ash/display/mouse_cursor_event_filter.h"
#include "ash/public/cpp/shell_window_ids.h" #include "ash/public/cpp/shell_window_ids.h"
#include "ash/resources/vector_icons/vector_icons.h" #include "ash/resources/vector_icons/vector_icons.h"
...@@ -31,6 +32,7 @@ ...@@ -31,6 +32,7 @@
#include "ui/views/background.h" #include "ui/views/background.h"
#include "ui/views/controls/button/label_button.h" #include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/label.h" #include "ui/views/controls/label.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash { namespace ash {
...@@ -168,6 +170,11 @@ CaptureModeSession::CaptureModeSession(CaptureModeController* controller, ...@@ -168,6 +170,11 @@ CaptureModeSession::CaptureModeSession(CaptureModeController* controller,
capture_mode_bar_widget_.Show(); capture_mode_bar_widget_.Show();
RefreshStackingOrder(parent); RefreshStackingOrder(parent);
if (controller_->source() == CaptureModeSource::kWindow) {
capture_window_observer_ =
std::make_unique<CaptureWindowObserver>(this, controller_->type());
}
} }
CaptureModeSession::~CaptureModeSession() { CaptureModeSession::~CaptureModeSession() {
...@@ -176,15 +183,18 @@ CaptureModeSession::~CaptureModeSession() { ...@@ -176,15 +183,18 @@ CaptureModeSession::~CaptureModeSession() {
} }
aura::Window* CaptureModeSession::GetSelectedWindow() const { aura::Window* CaptureModeSession::GetSelectedWindow() const {
// Note that the capture bar widget is activatable, so we can't use return capture_window_observer_ ? capture_window_observer_->window()
// window_util::GetActiveWindow(). Instead, we use the MRU window tracker and : nullptr;
// get the top-most window if any.
auto mru_windows =
Shell::Get()->mru_window_tracker()->BuildMruWindowList(kActiveDesk);
return mru_windows.empty() ? nullptr : mru_windows[0];
} }
void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) { void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) {
if (new_source == CaptureModeSource::kWindow) {
capture_window_observer_ =
std::make_unique<CaptureWindowObserver>(this, controller_->type());
} else {
capture_window_observer_.reset();
}
capture_mode_bar_view_->OnCaptureSourceChanged(new_source); capture_mode_bar_view_->OnCaptureSourceChanged(new_source);
SetMouseWarpEnabled(new_source != CaptureModeSource::kRegion); SetMouseWarpEnabled(new_source != CaptureModeSource::kRegion);
UpdateCaptureRegionWidgets(); UpdateCaptureRegionWidgets();
...@@ -192,6 +202,8 @@ void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) { ...@@ -192,6 +202,8 @@ void CaptureModeSession::OnCaptureSourceChanged(CaptureModeSource new_source) {
} }
void CaptureModeSession::OnCaptureTypeChanged(CaptureModeType new_type) { void CaptureModeSession::OnCaptureTypeChanged(CaptureModeType new_type) {
if (controller_->source() == CaptureModeSource::kWindow)
capture_window_observer_->OnCaptureTypeChanged(new_type);
capture_mode_bar_view_->OnCaptureTypeChanged(new_type); capture_mode_bar_view_->OnCaptureTypeChanged(new_type);
} }
...@@ -330,13 +342,45 @@ void CaptureModeSession::PaintCaptureRegion(gfx::Canvas* canvas) { ...@@ -330,13 +342,45 @@ void CaptureModeSession::PaintCaptureRegion(gfx::Canvas* canvas) {
void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event, void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
bool is_touch) { bool is_touch) {
// No need to handle events if the current source is not region. // No need to handle events if the current source is kFullscreen.
if (controller_->source() != CaptureModeSource::kRegion) const CaptureModeSource capture_source = controller_->source();
if (capture_source == CaptureModeSource::kFullscreen)
return; return;
gfx::Point location = event->location(); gfx::Point location = event->location();
aura::Window* source = static_cast<aura::Window*>(event->target()); aura::Window* source = static_cast<aura::Window*>(event->target());
aura::Window::ConvertPointToTarget(source, current_root_, &location); aura::Window::ConvertPointToTarget(source, current_root_, &location);
const bool is_event_on_capture_bar =
CaptureModeBarView::GetBounds(current_root_).Contains(location);
if (capture_source == CaptureModeSource::kWindow) {
// Do not handle any event located on the capture mode bar.
if (is_event_on_capture_bar)
return;
event->SetHandled();
event->StopPropagation();
switch (event->type()) {
case ui::ET_MOUSE_MOVED:
case ui::ET_TOUCH_PRESSED:
case ui::ET_TOUCH_MOVED: {
gfx::Point screen_location(event->location());
::wm::ConvertPointToScreen(source, &screen_location);
capture_window_observer_->UpdateSelectedWindowAtPosition(
screen_location);
break;
}
case ui::ET_MOUSE_RELEASED:
case ui::ET_TOUCH_RELEASED:
if (GetSelectedWindow())
controller_->PerformCapture();
break;
default:
break;
}
return;
}
// Let the capture button handle any events within its bounds. // Let the capture button handle any events within its bounds.
if (capture_button_widget_ && if (capture_button_widget_ &&
...@@ -346,7 +390,7 @@ void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event, ...@@ -346,7 +390,7 @@ void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
// Allow events that are located on the capture mode bar to pass through so we // Allow events that are located on the capture mode bar to pass through so we
// can click the buttons. // can click the buttons.
if (!CaptureModeBarView::GetBounds(current_root_).Contains(location)) { if (!is_event_on_capture_bar) {
event->SetHandled(); event->SetHandled();
event->StopPropagation(); event->StopPropagation();
} }
......
...@@ -24,6 +24,7 @@ namespace ash { ...@@ -24,6 +24,7 @@ namespace ash {
class CaptureModeBarView; class CaptureModeBarView;
class CaptureModeController; class CaptureModeController;
class CaptureWindowObserver;
// Encapsulates an active capture mode session (i.e. an instance of this class // Encapsulates an active capture mode session (i.e. an instance of this class
// lives as long as capture mode is active). It creates and owns the capture // lives as long as capture mode is active). It creates and owns the capture
...@@ -170,6 +171,9 @@ class ASH_EXPORT CaptureModeSession : public ui::LayerOwner, ...@@ -170,6 +171,9 @@ class ASH_EXPORT CaptureModeSession : public ui::LayerOwner,
// Caches the old status of mouse warping before the session started to be // Caches the old status of mouse warping before the session started to be
// restored at the end. // restored at the end.
bool old_mouse_warp_status_; bool old_mouse_warp_status_;
// Observer to observe the current selected to-be-captured window.
std::unique_ptr<CaptureWindowObserver> capture_window_observer_;
}; };
} // namespace ash } // namespace ash
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#include <memory>
#include "ash/capture_mode/capture_mode_bar_view.h" #include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_close_button.h" #include "ash/capture_mode/capture_mode_close_button.h"
#include "ash/capture_mode/capture_mode_controller.h" #include "ash/capture_mode/capture_mode_controller.h"
...@@ -18,6 +20,7 @@ ...@@ -18,6 +20,7 @@
#include "ash/shell.h" #include "ash/shell.h"
#include "ash/system/status_area_widget.h" #include "ash/system/status_area_widget.h"
#include "ash/test/ash_test_base.h" #include "ash/test/ash_test_base.h"
#include "ash/wm/window_state.h"
#include "base/test/scoped_feature_list.h" #include "base/test/scoped_feature_list.h"
#include "ui/events/keycodes/keyboard_codes_posix.h" #include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/insets.h" #include "ui/gfx/geometry/insets.h"
...@@ -497,4 +500,38 @@ TEST_F(CaptureModeTest, DimensionsLabelLocation) { ...@@ -497,4 +500,38 @@ TEST_F(CaptureModeTest, DimensionsLabelLocation) {
dimensions_label_window->bounds().bottom()); dimensions_label_window->bounds().bottom());
} }
TEST_F(CaptureModeTest, WindowCapture) {
// Create 2 windows that overlap with each other.
const gfx::Rect bounds1(0, 0, 200, 200);
std::unique_ptr<aura::Window> window1(CreateTestWindow(bounds1));
const gfx::Rect bounds2(150, 150, 200, 200);
std::unique_ptr<aura::Window> window2(CreateTestWindow(bounds2));
auto* controller = CaptureModeController::Get();
controller->SetSource(CaptureModeSource::kWindow);
controller->SetType(CaptureModeType::kImage);
controller->Start();
EXPECT_TRUE(controller->IsActive());
auto* event_generator = GetEventGenerator();
event_generator->MoveMouseToCenterOf(window1.get());
auto* capture_mode_session = controller->capture_mode_session();
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get());
event_generator->MoveMouseToCenterOf(window2.get());
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window2.get());
// Now move the mouse to the overlapped area.
event_generator->MoveMouseTo(gfx::Point(175, 175));
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window2.get());
// Close the current selected window should automatically focus to next one.
window2.reset();
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get());
// Open another one on top also change the selected window.
std::unique_ptr<aura::Window> window3(CreateTestWindow(bounds2));
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window3.get());
// Minimize the window should also automatically change the selected window.
WindowState::Get(window3.get())->Minimize();
EXPECT_EQ(capture_mode_session->GetSelectedWindow(), window1.get());
}
} // namespace ash } // namespace ash
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/capture_mode/capture_window_observer.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/window_finder.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ui/base/cursor/cursor_factory.h"
#include "ui/base/cursor/cursor_util.h"
#include "ui/display/screen.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
namespace ash {
CaptureWindowObserver::CaptureWindowObserver(
CaptureModeSession* capture_mode_session,
CaptureModeType type)
: capture_type_(type),
original_cursor_(Shell::Get()->cursor_manager()->GetCursor()),
capture_mode_session_(capture_mode_session) {
Shell::Get()->activation_client()->AddObserver(this);
}
CaptureWindowObserver::~CaptureWindowObserver() {
auto* shell = Shell::Get();
shell->activation_client()->RemoveObserver(this);
StopObserving();
::wm::CursorManager* cursor_manager = shell->cursor_manager();
if (is_cursor_locked_) {
cursor_manager->UnlockCursor();
cursor_manager->SetCursor(original_cursor_);
is_cursor_locked_ = false;
}
}
void CaptureWindowObserver::UpdateSelectedWindowAtPosition(
const gfx::Point& location_in_screen) {
UpdateSelectedWindowAtPosition(location_in_screen, /*ignore_windows=*/{});
}
void CaptureWindowObserver::OnCaptureTypeChanged(CaptureModeType new_type) {
capture_type_ = new_type;
UpdateMouseCursor();
}
void CaptureWindowObserver::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
DCHECK_EQ(window, window_);
RepaintCaptureRegion();
}
void CaptureWindowObserver::OnWindowVisibilityChanging(aura::Window* window,
bool visible) {
DCHECK_EQ(window, window_);
DCHECK(!visible);
StopObserving();
UpdateSelectedWindowAtPosition(location_in_screen_,
/*ignore_windows=*/{window});
}
void CaptureWindowObserver::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(window, window_);
StopObserving();
UpdateSelectedWindowAtPosition(location_in_screen_,
/*ignore_windows=*/{window});
}
void CaptureWindowObserver::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
// If another window is activated on top of the current selected window, we
// may change the selected window to the activated window if it's under the
// current event location. If there is no selected window at the moment, we
// also want to check if new activated window should be focused.
UpdateSelectedWindowAtPosition(location_in_screen_, /*ignore_windows=*/{});
}
void CaptureWindowObserver::StartObserving(aura::Window* window) {
window_ = window;
window_->AddObserver(this);
}
void CaptureWindowObserver::StopObserving() {
if (window_) {
window_->RemoveObserver(this);
window_ = nullptr;
}
}
void CaptureWindowObserver::UpdateSelectedWindowAtPosition(
const gfx::Point& location_in_screen,
const std::set<aura::Window*>& ignore_windows) {
location_in_screen_ = location_in_screen;
// Find the toplevel window under the mouse/touch position.
aura::Window* window =
GetTopmostWindowAtPoint(location_in_screen_, ignore_windows);
if (window_ == window)
return;
// Don't capture wallpaper window.
if (window && window->parent() &&
window->parent()->id() == kShellWindowId_WallpaperContainer) {
window = nullptr;
}
// Stop observing the current selected window if there is one.
aura::Window* previous_selected_window = window_;
StopObserving();
if (window)
StartObserving(window);
RepaintCaptureRegion();
// Change mouse cursor depending on capture type and capture window if
// applicable.
const bool should_update_cursor =
!previous_selected_window != !window_ &&
Shell::Get()->cursor_manager()->IsCursorVisible();
if (should_update_cursor)
UpdateMouseCursor();
}
void CaptureWindowObserver::RepaintCaptureRegion() {
ui::Layer* layer = capture_mode_session_->layer();
layer->SchedulePaint(layer->bounds());
}
void CaptureWindowObserver::UpdateMouseCursor() {
::wm::CursorManager* cursor_manager = Shell::Get()->cursor_manager();
if (window_) {
// Change the mouse cursor to a capture icon or a recording icon.
ui::Cursor cursor(ui::mojom::CursorType::kCustom);
const display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window_);
const float device_scale_factor = display.device_scale_factor();
// TODO: Adjust the icon color after spec is updated.
const gfx::ImageSkia icon = gfx::CreateVectorIcon(
capture_type_ == CaptureModeType::kImage ? kCaptureModeImageIcon
: kCaptureModeVideoIcon,
SK_ColorBLACK);
SkBitmap bitmap = *icon.bitmap();
gfx::Point hotspot(bitmap.width() / 2, bitmap.height() / 2);
ui::ScaleAndRotateCursorBitmapAndHotpoint(
device_scale_factor, display.panel_rotation(), &bitmap, &hotspot);
auto* cursor_factory = ui::CursorFactory::GetInstance();
ui::PlatformCursor platform_cursor =
cursor_factory->CreateImageCursor(bitmap, hotspot);
cursor.SetPlatformCursor(platform_cursor);
cursor.set_custom_bitmap(bitmap);
cursor.set_custom_hotspot(hotspot);
cursor_factory->UnrefImageCursor(platform_cursor);
// Unlock the cursor first so that it can be changed.
if (is_cursor_locked_)
cursor_manager->UnlockCursor();
cursor_manager->SetCursor(cursor);
cursor_manager->LockCursor();
is_cursor_locked_ = true;
} else {
// Revert back to its previous mouse cursor setting.
if (is_cursor_locked_) {
cursor_manager->UnlockCursor();
is_cursor_locked_ = false;
}
cursor_manager->SetCursor(original_cursor_);
}
}
} // namespace ash
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_CAPTURE_MODE_CAPTURE_WINDOW_OBSERVER_H_
#define ASH_CAPTURE_MODE_CAPTURE_WINDOW_OBSERVER_H_
#include <set>
#include "ash/ash_export.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ui/aura/window_observer.h"
#include "ui/base/cursor/cursor.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/wm/public/activation_change_observer.h"
namespace aura {
class Window;
} // namespace aura
namespace ash {
class CaptureModeSession;
// Class to observe the current selected to-be-captured window and update the
// capture region if applicable.
class ASH_EXPORT CaptureWindowObserver : public aura::WindowObserver,
public ::wm::ActivationChangeObserver {
public:
CaptureWindowObserver(CaptureModeSession* capture_mode_session,
CaptureModeType type);
CaptureWindowObserver(const CaptureWindowObserver&) = delete;
CaptureWindowObserver& operator=(const CaptureWindowObserver&) = delete;
~CaptureWindowObserver() override;
// Updates selected window depending on the mouse/touch event location. If
// there is an eligible window under the current mouse/touch event location,
// its bounds will be highlighted.
void UpdateSelectedWindowAtPosition(const gfx::Point& location_in_screen);
// Called when capture type changes. The mouse cursor image may update
// accordingly.
void OnCaptureTypeChanged(CaptureModeType new_type);
// aura::WindowObserver:
void OnWindowBoundsChanged(aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) override;
void OnWindowVisibilityChanging(aura::Window* window, bool visible) override;
void OnWindowDestroying(aura::Window* window) override;
// ::wm::ActivationChangeObserver:
void OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) override;
aura::Window* window() { return window_; }
private:
void StartObserving(aura::Window* window);
void StopObserving();
// Updates selected window depending on the mouse/touch event location with
// ignoring |ignore_windows|.
void UpdateSelectedWindowAtPosition(
const gfx::Point& location_in_screen,
const std::set<aura::Window*>& ignore_windows);
// Repaints the window capture region.
void RepaintCaptureRegion();
// Updates the mouse cursor to change it to a capture or record icon when the
// mouse hovers over an eligible window.
void UpdateMouseCursor();
// Current observed window.
aura::Window* window_ = nullptr;
// Stores current mouse or touch location in screen coordinate.
gfx::Point location_in_screen_;
// Current capture type.
CaptureModeType capture_type_;
// True if the current cursor is locked by this.
bool is_cursor_locked_ = false;
const gfx::NativeCursor original_cursor_;
// Pointer to current capture session. Not nullptr during this lifecycle.
CaptureModeSession* const capture_mode_session_;
};
} // namespace ash
#endif // ASH_CAPTURE_MODE_CAPTURE_WINDOW_OBSERVER_H_
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "ash/public/cpp/holding_space/holding_space_model_observer.h" #include "ash/public/cpp/holding_space/holding_space_model_observer.h"
#include "base/scoped_observer.h" #include "base/scoped_observer.h"
#include "base/test/bind_test_util.h" #include "base/test/bind_test_util.h"
#include "chrome/browser/ui/browser_window.h"
#include "content/public/test/browser_test.h" #include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h" #include "testing/gmock/include/gmock/gmock.h"
#include "ui/aura/window.h" #include "ui/aura/window.h"
...@@ -266,6 +267,14 @@ IN_PROC_BROWSER_TEST_P(HoldingSpaceUiScreenshotBrowserTest, AddScreenshot) { ...@@ -266,6 +267,14 @@ IN_PROC_BROWSER_TEST_P(HoldingSpaceUiScreenshotBrowserTest, AddScreenshot) {
// Otherwise the screenshot will be taken using the `ChromeScreenshotGrabber`. // Otherwise the screenshot will be taken using the `ChromeScreenshotGrabber`.
PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1, PressAndReleaseKey(ui::VKEY_MEDIA_LAUNCH_APP1,
ui::EF_ALT_DOWN | ui::EF_CONTROL_DOWN); ui::EF_ALT_DOWN | ui::EF_CONTROL_DOWN);
// Move the mouse over to the browser window. The reason for that is with
// `features::kCaptureMode` enabled, the new capture mode implementation will
// not automatically capture the topmost window unless the mouse is hovered
// above it.
aura::Window* browser_window = browser()->window()->GetNativeWindow();
ui::test::EventGenerator event_generator(browser_window->GetRootWindow());
event_generator.MoveMouseTo(
browser_window->GetBoundsInScreen().CenterPoint());
PressAndReleaseKey(ui::VKEY_RETURN); PressAndReleaseKey(ui::VKEY_RETURN);
// Bind an observer to watch for updates to the holding space model. // Bind an observer to watch for updates to the holding space model.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment