Commit 5b2aedd6 authored by sadrul's avatar sadrul Committed by Commit bot

mus: Allow the WM to specify the windows that can have active children.

. The sample WindowManager creates various containers, and marks the container
  for user windows to be allowed to have active windows. Other containers may
  also be allowed to have active children in the future.

. For mandoline, the BrowserWindow (which is the WM) marks the root as being
  able to contain active windows. Consequently, the browser window can be
  properly activated.

. Raise a Window to the top when it becomes active.

BUG=548422

Review URL: https://codereview.chromium.org/1459463004

Cr-Commit-Position: refs/heads/master@{#361274}
parent 5010e358
......@@ -87,6 +87,7 @@ bool WindowServerTestBase::ConfigureIncomingConnection(
void WindowServerTestBase::OnEmbed(Window* root) {
most_recent_connection_ = root->connection();
EXPECT_TRUE(QuitRunLoop());
host_->AddActivationParent(root->id());
}
void WindowServerTestBase::OnConnectionLost(WindowTreeConnection* connection) {
......
......@@ -45,6 +45,7 @@ class WindowServerTestBase
WindowTreeConnection* window_manager() { return window_manager_; }
protected:
mojom::WindowTreeHost* host() { return host_.get(); }
WindowTreeConnection* most_recent_connection() {
return most_recent_connection_;
}
......
......@@ -27,6 +27,10 @@ interface WindowTreeHost {
// easily identify the accelerator's action.
AddAccelerator(uint32 id, EventMatcher matcher);
RemoveAccelerator(uint32 id);
// Enables (or disables) child windows of |window_id| to be activated.
AddActivationParent(uint32 window_id);
RemoveActivationParent(uint32 window_id);
};
interface WindowTreeHostClient {
......
......@@ -27,6 +27,7 @@ source_set("lib") {
"event_dispatcher_delegate.h",
"focus_controller.cc",
"focus_controller.h",
"focus_controller_delegate.h",
"focus_controller_observer.h",
"forwarding_window_manager.cc",
"forwarding_window_manager.h",
......
......@@ -4,6 +4,7 @@
#include "components/mus/ws/focus_controller.h"
#include "components/mus/ws/focus_controller_delegate.h"
#include "components/mus/ws/focus_controller_observer.h"
#include "components/mus/ws/server_window.h"
#include "components/mus/ws/server_window_drawn_tracker.h"
......@@ -11,7 +12,8 @@
namespace mus {
namespace ws {
FocusController::FocusController() {}
FocusController::FocusController(FocusControllerDelegate* delegate)
: delegate_(delegate), focused_window_(nullptr), active_window_(nullptr) {}
FocusController::~FocusController() {}
......@@ -23,7 +25,7 @@ void FocusController::SetFocusedWindow(ServerWindow* window) {
}
ServerWindow* FocusController::GetFocusedWindow() {
return drawn_tracker_ ? drawn_tracker_->window() : nullptr;
return focused_window_;
}
void FocusController::AddObserver(FocusControllerObserver* observer) {
......@@ -44,42 +46,73 @@ bool FocusController::CanBeFocused(ServerWindow* window) const {
}
// |window| must be a descendent of an activatable window.
for (ServerWindow* w = window; w; w = w->parent()) {
if (CanBeActivated(w))
return true;
}
return false;
return GetActivatableAncestorOf(window) != nullptr;
}
bool FocusController::CanBeActivated(ServerWindow* window) const {
DCHECK(window);
// The parent window must be allowed to have active children.
if (!delegate_->CanHaveActiveChildren(window->parent()))
return false;
// TODO(sad): Implement this.
return true;
}
ServerWindow* FocusController::GetActivatableAncestorOf(
ServerWindow* window) const {
for (ServerWindow* w = window; w; w = w->parent()) {
if (CanBeActivated(w))
return w;
}
return nullptr;
}
void FocusController::SetFocusedWindowImpl(
FocusControllerChangeSource change_source,
ServerWindow* window) {
if (window && !CanBeFocused(window))
return;
ServerWindow* old = GetFocusedWindow();
ServerWindow* old_focused = GetFocusedWindow();
DCHECK(!window || window->IsDrawn());
// TODO(sad): Activate the closest activatable ancestor window.
if (window)
drawn_tracker_.reset(new ServerWindowDrawnTracker(window, this));
else
drawn_tracker_.reset();
// Activate the closest activatable ancestor window.
// TODO(sad): The window to activate doesn't necessarily have to be a direct
// ancestor (e.g. could be a transient parent).
ServerWindow* old_active = active_window_;
active_window_ = GetActivatableAncestorOf(window);
if (old_active != active_window_) {
FOR_EACH_OBSERVER(FocusControllerObserver, observers_,
OnActivationChanged(old_active, active_window_));
}
FOR_EACH_OBSERVER(FocusControllerObserver, observers_,
OnFocusChanged(change_source, old, window));
OnFocusChanged(change_source, old_focused, window));
focused_window_ = window;
// We can currently use only a single ServerWindowDrawnTracker since focused
// window is expected to be a direct descendant of the active window.
if (focused_window_ && active_window_) {
DCHECK(active_window_->Contains(focused_window_));
}
ServerWindow* track_window = focused_window_;
if (!track_window)
track_window = active_window_;
if (track_window)
drawn_tracker_.reset(new ServerWindowDrawnTracker(track_window, this));
else
drawn_tracker_.reset();
}
void FocusController::OnDrawnStateChanged(ServerWindow* ancestor,
ServerWindow* window,
bool is_drawn) {
DCHECK(!is_drawn); // We only observe when drawn.
// TODO(sad): If |window| is |focused_window_|, then move focus to the next
// focusable window in |active_window_|, if |active_window_| is still visible.
// If |active_window_| is invisible, or if |window| is |active_window_|, then
// activate the next window that can be activated.
SetFocusedWindowImpl(FocusControllerChangeSource::DRAWN_STATE_CHANGED,
ancestor);
}
......
......@@ -13,6 +13,7 @@ namespace mus {
namespace ws {
class FocusControllerDelegate;
class FocusControllerObserver;
class ServerWindow;
class ServerWindowDrawnTracker;
......@@ -27,7 +28,7 @@ enum class FocusControllerChangeSource {
// state of the focused window changes.
class FocusController : public ServerWindowDrawnTrackerObserver {
public:
FocusController();
explicit FocusController(FocusControllerDelegate* delegate);
~FocusController() override;
// Sets the focused window. Does nothing if |window| is currently focused.
......@@ -35,9 +36,6 @@ class FocusController : public ServerWindowDrawnTrackerObserver {
void SetFocusedWindow(ServerWindow* window);
ServerWindow* GetFocusedWindow();
// Moves activation to the next activatable window.
void CycleActivationForward();
void AddObserver(FocusControllerObserver* observer);
void RemoveObserver(FocusControllerObserver* observer);
......@@ -46,6 +44,10 @@ class FocusController : public ServerWindowDrawnTrackerObserver {
bool CanBeFocused(ServerWindow* window) const;
bool CanBeActivated(ServerWindow* window) const;
// Returns the closest activatable ancestor of |window|. Returns nullptr if
// there is no such ancestor.
ServerWindow* GetActivatableAncestorOf(ServerWindow* window) const;
// Implementation of SetFocusedWindow().
void SetFocusedWindowImpl(FocusControllerChangeSource change_source,
ServerWindow* window);
......@@ -55,7 +57,14 @@ class FocusController : public ServerWindowDrawnTrackerObserver {
ServerWindow* window,
bool is_drawn) override;
FocusControllerDelegate* delegate_;
ServerWindow* focused_window_;
ServerWindow* active_window_;
base::ObserverList<FocusControllerObserver> observers_;
// Keeps track of the visibility of the focused and active window.
scoped_ptr<ServerWindowDrawnTracker> drawn_tracker_;
DISALLOW_COPY_AND_ASSIGN(FocusController);
......
// Copyright 2015 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 COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
#define COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
namespace mus {
namespace ws {
class ServerWindow;
class FocusControllerDelegate {
public:
virtual bool CanHaveActiveChildren(ServerWindow* window) const = 0;
protected:
~FocusControllerDelegate() {}
};
} // namespace ws
} // namespace mus
#endif // COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
......@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
#define COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
#ifndef COMPONENTS_MUS_WS_FOCUS_CONTROLLER_OBSERVER_H_
#define COMPONENTS_MUS_WS_FOCUS_CONTROLLER_OBSERVER_H_
namespace mus {
namespace ws {
......@@ -13,6 +13,8 @@ class ServerWindow;
class FocusControllerObserver {
public:
virtual void OnActivationChanged(ServerWindow* old_active_window,
ServerWindow* new_active_window) = 0;
virtual void OnFocusChanged(FocusControllerChangeSource change_source,
ServerWindow* old_focused_window,
ServerWindow* new_focused_window) = 0;
......@@ -24,4 +26,4 @@ class FocusControllerObserver {
} // namespace ws
} // namespace mus
#endif // COMPONENTS_MUS_WS_FOCUS_CONTROLLER_DELEGATE_H_
#endif // COMPONENTS_MUS_WS_FOCUS_CONTROLLER_OBSERVER_H_
......@@ -4,6 +4,7 @@
#include "components/mus/ws/focus_controller.h"
#include "components/mus/ws/focus_controller_delegate.h"
#include "components/mus/ws/focus_controller_observer.h"
#include "components/mus/ws/server_window.h"
#include "components/mus/ws/test_server_window_delegate.h"
......@@ -14,7 +15,8 @@ namespace mus {
namespace ws {
namespace {
class TestFocusControllerObserver : public FocusControllerObserver {
class TestFocusControllerObserver : public FocusControllerObserver,
public FocusControllerDelegate {
public:
TestFocusControllerObserver()
: change_count_(0u),
......@@ -31,7 +33,13 @@ class TestFocusControllerObserver : public FocusControllerObserver {
ServerWindow* new_focused_window() { return new_focused_window_; }
private:
// FocusControllerDelegate:
bool CanHaveActiveChildren(ServerWindow* window) const override {
return true;
}
// FocusControllerObserver:
void OnActivationChanged(ServerWindow* old_active_window,
ServerWindow* new_active_window) override {}
void OnFocusChanged(FocusControllerChangeSource source,
ServerWindow* old_focused_window,
ServerWindow* new_focused_window) override {
......@@ -65,7 +73,7 @@ TEST(FocusControllerTest, Basic) {
child.Add(&child_child);
TestFocusControllerObserver focus_observer;
FocusController focus_controller;
FocusController focus_controller(&focus_observer);
focus_controller.AddObserver(&focus_observer);
focus_controller.SetFocusedWindow(&child_child);
......
......@@ -23,6 +23,12 @@ namespace ws {
namespace {
int ValidIndexOf(const Window::Children& windows, Window* window) {
Window::Children::const_iterator it =
std::find(windows.begin(), windows.end(), window);
return (it != windows.end()) ? (it - windows.begin()) : -1;
}
class BoundsChangeObserver : public WindowObserver {
public:
explicit BoundsChangeObserver(Window* window) : window_(window) {
......@@ -201,6 +207,13 @@ class WindowServerTest : public WindowServerTestBase {
public:
WindowServerTest() {}
Window* NewVisibleWindow(Window* parent, WindowTreeConnection* connection) {
Window* window = connection->NewWindow();
window->SetVisible(true);
parent->AddChild(window);
return window;
}
// Embeds another version of the test app @ window. This runs a run loop until
// a response is received, or a timeout. On success the new WindowServer is
// returned.
......@@ -623,6 +636,13 @@ class FocusChangeObserver : public WindowObserver {
MOJO_DISALLOW_COPY_AND_ASSIGN(FocusChangeObserver);
};
bool WaitForWindowToHaveFocus(Window* window) {
if (window->HasFocus())
return true;
FocusChangeObserver observer(window);
return WindowServerTestBase::DoRunLoopWithTimeout();
}
} // namespace
TEST_F(WindowServerTest, Focus) {
......@@ -657,8 +677,7 @@ TEST_F(WindowServerTest, Focus) {
}
{
// Add an observer on the Window that loses focus, and make sure the
// observer
// sees the right values.
// observer sees the right values.
FocusChangeObserver observer(window11);
embedded->GetRoot()->SetFocus();
ASSERT_TRUE(DoRunLoopWithTimeout());
......@@ -669,6 +688,64 @@ TEST_F(WindowServerTest, Focus) {
}
}
TEST_F(WindowServerTest, Activation) {
Window* parent =
NewVisibleWindow(window_manager()->GetRoot(), window_manager());
Window* child1 = NewVisibleWindow(parent, window_manager());
Window* child2 = NewVisibleWindow(parent, window_manager());
Window* child3 = NewVisibleWindow(parent, window_manager());
child1->AddTransientWindow(child3);
WindowTreeConnection* embedded1 = Embed(child1).connection;
ASSERT_NE(nullptr, embedded1);
WindowTreeConnection* embedded2 = Embed(child2).connection;
ASSERT_NE(nullptr, embedded2);
Window* child11 = NewVisibleWindow(embedded1->GetRoot(), embedded1);
Window* child21 = NewVisibleWindow(embedded2->GetRoot(), embedded2);
WaitForTreeSizeToMatch(parent, 6);
// Allow the child windows to be activated.
host()->AddActivationParent(parent->id());
// |child2| and |child3| are stacked about |child1|.
EXPECT_GT(ValidIndexOf(parent->children(), child2),
ValidIndexOf(parent->children(), child1));
EXPECT_GT(ValidIndexOf(parent->children(), child3),
ValidIndexOf(parent->children(), child1));
// Set focus on |child11|. This should activate |child1|, and raise it over
// |child2|. But |child3| should still be above |child1| because of
// transiency.
child11->SetFocus();
ASSERT_TRUE(WaitForWindowToHaveFocus(child11));
ASSERT_TRUE(
WaitForWindowToHaveFocus(window_manager()->GetWindowById(child11->id())));
EXPECT_EQ(child11->id(), window_manager()->GetFocusedWindow()->id());
EXPECT_EQ(child11->id(), embedded1->GetFocusedWindow()->id());
EXPECT_EQ(nullptr, embedded2->GetFocusedWindow());
EXPECT_GT(ValidIndexOf(parent->children(), child1),
ValidIndexOf(parent->children(), child2));
EXPECT_GT(ValidIndexOf(parent->children(), child3),
ValidIndexOf(parent->children(), child1));
// Set focus on |child21|. This should activate |child2|, and raise it over
// |child1|.
child21->SetFocus();
ASSERT_TRUE(WaitForWindowToHaveFocus(child21));
ASSERT_TRUE(
WaitForWindowToHaveFocus(window_manager()->GetWindowById(child21->id())));
EXPECT_EQ(child21->id(), window_manager()->GetFocusedWindow()->id());
EXPECT_EQ(child21->id(), embedded2->GetFocusedWindow()->id());
EXPECT_EQ(nullptr, embedded1->GetFocusedWindow());
EXPECT_GT(ValidIndexOf(parent->children(), child2),
ValidIndexOf(parent->children(), child1));
EXPECT_GT(ValidIndexOf(parent->children(), child3),
ValidIndexOf(parent->children(), child1));
}
namespace {
class DestroyedChangedObserver : public WindowObserver {
......
......@@ -30,7 +30,7 @@ WindowTreeHostImpl::WindowTreeHostImpl(
event_dispatcher_(this),
display_manager_(
DisplayManager::Create(app_impl, gpu_state, surfaces_state)),
focus_controller_(new FocusController),
focus_controller_(new FocusController(this)),
window_manager_(window_manager.Pass()) {
focus_controller_->AddObserver(this);
display_manager_->Init(this);
......@@ -137,6 +137,20 @@ void WindowTreeHostImpl::RemoveAccelerator(uint32_t id) {
event_dispatcher_.RemoveAccelerator(id);
}
void WindowTreeHostImpl::AddActivationParent(uint32_t window_id) {
ServerWindow* window =
connection_manager_->GetWindow(WindowIdFromTransportId(window_id));
if (window)
activation_parents_.insert(window->id());
}
void WindowTreeHostImpl::RemoveActivationParent(uint32_t window_id) {
ServerWindow* window =
connection_manager_->GetWindow(WindowIdFromTransportId(window_id));
if (window)
activation_parents_.erase(window->id());
}
void WindowTreeHostImpl::OnClientClosed() {
// |display_manager_.reset()| destroys the display-manager first, and then
// sets |display_manager_| to nullptr. However, destroying |display_manager_|
......@@ -190,6 +204,20 @@ void WindowTreeHostImpl::OnCompositorFrameDrawn() {
}
}
bool WindowTreeHostImpl::CanHaveActiveChildren(ServerWindow* window) const {
return window && activation_parents_.count(window->id()) > 0;
}
void WindowTreeHostImpl::OnActivationChanged(ServerWindow* old_active_window,
ServerWindow* new_active_window) {
DCHECK_NE(new_active_window, old_active_window);
if (new_active_window && new_active_window->parent()) {
// Raise the new active window.
// TODO(sad): Let the WM dictate whether to raise the window or not?
new_active_window->parent()->StackChildAtTop(new_active_window);
}
}
void WindowTreeHostImpl::OnFocusChanged(
FocusControllerChangeSource change_source,
ServerWindow* old_focused_window,
......
......@@ -12,6 +12,7 @@
#include "components/mus/ws/display_manager.h"
#include "components/mus/ws/event_dispatcher.h"
#include "components/mus/ws/event_dispatcher_delegate.h"
#include "components/mus/ws/focus_controller_delegate.h"
#include "components/mus/ws/focus_controller_observer.h"
#include "components/mus/ws/server_window.h"
#include "components/mus/ws/server_window_observer.h"
......@@ -32,6 +33,7 @@ class WindowTreeImpl;
class WindowTreeHostImpl : public DisplayManagerDelegate,
public mojom::WindowTreeHost,
public FocusControllerObserver,
public FocusControllerDelegate,
public EventDispatcherDelegate,
public ServerWindowObserver {
public:
......@@ -94,6 +96,8 @@ class WindowTreeHostImpl : public DisplayManagerDelegate,
void AddAccelerator(uint32_t id,
mojom::EventMatcherPtr event_matcher) override;
void RemoveAccelerator(uint32_t id) override;
void AddActivationParent(uint32_t window_id) override;
void RemoveActivationParent(uint32_t window_id) override;
private:
void OnClientClosed();
......@@ -108,7 +112,12 @@ class WindowTreeHostImpl : public DisplayManagerDelegate,
void OnTopLevelSurfaceChanged(cc::SurfaceId surface_id) override;
void OnCompositorFrameDrawn() override;
// FocusControllerDelegate:
bool CanHaveActiveChildren(ServerWindow* window) const override;
// FocusControllerObserver:
void OnActivationChanged(ServerWindow* old_active_window,
ServerWindow* new_active_window) override;
void OnFocusChanged(FocusControllerChangeSource change_source,
ServerWindow* old_focused_window,
ServerWindow* new_focused_window) override;
......@@ -133,6 +142,8 @@ class WindowTreeHostImpl : public DisplayManagerDelegate,
scoped_ptr<FocusController> focus_controller_;
mojom::WindowManagerPtr window_manager_;
std::set<WindowId> activation_parents_;
// Set of windows with surfaces that need to be destroyed once the frame
// draws.
std::set<ServerWindow*> windows_needing_frame_destruction_;
......
......@@ -390,9 +390,20 @@ TEST_F(WindowTreeTest, FocusOnPointer) {
connection1_client->tracker()->changes()->clear();
wm_client()->tracker()->changes()->clear();
// Focus should not go to |child1| yet, since the parent still doesn't allow
// active children.
display_manager_delegate()->OnEvent(CreatePointerDownEvent(21, 22));
EXPECT_EQ(nullptr, connection1->GetHost()->GetFocusedWindow());
display_manager_delegate()->OnEvent(CreatePointerUpEvent(21, 22));
connection1_client->tracker()->changes()->clear();
wm_client()->tracker()->changes()->clear();
connection1->GetHost()->AddActivationParent(
WindowIdToTransportId(embed_window_id));
// Focus should go to child1. This result in notifying both the window
// manager and client connection being notified.
display_manager_delegate()->OnEvent(CreatePointerDownEvent(21, 22));
EXPECT_EQ(v1, connection1->GetHost()->GetFocusedWindow());
ASSERT_GE(wm_client()->tracker()->changes()->size(), 1u);
EXPECT_EQ("Focused id=2,1",
......@@ -406,19 +417,10 @@ TEST_F(WindowTreeTest, FocusOnPointer) {
wm_client()->tracker()->changes()->clear();
connection1_client->tracker()->changes()->clear();
// Press outside of the embedded window. Focus should go to the root. Notice
// the client1 doesn't see who has focus as the focused window (root) isn't
// visible to it.
// Press outside of the embedded window. Note that root cannot be focused
// (because it cannot be activated). So the focus would not move in this case.
display_manager_delegate()->OnEvent(CreatePointerDownEvent(61, 22));
EXPECT_EQ(host_connection()->window_tree_host()->root_window(),
host_connection()->window_tree_host()->GetFocusedWindow());
ASSERT_GE(wm_client()->tracker()->changes()->size(), 1u);
EXPECT_EQ("Focused id=0,2",
ChangesToDescription1(*wm_client()->tracker()->changes())[0]);
ASSERT_GE(connection1_client->tracker()->changes()->size(), 1u);
EXPECT_EQ(
"Focused id=null",
ChangesToDescription1(*connection1_client->tracker()->changes())[0]);
EXPECT_EQ(v1, host_connection()->window_tree_host()->GetFocusedWindow());
display_manager_delegate()->OnEvent(CreatePointerUpEvent(21, 22));
wm_client()->tracker()->changes()->clear();
......@@ -427,9 +429,9 @@ TEST_F(WindowTreeTest, FocusOnPointer) {
// Press in the same location. Should not get a focus change event (only input
// event).
display_manager_delegate()->OnEvent(CreatePointerDownEvent(61, 22));
EXPECT_EQ(host_connection()->window_tree_host()->root_window(),
host_connection()->window_tree_host()->GetFocusedWindow());
ASSERT_EQ(wm_client()->tracker()->changes()->size(), 1u);
EXPECT_EQ(v1, host_connection()->window_tree_host()->GetFocusedWindow());
ASSERT_EQ(wm_client()->tracker()->changes()->size(), 1u)
<< SingleChangeToDescription(*wm_client()->tracker()->changes());
EXPECT_EQ("InputEvent window=0,2 event_action=4",
ChangesToDescription1(*wm_client()->tracker()->changes())[0]);
EXPECT_TRUE(connection1_client->tracker()->changes()->empty());
......@@ -465,6 +467,9 @@ TEST_F(WindowTreeTest, BasicInputEventTarget) {
const WindowId child1(connection1->id(), 1);
EXPECT_TRUE(connection1->NewWindow(child1, ServerWindow::Properties()));
EXPECT_TRUE(connection1->AddWindow(embed_window_id, child1));
connection1->GetHost()->AddActivationParent(
WindowIdToTransportId(embed_window_id));
ServerWindow* v1 = connection1->GetWindow(child1);
v1->SetVisible(true);
v1->SetBounds(gfx::Rect(20, 20, 20, 20));
......
......@@ -200,6 +200,7 @@ void BrowserWindow::OnEmbed(mus::Window* root) {
host_->SetSize(mojo::Size::From(gfx::Size(1280, 800)));
root_->AddChild(content_);
host_->AddActivationParent(root_->id());
content_->SetVisible(true);
web_view_.Init(app_, content_);
......
......@@ -57,8 +57,12 @@ void WindowManagerApplication::OnEmbed(mus::Window* root) {
GetWindowForContainer(mash::wm::mojom::CONTAINER_USER_BACKGROUND)));
shelf_layout_.reset(new ShelfLayout(
GetWindowForContainer(mash::wm::mojom::CONTAINER_USER_SHELF)));
mus::Window* window =
GetWindowForContainer(mash::wm::mojom::CONTAINER_USER_WINDOWS);
window_layout_.reset(new WindowLayout(
GetWindowForContainer(mash::wm::mojom::CONTAINER_USER_WINDOWS)));
host_->AddActivationParent(window->id());
window_manager_.reset(new WindowManagerImpl(this));
......
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