Commit 0663980b authored by Mike Wasserman's avatar Mike Wasserman Committed by Commit Bot

ws: Add touch selection controller support for pointer watcher.

Allows ws clients to close touch ui on events outside their windows.

Add a new PointerWatcher plumbing to [Mus]ViewsDelegate.
Wires up watchers to the Mus-only PointerWatcherEventRouter.
(ui/views/mus/* is not allowed as a dependency of ui/views/*)

Destroy the touch selection ui on PointerWatcher mouse events.

Bug: 884394, 887725
Test: Mouse events outside the KSV window close KSV text touch ui.
Change-Id: I0e41da4d03719001191f7d5ab669bcc1805bd63c
Reviewed-on: https://chromium-review.googlesource.com/1232955Reviewed-by: default avatarScott Violet <sky@chromium.org>
Commit-Queue: Michael Wasserman <msw@chromium.org>
Cr-Commit-Position: refs/heads/master@{#593001}
parent 4d13de11
...@@ -92,13 +92,12 @@ class UI_BASE_EXPORT TouchEditingControllerDeprecated { ...@@ -92,13 +92,12 @@ class UI_BASE_EXPORT TouchEditingControllerDeprecated {
class UI_BASE_EXPORT TouchEditingControllerFactory { class UI_BASE_EXPORT TouchEditingControllerFactory {
public: public:
virtual ~TouchEditingControllerFactory() {}
static void SetInstance(TouchEditingControllerFactory* instance); static void SetInstance(TouchEditingControllerFactory* instance);
virtual TouchEditingControllerDeprecated* Create(TouchEditable* client_view) virtual TouchEditingControllerDeprecated* Create(TouchEditable* client_view)
= 0; = 0;
protected:
virtual ~TouchEditingControllerFactory() {}
}; };
} // namespace ui } // namespace ui
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
#include "ui/views/mus/ax_remote_host.h" #include "ui/views/mus/ax_remote_host.h"
#include "ui/views/mus/mus_client.h" #include "ui/views/mus/mus_client.h"
#include "ui/views/mus/pointer_watcher_event_router.h"
namespace views { namespace views {
...@@ -19,4 +20,19 @@ void MusViewsDelegate::NotifyAccessibilityEvent(View* view, ...@@ -19,4 +20,19 @@ void MusViewsDelegate::NotifyAccessibilityEvent(View* view,
MusClient::Get()->ax_remote_host()->HandleEvent(view, event_type); MusClient::Get()->ax_remote_host()->HandleEvent(view, event_type);
} }
void MusViewsDelegate::AddPointerWatcher(PointerWatcher* pointer_watcher,
bool wants_moves) {
MusClient::Get()->pointer_watcher_event_router()->AddPointerWatcher(
pointer_watcher, wants_moves);
}
void MusViewsDelegate::RemovePointerWatcher(PointerWatcher* pointer_watcher) {
MusClient::Get()->pointer_watcher_event_router()->RemovePointerWatcher(
pointer_watcher);
}
bool MusViewsDelegate::IsPointerWatcherSupported() const {
return true;
}
} // namespace views } // namespace views
...@@ -20,6 +20,10 @@ class VIEWS_MUS_EXPORT MusViewsDelegate : public ViewsDelegate { ...@@ -20,6 +20,10 @@ class VIEWS_MUS_EXPORT MusViewsDelegate : public ViewsDelegate {
// ViewsDelegate: // ViewsDelegate:
void NotifyAccessibilityEvent(View* view, void NotifyAccessibilityEvent(View* view,
ax::mojom::Event event_type) override; ax::mojom::Event event_type) override;
void AddPointerWatcher(PointerWatcher* pointer_watcher,
bool wants_moves) override;
void RemovePointerWatcher(PointerWatcher* pointer_watcher) override;
bool IsPointerWatcherSupported() const override;
private: private:
LayoutProvider layout_provider_; LayoutProvider layout_provider_;
......
// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Copyright 2018 The Chromium Authors. All rights reserved.
// 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 "ui/views/touchui/touch_selection_controller_impl.h" #include "ui/views/touchui/touch_selection_controller_impl.h"
#include "base/macros.h"
#include "base/metrics/histogram_macros.h" #include "base/metrics/histogram_macros.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "ui/aura/client/cursor_client.h" #include "ui/aura/client/cursor_client.h"
...@@ -19,6 +18,7 @@ ...@@ -19,6 +18,7 @@
#include "ui/gfx/image/image.h" #include "ui/gfx/image/image.h"
#include "ui/gfx/path.h" #include "ui/gfx/path.h"
#include "ui/resources/grit/ui_resources.h" #include "ui/resources/grit/ui_resources.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/widget.h" #include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h" #include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/coordinate_conversion.h" #include "ui/wm/core/coordinate_conversion.h"
...@@ -74,14 +74,14 @@ const int kSelectionHandleBarBottomAllowance = 3; ...@@ -74,14 +74,14 @@ const int kSelectionHandleBarBottomAllowance = 3;
// Creates a widget to host SelectionHandleView. // Creates a widget to host SelectionHandleView.
views::Widget* CreateTouchSelectionPopupWidget( views::Widget* CreateTouchSelectionPopupWidget(
gfx::NativeView context, gfx::NativeView parent,
views::WidgetDelegate* widget_delegate) { views::WidgetDelegate* widget_delegate) {
views::Widget* widget = new views::Widget; views::Widget* widget = new views::Widget;
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW;
params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE;
params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
params.parent = context; params.parent = parent;
params.delegate = widget_delegate; params.delegate = widget_delegate;
widget->Init(params); widget->Init(params);
return widget; return widget;
...@@ -210,17 +210,17 @@ using EditingHandleView = TouchSelectionControllerImpl::EditingHandleView; ...@@ -210,17 +210,17 @@ using EditingHandleView = TouchSelectionControllerImpl::EditingHandleView;
// A View that displays the text selection handle. // A View that displays the text selection handle.
class TouchSelectionControllerImpl::EditingHandleView class TouchSelectionControllerImpl::EditingHandleView
: public views::WidgetDelegateView { : public WidgetDelegateView {
public: public:
EditingHandleView(TouchSelectionControllerImpl* controller, EditingHandleView(TouchSelectionControllerImpl* controller,
gfx::NativeView context, gfx::NativeView parent,
bool is_cursor_handle) bool is_cursor_handle)
: controller_(controller), : controller_(controller),
image_(GetCenterHandleImage()), image_(GetCenterHandleImage()),
is_cursor_handle_(is_cursor_handle), is_cursor_handle_(is_cursor_handle),
draw_invisible_(false), draw_invisible_(false),
weak_ptr_factory_(this) { weak_ptr_factory_(this) {
widget_.reset(CreateTouchSelectionPopupWidget(context, this)); widget_.reset(CreateTouchSelectionPopupWidget(parent, this));
targeter_ = new aura::WindowTargeter(); targeter_ = new aura::WindowTargeter();
aura::Window* window = widget_->GetNativeWindow(); aura::Window* window = widget_->GetNativeWindow();
...@@ -240,12 +240,12 @@ class TouchSelectionControllerImpl::EditingHandleView ...@@ -240,12 +240,12 @@ class TouchSelectionControllerImpl::EditingHandleView
return selection_bound_.type(); return selection_bound_.type();
} }
// Overridden from views::WidgetDelegateView: // WidgetDelegateView:
void DeleteDelegate() override { void DeleteDelegate() override {
// We are owned and deleted by TouchSelectionControllerImpl. // We are owned and deleted by TouchSelectionControllerImpl.
} }
// Overridden from views::View: // View:
void OnPaint(gfx::Canvas* canvas) override { void OnPaint(gfx::Canvas* canvas) override {
if (draw_invisible_) if (draw_invisible_)
return; return;
...@@ -405,23 +405,20 @@ class TouchSelectionControllerImpl::EditingHandleView ...@@ -405,23 +405,20 @@ class TouchSelectionControllerImpl::EditingHandleView
TouchSelectionControllerImpl::TouchSelectionControllerImpl( TouchSelectionControllerImpl::TouchSelectionControllerImpl(
ui::TouchEditable* client_view) ui::TouchEditable* client_view)
: client_view_(client_view), : client_view_(client_view),
client_widget_(nullptr), selection_handle_1_(
selection_handle_1_(new EditingHandleView(this, new EditingHandleView(this, client_view->GetNativeView(), false)),
client_view->GetNativeView(), selection_handle_2_(
false)), new EditingHandleView(this, client_view->GetNativeView(), false)),
selection_handle_2_(new EditingHandleView(this, cursor_handle_(
client_view->GetNativeView(), new EditingHandleView(this, client_view->GetNativeView(), true)) {
false)),
cursor_handle_(new EditingHandleView(this,
client_view->GetNativeView(),
true)),
command_executed_(false),
dragging_handle_(nullptr) {
selection_start_time_ = base::TimeTicks::Now(); selection_start_time_ = base::TimeTicks::Now();
aura::Window* client_window = client_view_->GetNativeView(); aura::Window* client_window = client_view_->GetNativeView();
client_widget_ = Widget::GetTopLevelWidgetForNativeView(client_window); client_widget_ = Widget::GetTopLevelWidgetForNativeView(client_window);
// Observe client widget moves and resizes to update the selection handles.
if (client_widget_) if (client_widget_)
client_widget_->AddObserver(this); client_widget_->AddObserver(this);
if (ViewsDelegate::GetInstance()->IsPointerWatcherSupported())
ViewsDelegate::GetInstance()->AddPointerWatcher(this, true);
aura::Env::GetInstance()->AddPreTargetHandler(this); aura::Env::GetInstance()->AddPreTargetHandler(this);
} }
...@@ -430,6 +427,8 @@ TouchSelectionControllerImpl::~TouchSelectionControllerImpl() { ...@@ -430,6 +427,8 @@ TouchSelectionControllerImpl::~TouchSelectionControllerImpl() {
command_executed_); command_executed_);
HideQuickMenu(); HideQuickMenu();
aura::Env::GetInstance()->RemovePreTargetHandler(this); aura::Env::GetInstance()->RemovePreTargetHandler(this);
if (ViewsDelegate::GetInstance()->IsPointerWatcherSupported())
ViewsDelegate::GetInstance()->RemovePointerWatcher(this);
if (client_widget_) if (client_widget_)
client_widget_->RemoveObserver(this); client_widget_->RemoveObserver(this);
} }
...@@ -627,6 +626,18 @@ void TouchSelectionControllerImpl::OnWidgetBoundsChanged( ...@@ -627,6 +626,18 @@ void TouchSelectionControllerImpl::OnWidgetBoundsChanged(
SelectionChanged(); SelectionChanged();
} }
void TouchSelectionControllerImpl::OnPointerEventObserved(
const ui::PointerEvent& event,
const gfx::Point& location_in_screen,
gfx::NativeView target) {
// Disregard CursorClient::IsMouseEventsEnabled, it is disabled for touch
// events in this client, but not re-enabled for mouse events sent elsewhere.
if (event.pointer_details().pointer_type ==
ui::EventPointerType::POINTER_TYPE_MOUSE) {
client_view_->DestroyTouchSelection();
}
}
void TouchSelectionControllerImpl::OnKeyEvent(ui::KeyEvent* event) { void TouchSelectionControllerImpl::OnKeyEvent(ui::KeyEvent* event) {
client_view_->DestroyTouchSelection(); client_view_->DestroyTouchSelection();
} }
...@@ -751,11 +762,11 @@ gfx::Rect TouchSelectionControllerImpl::GetExpectedHandleBounds( ...@@ -751,11 +762,11 @@ gfx::Rect TouchSelectionControllerImpl::GetExpectedHandleBounds(
return GetSelectionWidgetBounds(bound); return GetSelectionWidgetBounds(bound);
} }
views::WidgetDelegateView* TouchSelectionControllerImpl::GetHandle1View() { WidgetDelegateView* TouchSelectionControllerImpl::GetHandle1View() {
return selection_handle_1_.get(); return selection_handle_1_.get();
} }
views::WidgetDelegateView* TouchSelectionControllerImpl::GetHandle2View() { WidgetDelegateView* TouchSelectionControllerImpl::GetHandle2View() {
return selection_handle_2_.get(); return selection_handle_2_.get();
} }
......
// Copyright (c) 2013 The Chromium Authors. All rights reserved. // Copyright 2018 The Chromium Authors. All rights reserved.
// 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.
#ifndef UI_UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_ #ifndef UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_
#define UI_UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_ #define UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_
#include "base/macros.h" #include "base/macros.h"
#include "base/timer/timer.h" #include "base/timer/timer.h"
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "ui/gfx/geometry/point.h" #include "ui/gfx/geometry/point.h"
#include "ui/gfx/selection_bound.h" #include "ui/gfx/selection_bound.h"
#include "ui/touch_selection/touch_selection_menu_runner.h" #include "ui/touch_selection/touch_selection_menu_runner.h"
#include "ui/views/pointer_watcher.h"
#include "ui/views/view.h" #include "ui/views/view.h"
#include "ui/views/views_export.h" #include "ui/views/views_export.h"
#include "ui/views/widget/widget_observer.h" #include "ui/views/widget/widget_observer.h"
...@@ -29,17 +30,16 @@ class VIEWS_EXPORT TouchSelectionControllerImpl ...@@ -29,17 +30,16 @@ class VIEWS_EXPORT TouchSelectionControllerImpl
: public ui::TouchEditingControllerDeprecated, : public ui::TouchEditingControllerDeprecated,
public ui::TouchSelectionMenuClient, public ui::TouchSelectionMenuClient,
public WidgetObserver, public WidgetObserver,
public PointerWatcher,
public ui::EventHandler { public ui::EventHandler {
public: public:
class EditingHandleView; class EditingHandleView;
// Use TextSelectionController::create(). // Use ui::TouchEditingControllerFactory::Create() instead.
explicit TouchSelectionControllerImpl( explicit TouchSelectionControllerImpl(ui::TouchEditable* client_view);
ui::TouchEditable* client_view);
~TouchSelectionControllerImpl() override; ~TouchSelectionControllerImpl() override;
// TextSelectionController. // ui::TouchEditingControllerDeprecated:
void SelectionChanged() override; void SelectionChanged() override;
bool IsHandleDragInProgress() override; bool IsHandleDragInProgress() override;
void HideHandles(bool quick) override; void HideHandles(bool quick) override;
...@@ -70,19 +70,22 @@ class VIEWS_EXPORT TouchSelectionControllerImpl ...@@ -70,19 +70,22 @@ class VIEWS_EXPORT TouchSelectionControllerImpl
// |bound| should be the clipped version of the selection bound. // |bound| should be the clipped version of the selection bound.
bool ShouldShowHandleFor(const gfx::SelectionBound& bound) const; bool ShouldShowHandleFor(const gfx::SelectionBound& bound) const;
// Overridden from ui::TouchSelectionMenuClient. // ui::TouchSelectionMenuClient:
bool IsCommandIdEnabled(int command_id) const override; bool IsCommandIdEnabled(int command_id) const override;
void ExecuteCommand(int command_id, int event_flags) override; void ExecuteCommand(int command_id, int event_flags) override;
void RunContextMenu() override; void RunContextMenu() override;
// Overridden from WidgetObserver. We will observe the widget backing the // WidgetObserver:
// |client_view_| so that when its moved/resized, we can update the selection
// handles appropriately.
void OnWidgetClosing(Widget* widget) override; void OnWidgetClosing(Widget* widget) override;
void OnWidgetBoundsChanged(Widget* widget, void OnWidgetBoundsChanged(Widget* widget,
const gfx::Rect& new_bounds) override; const gfx::Rect& new_bounds) override;
// Overriden from ui::EventHandler. // PointerWatcher:
void OnPointerEventObserved(const ui::PointerEvent& event,
const gfx::Point& location_in_screen,
gfx::NativeView target) override;
// ui::EventHandler:
void OnKeyEvent(ui::KeyEvent* event) override; void OnKeyEvent(ui::KeyEvent* event) override;
void OnMouseEvent(ui::MouseEvent* event) override; void OnMouseEvent(ui::MouseEvent* event) override;
void OnScrollEvent(ui::ScrollEvent* event) override; void OnScrollEvent(ui::ScrollEvent* event) override;
...@@ -112,15 +115,15 @@ class VIEWS_EXPORT TouchSelectionControllerImpl ...@@ -112,15 +115,15 @@ class VIEWS_EXPORT TouchSelectionControllerImpl
bool IsSelectionHandle2Visible(); bool IsSelectionHandle2Visible();
bool IsCursorHandleVisible(); bool IsCursorHandleVisible();
gfx::Rect GetExpectedHandleBounds(const gfx::SelectionBound& bound); gfx::Rect GetExpectedHandleBounds(const gfx::SelectionBound& bound);
views::WidgetDelegateView* GetHandle1View(); WidgetDelegateView* GetHandle1View();
views::WidgetDelegateView* GetHandle2View(); WidgetDelegateView* GetHandle2View();
ui::TouchEditable* client_view_; ui::TouchEditable* client_view_;
Widget* client_widget_; Widget* client_widget_ = nullptr;
std::unique_ptr<EditingHandleView> selection_handle_1_; std::unique_ptr<EditingHandleView> selection_handle_1_;
std::unique_ptr<EditingHandleView> selection_handle_2_; std::unique_ptr<EditingHandleView> selection_handle_2_;
std::unique_ptr<EditingHandleView> cursor_handle_; std::unique_ptr<EditingHandleView> cursor_handle_;
bool command_executed_; bool command_executed_ = false;
base::TimeTicks selection_start_time_; base::TimeTicks selection_start_time_;
// Timer to trigger quick menu (Quick menu is not shown if the selection // Timer to trigger quick menu (Quick menu is not shown if the selection
...@@ -129,7 +132,7 @@ class VIEWS_EXPORT TouchSelectionControllerImpl ...@@ -129,7 +132,7 @@ class VIEWS_EXPORT TouchSelectionControllerImpl
base::OneShotTimer quick_menu_timer_; base::OneShotTimer quick_menu_timer_;
// Pointer to the SelectionHandleView being dragged during a drag session. // Pointer to the SelectionHandleView being dragged during a drag session.
EditingHandleView* dragging_handle_; EditingHandleView* dragging_handle_ = nullptr;
// In cursor mode, the two selection bounds are the same and correspond to // In cursor mode, the two selection bounds are the same and correspond to
// |cursor_handle_|; otherwise, they correspond to |selection_handle_1_| and // |cursor_handle_|; otherwise, they correspond to |selection_handle_1_| and
...@@ -148,4 +151,4 @@ class VIEWS_EXPORT TouchSelectionControllerImpl ...@@ -148,4 +151,4 @@ class VIEWS_EXPORT TouchSelectionControllerImpl
} // namespace views } // namespace views
#endif // UI_UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_ #endif // UI_VIEWS_TOUCHUI_TOUCH_SELECTION_CONTROLLER_IMPL_H_
...@@ -48,8 +48,7 @@ ViewsDelegate* ViewsDelegate::GetInstance() { ...@@ -48,8 +48,7 @@ ViewsDelegate* ViewsDelegate::GetInstance() {
void ViewsDelegate::SaveWindowPlacement(const Widget* widget, void ViewsDelegate::SaveWindowPlacement(const Widget* widget,
const std::string& window_name, const std::string& window_name,
const gfx::Rect& bounds, const gfx::Rect& bounds,
ui::WindowShowState show_state) { ui::WindowShowState show_state) {}
}
bool ViewsDelegate::GetSavedWindowPlacement( bool ViewsDelegate::GetSavedWindowPlacement(
const Widget* widget, const Widget* widget,
...@@ -66,8 +65,7 @@ void ViewsDelegate::NotifyMenuItemFocused(const base::string16& menu_name, ...@@ -66,8 +65,7 @@ void ViewsDelegate::NotifyMenuItemFocused(const base::string16& menu_name,
const base::string16& menu_item_name, const base::string16& menu_item_name,
int item_index, int item_index,
int item_count, int item_count,
bool has_submenu) { bool has_submenu) {}
}
ViewsDelegate::ProcessMenuAcceleratorResult ViewsDelegate::ProcessMenuAcceleratorResult
ViewsDelegate::ProcessAcceleratorWhileMenuShowing( ViewsDelegate::ProcessAcceleratorWhileMenuShowing(
...@@ -98,11 +96,9 @@ NonClientFrameView* ViewsDelegate::CreateDefaultNonClientFrameView( ...@@ -98,11 +96,9 @@ NonClientFrameView* ViewsDelegate::CreateDefaultNonClientFrameView(
return nullptr; return nullptr;
} }
void ViewsDelegate::AddRef() { void ViewsDelegate::AddRef() {}
}
void ViewsDelegate::ReleaseRef() { void ViewsDelegate::ReleaseRef() {}
}
void ViewsDelegate::OnBeforeWidgetInit( void ViewsDelegate::OnBeforeWidgetInit(
Widget::InitParams* params, Widget::InitParams* params,
...@@ -140,4 +136,16 @@ bool ViewsDelegate::ShouldMirrorArrowsInRTL() const { ...@@ -140,4 +136,16 @@ bool ViewsDelegate::ShouldMirrorArrowsInRTL() const {
return true; return true;
} }
void ViewsDelegate::AddPointerWatcher(PointerWatcher*, bool) {
NOTREACHED();
}
void ViewsDelegate::RemovePointerWatcher(PointerWatcher*) {
NOTREACHED();
}
bool ViewsDelegate::IsPointerWatcherSupported() const {
return false;
}
} // namespace views } // namespace views
...@@ -34,13 +34,14 @@ class Rect; ...@@ -34,13 +34,14 @@ class Rect;
namespace ui { namespace ui {
class ContextFactory; class ContextFactory;
class TouchEditingControllerFactory;
} }
namespace views { namespace views {
class NativeWidget; class NativeWidget;
class NonClientFrameView; class NonClientFrameView;
class ViewsTouchEditingControllerFactory; class PointerWatcher;
class View; class View;
class Widget; class Widget;
...@@ -206,11 +207,18 @@ class VIEWS_EXPORT ViewsDelegate { ...@@ -206,11 +207,18 @@ class VIEWS_EXPORT ViewsDelegate {
// opens in the opposite direction. // opens in the opposite direction.
virtual bool ShouldMirrorArrowsInRTL() const; virtual bool ShouldMirrorArrowsInRTL() const;
// Allows lower-level views components to use Mus-only PointerWatcher wiring.
// TODO(crbug.com/887725): Support PointerWatcher without mus, refactor.
virtual void AddPointerWatcher(PointerWatcher* pointer_watcher,
bool wants_moves);
virtual void RemovePointerWatcher(PointerWatcher* pointer_watcher);
virtual bool IsPointerWatcherSupported() const;
protected: protected:
ViewsDelegate(); ViewsDelegate();
private: private:
std::unique_ptr<ViewsTouchEditingControllerFactory> std::unique_ptr<ui::TouchEditingControllerFactory>
editing_controller_factory_; editing_controller_factory_;
#if defined(USE_AURA) #if defined(USE_AURA)
......
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