Commit ef9025a5 authored by mfomitchev's avatar mfomitchev Committed by Commit bot

Adding split view divider widget.

Adding split view divider widget which could be dragged to exit the split view.
Resubmit of https://codereview.chromium.org/545393002

TBR=mukai@chromium.org
BUG=403207, 408691

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

Cr-Commit-Position: refs/heads/master@{#296464}
parent f173cbc4
......@@ -51,11 +51,6 @@
'activity/public/activity_view_manager.h',
'activity/public/activity_view_model.h',
'athena_export.h',
'util/container_priorities.h',
'util/fill_layout_manager.cc',
'util/fill_layout_manager.h',
'util/switches.cc',
'util/switches.h',
'env/athena_env_impl.cc',
'env/public/athena_env.h',
'home/app_list_view_delegate.cc',
......@@ -100,6 +95,13 @@
'system/time_view.h',
'system/public/system_ui.h',
'system/system_ui_impl.cc',
'util/container_priorities.h',
'util/drag_handle.cc',
'util/drag_handle.h',
'util/fill_layout_manager.cc',
'util/fill_layout_manager.h',
'util/switches.cc',
'util/switches.h',
'wm/bezel_controller.cc',
'wm/bezel_controller.h',
'wm/overview_toolbar.cc',
......@@ -267,7 +269,6 @@
],
'sources': [
'activity/activity_manager_unittest.cc',
'util/fill_layout_manager_unittest.cc',
'content/app_activity_unittest.cc',
'env/athena_env_unittest.cc',
'home/athena_start_page_view_unittest.cc',
......@@ -278,6 +279,8 @@
'resource_manager/resource_manager_unittest.cc',
'screen/screen_manager_unittest.cc',
'test/athena_unittests.cc',
'util/drag_handle_unittest.cc',
'util/fill_layout_manager_unittest.cc',
'wm/split_view_controller_unittest.cc',
'wm/window_list_provider_impl_unittest.cc',
'wm/window_manager_unittest.cc',
......
......@@ -2,4 +2,5 @@ include_rules = [
"+athena/athena_export.h",
"+ui/aura",
"+ui/compositor",
"+ui/views",
]
// Copyright 2014 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 "athena/util/drag_handle.h"
#include "ui/views/background.h"
#include "ui/views/view.h"
namespace athena {
namespace {
const SkColor kDragHandleColorNormal = SK_ColorGRAY;
const SkColor kDragHandleColorHot = SK_ColorWHITE;
// This view notifies its delegate of the touch scroll gestures performed on it.
class DragHandleView : public views::View {
public:
DragHandleView(DragHandleScrollDirection scroll_direction,
DragHandleScrollDelegate* delegate,
int preferred_width,
int preferred_height);
virtual ~DragHandleView();
private:
void SetColor(SkColor color);
void SetIsScrolling(bool scrolling);
// views::View:
virtual gfx::Size GetPreferredSize() const OVERRIDE;
virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE;
bool scroll_in_progress_;
DragHandleScrollDelegate* delegate_;
DragHandleScrollDirection scroll_direction_;
SkColor color_;
float scroll_start_location_;
const int preferred_width_;
const int preferred_height_;
DISALLOW_COPY_AND_ASSIGN(DragHandleView);
};
DragHandleView::DragHandleView(DragHandleScrollDirection scroll_direction,
DragHandleScrollDelegate* delegate,
int preferred_width,
int preferred_height)
: scroll_in_progress_(false),
delegate_(delegate),
scroll_direction_(scroll_direction),
color_(SK_ColorTRANSPARENT),
preferred_width_(preferred_width),
preferred_height_(preferred_height) {
SetColor(kDragHandleColorNormal);
}
DragHandleView::~DragHandleView() {
}
void DragHandleView::SetColor(SkColor color) {
if (color_ == color)
return;
color_ = color;
set_background(views::Background::CreateSolidBackground(color_));
SchedulePaint();
}
void DragHandleView::SetIsScrolling(bool scrolling) {
if (scroll_in_progress_ == scrolling)
return;
scroll_in_progress_ = scrolling;
if (!scroll_in_progress_)
scroll_start_location_ = 0;
}
// views::View:
gfx::Size DragHandleView::GetPreferredSize() const {
return gfx::Size(preferred_width_, preferred_height_);
}
void DragHandleView::OnGestureEvent(ui::GestureEvent* event) {
SkColor change_color = SK_ColorTRANSPARENT;
if (event->type() == ui::ET_GESTURE_BEGIN &&
event->details().touch_points() == 1) {
change_color = kDragHandleColorHot;
} else if (event->type() == ui::ET_GESTURE_END &&
event->details().touch_points() == 1) {
change_color = kDragHandleColorNormal;
}
if (change_color != SK_ColorTRANSPARENT) {
SetColor(change_color);
event->SetHandled();
return;
}
if (event->type() == ui::ET_GESTURE_SCROLL_BEGIN) {
if (scroll_in_progress_)
return;
float delta;
if (scroll_direction_ == DRAG_HANDLE_VERTICAL) {
delta = event->details().scroll_y_hint();
scroll_start_location_ = event->root_location().y();
} else {
delta = event->details().scroll_x_hint();
scroll_start_location_ = event->root_location().x();
}
delegate_->HandleScrollBegin(delta);
SetIsScrolling(true);
event->SetHandled();
} else if (event->type() == ui::ET_GESTURE_SCROLL_END ||
event->type() == ui::ET_SCROLL_FLING_START) {
if (!scroll_in_progress_)
return;
delegate_->HandleScrollEnd();
SetColor(kDragHandleColorNormal);
SetIsScrolling(false);
event->SetHandled();
} else if (event->type() == ui::ET_GESTURE_SCROLL_UPDATE) {
if (!scroll_in_progress_)
return;
float delta = scroll_direction_ == DRAG_HANDLE_VERTICAL
? event->root_location().y() - scroll_start_location_
: event->root_location().x() - scroll_start_location_;
delegate_->HandleScrollUpdate(delta);
event->SetHandled();
}
}
} // namespace
views::View* CreateDragHandleView(DragHandleScrollDirection scroll_direction,
DragHandleScrollDelegate* delegate,
int preferred_width,
int preferred_height) {
views::View* view = new DragHandleView(
scroll_direction, delegate, preferred_width, preferred_height);
return view;
}
} // namespace athena
// Copyright 2014 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 ATHENA_UTIL_DRAG_HANDLE_H_
#define ATHENA_UTIL_DRAG_HANDLE_H_
#include "athena/athena_export.h"
namespace views {
class View;
}
namespace athena {
class DragHandleScrollDelegate {
public:
virtual ~DragHandleScrollDelegate() {}
// Beginning of a scroll gesture.
virtual void HandleScrollBegin(float delta) = 0;
// End of the current scroll gesture.
virtual void HandleScrollEnd() = 0;
// Update of the scroll position for the currently active scroll gesture.
virtual void HandleScrollUpdate(float delta) = 0;
};
enum DragHandleScrollDirection { DRAG_HANDLE_VERTICAL, DRAG_HANDLE_HORIZONTAL };
// Creates a handle view which notifies the delegate of the scrolls performed on
// it.
ATHENA_EXPORT views::View* CreateDragHandleView(
DragHandleScrollDirection scroll_direction,
DragHandleScrollDelegate* delegate,
int preferred_width,
int preferred_height);
} // namespace athena
#endif // ATHENA_UTIL_DRAG_HANDLE_H_
// Copyright 2014 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 "athena/util/drag_handle.h"
#include "ui/aura/test/aura_test_base.h"
#include "ui/aura/window.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
namespace athena {
class DragHandleDelegateTest : public DragHandleScrollDelegate {
public:
explicit DragHandleDelegateTest()
: begin_delta_(0),
got_scroll_end_(false),
update_delta_(0) {}
virtual ~DragHandleDelegateTest() {}
void Reset() {
begin_delta_ = 0;
got_scroll_end_ = false;
update_delta_ = 0;
}
float begin_delta() { return begin_delta_; }
bool got_scroll_end() { return got_scroll_end_; }
float update_delta() { return update_delta_; }
private:
// DragHandleScrollDelegate:
virtual void HandleScrollBegin(float delta) OVERRIDE {
begin_delta_ = delta;
}
virtual void HandleScrollEnd() OVERRIDE {
got_scroll_end_ = true;
}
virtual void HandleScrollUpdate(float delta) OVERRIDE {
update_delta_ = delta;
}
float begin_delta_;
bool got_scroll_end_;
float update_delta_;
DISALLOW_COPY_AND_ASSIGN(DragHandleDelegateTest);
};
typedef aura::test::AuraTestBase DragHandleTest;
const int kDragHandleWidth = 10;
const int kDragHandleHeight = 100;
ui::GestureEvent CreateGestureEvent(ui::EventType type,
float x,
float delta_x) {
ui::GestureEvent event(
x,
1,
0,
base::TimeDelta::FromInternalValue(base::Time::Now().ToInternalValue()),
type == ui::ET_GESTURE_SCROLL_END
? ui::GestureEventDetails(type)
: ui::GestureEventDetails(type, delta_x, 0));
event.set_root_location(gfx::PointF(x, 1));
return event;
}
TEST_F(DragHandleTest, ScrollTest) {
DragHandleDelegateTest delegate;
scoped_ptr<views::View> drag_handle(
CreateDragHandleView(DragHandleScrollDirection::DRAG_HANDLE_HORIZONTAL,
&delegate,
kDragHandleWidth,
kDragHandleHeight));
views::Widget widget;
views::Widget::InitParams params(
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.parent = root_window();
params.accept_events = true;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
widget.Init(params);
widget.SetContentsView(drag_handle.get());
const gfx::Size& size = gfx::Size(kDragHandleWidth, kDragHandleHeight);
widget.SetSize(size);
widget.SetBounds(gfx::Rect(0, 0, size.width(), size.height()));
const float begin_x = 4.0;
const float begin_delta = 10.0;
const float update_delta = 15.0;
ui::GestureEvent scroll_begin_model =
CreateGestureEvent(ui::ET_GESTURE_SCROLL_BEGIN, begin_x, begin_delta);
ui::GestureEvent scroll_update_model =
CreateGestureEvent(ui::ET_GESTURE_SCROLL_UPDATE,
begin_x + update_delta,
update_delta - begin_delta);
ui::GestureEvent scroll_end_model =
CreateGestureEvent(ui::ET_GESTURE_SCROLL_END, begin_x + update_delta, 0);
ui::GestureEvent fling_start_model =
CreateGestureEvent(ui::ET_SCROLL_FLING_START, begin_x + update_delta, 0);
// Normal scroll
ui::GestureEvent e(scroll_begin_model);
widget.OnGestureEvent(&e);
EXPECT_EQ(begin_delta, delegate.begin_delta());
EXPECT_EQ(0, delegate.update_delta());
EXPECT_FALSE(delegate.got_scroll_end());
e = ui::GestureEvent(scroll_update_model);
widget.OnGestureEvent(&e);
EXPECT_EQ(update_delta, delegate.update_delta());
EXPECT_FALSE(delegate.got_scroll_end());
e = ui::GestureEvent(scroll_end_model);
widget.OnGestureEvent(&e);
EXPECT_EQ(update_delta, delegate.update_delta());
EXPECT_TRUE(delegate.got_scroll_end());
delegate.Reset();
// Scroll ending with a fling
e = ui::GestureEvent(scroll_begin_model);
widget.OnGestureEvent(&e);
e = ui::GestureEvent(scroll_update_model);
widget.OnGestureEvent(&e);
e = ui::GestureEvent(fling_start_model);
widget.OnGestureEvent(&e);
EXPECT_TRUE(delegate.got_scroll_end());
delegate.Reset();
drag_handle.reset();
}
} // namespace athena
......@@ -9,5 +9,6 @@ include_rules = [
"+ui/compositor",
"+ui/events",
"+ui/gfx",
"+ui/views",
"+ui/wm",
]
......@@ -46,6 +46,21 @@ float GetDistance(const gfx::PointF& location,
: point_in_screen.x() - GetDisplay(window).bounds().width();
}
// Returns the bezel corresponding to the |location| in |window| or BEZEL_NONE
// if the location is outside of the bezel area.
// Only implemented for LEFT and RIGHT bezels.
BezelController::Bezel GetBezel(const gfx::PointF& location,
aura::Window* window) {
int screen_width = GetDisplay(window).bounds().width();
gfx::Point point_in_screen(gfx::ToRoundedPoint(location));
wm::ConvertPointToScreen(window, &point_in_screen);
if (point_in_screen.x() < kBezelWidth)
return BezelController::BEZEL_LEFT;
if (point_in_screen.x() > screen_width - kBezelWidth)
return BezelController::BEZEL_RIGHT;
return BezelController::BEZEL_NONE;
}
} // namespace
BezelController::BezelController(aura::Window* container)
......@@ -69,9 +84,9 @@ void BezelController::SetState(BezelController::State state,
return;
if (state == BEZEL_SCROLLING_TWO_FINGERS)
left_right_delegate_->ScrollBegin(scroll_bezel_, scroll_delta);
left_right_delegate_->BezelScrollBegin(scroll_bezel_, scroll_delta);
else if (state_ == BEZEL_SCROLLING_TWO_FINGERS)
left_right_delegate_->ScrollEnd();
left_right_delegate_->BezelScrollEnd();
state_ = state;
if (state == NONE) {
scroll_bezel_ = BEZEL_NONE;
......@@ -79,18 +94,6 @@ void BezelController::SetState(BezelController::State state,
}
}
// Only implemented for LEFT and RIGHT bezels ATM.
BezelController::Bezel BezelController::GetBezel(const gfx::PointF& location) {
int screen_width = GetDisplay(container_).bounds().width();
if (location.x() < kBezelWidth) {
return BEZEL_LEFT;
} else if (location.x() > screen_width - kBezelWidth) {
return BEZEL_RIGHT;
} else {
return BEZEL_NONE;
}
}
void BezelController::OnGestureEvent(ui::GestureEvent* event) {
// TODO(mfomitchev): Currently we aren't retargetting or consuming any of the
// touch events. This means that content can prevent the generation of gesture
......@@ -105,24 +108,35 @@ void BezelController::OnGestureEvent(ui::GestureEvent* event) {
if (!ShouldProcessGesture(type))
return;
const ui::GestureEventDetails& event_details = event->details();
int num_touch_points = event_details.touch_points();
if (num_touch_points == 1 && type == ui::ET_GESTURE_BEGIN) {
// Reset the state when the first finger touches and starts a gesture.
// Normally, the state gets reset when the last finger is lifted and we
// receive ET_GESTURE_END. However ET_GESTURE_END doesn't always get
// dispatched. (E.g. if the gesture target was hidden or deleted).
// Since we can't rely on receiving ET_GESTURE_END when the last finger is
// lifted, we also reset the state on ET_GESTURE_BEGIN when the first
// finger touches the screen.
SetState(NONE);
}
if (scroll_target_ && event->target() != scroll_target_)
return;
const gfx::PointF& event_location = event->location_f();
const ui::GestureEventDetails& event_details = event->details();
int num_touch_points = event_details.touch_points();
float scroll_delta = kScrollDeltaNone;
if (scroll_bezel_ != BEZEL_NONE) {
aura::Window* target_window = static_cast<aura::Window*>(event->target());
aura::Window* target_window = static_cast<aura::Window*>(event->target());
if (scroll_bezel_ != BEZEL_NONE)
scroll_delta = GetDistance(event_location, target_window, scroll_bezel_);
}
if (type == ui::ET_GESTURE_BEGIN) {
if (num_touch_points > 2) {
SetState(IGNORE_CURRENT_SCROLL);
return;
}
BezelController::Bezel event_bezel = GetBezel(event->location_f());
BezelController::Bezel event_bezel =
GetBezel(event->location_f(), target_window);
switch (state_) {
case NONE:
scroll_bezel_ = event_bezel;
......@@ -174,14 +188,14 @@ void BezelController::OnGestureEvent(ui::GestureEvent* event) {
DCHECK_EQ(num_touch_points, 2);
SetState(BEZEL_SCROLLING_TWO_FINGERS, scroll_delta);
if (left_right_delegate_->CanScroll())
if (left_right_delegate_->BezelCanScroll())
event->SetHandled();
} else if (type == ui::ET_GESTURE_SCROLL_UPDATE) {
if (state_ != BEZEL_SCROLLING_TWO_FINGERS)
return;
left_right_delegate_->ScrollUpdate(scroll_delta);
if (left_right_delegate_->CanScroll())
left_right_delegate_->BezelScrollUpdate(scroll_delta);
if (left_right_delegate_->BezelCanScroll())
event->SetHandled();
}
}
......
......@@ -37,18 +37,18 @@ class BezelController : public ui::EventHandler {
// Beginning of a bezel scroll gesture started from the |bezel|.
// |delta| is the difference between the x-coordinate of the current scroll
// position and the bezel. It will be zero or negative for the right bezel.
virtual void ScrollBegin(Bezel bezel, float delta) = 0;
virtual void BezelScrollBegin(Bezel bezel, float delta) = 0;
// End of the current bezel scroll
virtual void ScrollEnd() = 0;
virtual void BezelScrollEnd() = 0;
// Update of the scroll position for the currently active bezel scroll.
// |delta| has the same meaning as in ScrollBegin().
virtual void ScrollUpdate(float delta) = 0;
virtual void BezelScrollUpdate(float delta) = 0;
// Should return false if the delegate isn't going to react to the scroll
// events.
virtual bool CanScroll() = 0;
virtual bool BezelCanScroll() = 0;
};
explicit BezelController(aura::Window* container);
......@@ -75,10 +75,6 @@ class BezelController : public ui::EventHandler {
// BEZEL_SROLLING states.
void SetState(State state, float scroll_delta);
// Returns the bezel corresponding to the |location| or BEZEL_NONE if the
// location is outside of the bezel area.
Bezel GetBezel(const gfx::PointF& location);
// ui::EventHandler overrides
virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE;
......
This diff is collapsed.
......@@ -6,7 +6,9 @@
#define ATHENA_WM_SPLIT_VIEW_CONTROLLER_H_
#include "athena/athena_export.h"
#include "athena/util/drag_handle.h"
#include "athena/wm/bezel_controller.h"
#include "athena/wm/public/window_manager_observer.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/weak_ptr.h"
......@@ -15,13 +17,26 @@ class Rect;
class Transform;
}
namespace aura {
class ScopedWindowTargeter;
class Window;
class WindowTargeter;
}
namespace views {
class ViewTargeterDelegate;
class Widget;
}
namespace athena {
class WindowListProvider;
// Responsible for entering split view mode, exiting from split view mode, and
// laying out the windows in split view mode.
class ATHENA_EXPORT SplitViewController
: public BezelController::ScrollDelegate {
: public BezelController::ScrollDelegate,
public DragHandleScrollDelegate,
public WindowManagerObserver {
public:
SplitViewController(aura::Window* container,
WindowListProvider* window_list_provider);
......@@ -45,12 +60,10 @@ class ATHENA_EXPORT SplitViewController
void ReplaceWindow(aura::Window* window,
aura::Window* replace_with);
// Returns the bounds that the left and right windows will have once split
// view is active and they are done animating. If |left_window_| and
// |right_window_| are still animating this may be different than their
// current bounds.
gfx::Rect GetLeftTargetBounds();
gfx::Rect GetRightTargetBounds();
// Returns the bounds of the left and right parts of the |container_| based
// on the current value of |divider_position_|.
gfx::Rect GetLeftAreaBounds();
gfx::Rect GetRightAreaBounds();
aura::Window* left_window() { return left_window_; }
aura::Window* right_window() { return right_window_; }
......@@ -61,7 +74,7 @@ class ATHENA_EXPORT SplitViewController
// NULL.
INACTIVE,
// Two windows |left_window_| and |right_window| are shown side by side and
// there is a horizontal scroll in progress which is dragging the separator
// there is a horizontal scroll in progress which is dragging the divider
// between the two windows.
SCROLLING,
// Split View mode is active with |left_window_| and |right_window| showing
......@@ -70,22 +83,42 @@ class ATHENA_EXPORT SplitViewController
};
void SetState(State state);
void InitializeDivider();
void HideDivider();
void ShowDivider();
void UpdateLayout(bool animate);
void SetWindowTransforms(const gfx::Transform& left_transform,
const gfx::Transform& right_transform,
const gfx::Transform& divider_transform,
bool animate);
// Called when the animation initiated by SetWindowTransforms() completes.
void OnAnimationCompleted();
void UpdateSeparatorPositionFromScrollDelta(float delta);
// Returns the default divider position for when the split view mode is
// active and the divider is not being dragged.
int GetDefaultDividerPosition();
// BezelController::ScrollDelegate:
virtual void ScrollBegin(BezelController::Bezel bezel, float delta) OVERRIDE;
virtual void ScrollEnd() OVERRIDE;
virtual void ScrollUpdate(float delta) OVERRIDE;
virtual bool CanScroll() OVERRIDE;
virtual void BezelScrollBegin(BezelController::Bezel bezel,
float delta) OVERRIDE;
virtual void BezelScrollEnd() OVERRIDE;
virtual void BezelScrollUpdate(float delta) OVERRIDE;
virtual bool BezelCanScroll() OVERRIDE;
// DragHandleScrollDelegate:
virtual void HandleScrollBegin(float delta) OVERRIDE;
virtual void HandleScrollEnd() OVERRIDE;
virtual void HandleScrollUpdate(float delta) OVERRIDE;
// WindowManagerObserver:
virtual void OnOverviewModeEnter() OVERRIDE;
virtual void OnOverviewModeExit() OVERRIDE;
virtual void OnSplitViewModeEnter() OVERRIDE;
virtual void OnSplitViewModeExit() OVERRIDE;
State state_;
......@@ -99,9 +132,23 @@ class ATHENA_EXPORT SplitViewController
aura::Window* left_window_;
aura::Window* right_window_;
// Position of the separator between left_window_ and right_window_ in
// container_ coordinates (along the x axis).
int separator_position_;
// X-Coordinate of the (center of the) divider between left_window_ and
// right_window_ in |container_| coordinates.
int divider_position_;
// Meaningful only when state_ is SCROLLING.
// X-Coordinate of the divider when the scroll started.
int divider_scroll_start_position_;
// Visually separates the windows and contains the drag handle.
views::Widget* divider_widget_;
// The drag handle which can be used when split view is active to exit the
// split view mode.
views::View* drag_handle_;
scoped_ptr<aura::ScopedWindowTargeter> window_targeter_;
scoped_ptr<views::ViewTargeterDelegate> view_targeter_delegate_;
// Windows which should be hidden when the animation initiated by
// UpdateLayout() completes.
......
......@@ -149,6 +149,7 @@ WindowManagerImpl::WindowManagerImpl() {
bezel_controller_.reset(new BezelController(container_.get()));
split_view_controller_.reset(
new SplitViewController(container_.get(), window_list_provider_.get()));
AddObserver(split_view_controller_.get());
bezel_controller_->set_left_right_delegate(split_view_controller_.get());
container_->AddPreTargetHandler(bezel_controller_.get());
title_drag_controller_.reset(new TitleDragController(container_.get(), this));
......@@ -162,6 +163,7 @@ WindowManagerImpl::WindowManagerImpl() {
WindowManagerImpl::~WindowManagerImpl() {
overview_.reset();
RemoveObserver(split_view_controller_.get());
split_view_controller_.reset();
window_list_provider_.reset();
if (container_) {
......
......@@ -318,8 +318,8 @@ TEST_F(WindowManagerTest, SplitModeActivationByShortcut) {
int width =
gfx::Screen::GetNativeScreen()->GetPrimaryDisplay().work_area().width();
EXPECT_EQ(width / 2, w1->bounds().width());
EXPECT_EQ(width / 2, w2->bounds().width());
EXPECT_EQ(w1->bounds().width(), w2->bounds().width());
EXPECT_GE(width / 2, w1->bounds().width());
// Toggle back to normal mode.
generator.PressKey(ui::VKEY_F6, ui::EF_CONTROL_DOWN);
......
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