Commit 3b1e224a authored by b.kelemen@samsung.com's avatar b.kelemen@samsung.com

Deduplicate DeviceEvent* classes

r175792 did a good job on refactoring this for the most parts, but it didn't do
so well by leaving behind the old classes only for NavigatorGamepad. This CL
trasitions NavigatorGamepad to the new base classes and and removes the code
duplication.
Also removeEventListener fixed not to assume that it was the last listener.

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

git-svn-id: svn://svn.chromium.org/blink/trunk@176482 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent e02c3f95
Test that when there are multiple gamepad event listeners and one is getting removed the others still receive events.
On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
PASS gamepadconnected event received
PASS successfullyParsed is true
TEST COMPLETE
<!DOCTYPE html>
<html>
<body>
<script src="../resources/js-test.js"></script>
<script>
description("Test that when there are multiple gamepad event listeners and one is getting removed the others still receive events.");
window.jsTestIsAsync = true;
if (!window.gamepadController) {
debug("This test cannot work without gamepadController.");
}
var listenerToRemove = window.addEventListener('gamepadconnected', function() {});
window.addEventListener('gamepadconnected', function() {
testPassed("gamepadconnected event received");
finishJSTest();
});
window.removeEventListener('gamepadconnected', listenerToRemove);
setTimeout(function() {
testFailed("gamepadconnected event should have been recieved.");
finishJSTest();
}, 50);
if (gamepadController) {
gamepadController.connect(0);
gamepadController.dispatchConnected(0);
}
</script>
</html>
......@@ -1153,10 +1153,6 @@
'frame/DeviceEventControllerBase.h',
'frame/DeviceEventDispatcherBase.cpp',
'frame/DeviceEventDispatcherBase.h',
'frame/DeviceSensorEventController.cpp',
'frame/DeviceSensorEventController.h',
'frame/DeviceSensorEventDispatcher.cpp',
'frame/DeviceSensorEventDispatcher.h',
'frame/DeviceSingleWindowEventController.cpp',
'frame/DeviceSingleWindowEventController.h',
'frame/EventHandlerRegistry.cpp',
......
/*
* Copyright 2010 Apple Inc. All rights reserved.
* Copyright (C) 2012 Samsung Electronics. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "core/frame/DeviceSensorEventController.h"
#include "core/dom/Document.h"
#include "core/frame/DOMWindow.h"
#include "core/page/Page.h"
namespace WebCore {
DeviceSensorEventController::DeviceSensorEventController(Page* page)
: PageLifecycleObserver(page)
, m_hasEventListener(false)
, m_isActive(false)
, m_needsCheckingNullEvents(true)
, m_timer(this, &DeviceSensorEventController::fireDeviceEvent)
{
}
DeviceSensorEventController::~DeviceSensorEventController()
{
}
void DeviceSensorEventController::fireDeviceEvent(Timer<DeviceSensorEventController>* timer)
{
ASSERT_UNUSED(timer, timer == &m_timer);
ASSERT(hasLastData());
m_timer.stop();
dispatchDeviceEvent(getLastEvent());
}
void DeviceSensorEventController::dispatchDeviceEvent(PassRefPtrWillBeRawPtr<Event> prpEvent)
{
Document* targetDocument = document();
if (!targetDocument || !targetDocument->domWindow() || targetDocument->activeDOMObjectsAreSuspended() || targetDocument->activeDOMObjectsAreStopped())
return;
RefPtrWillBeRawPtr<Event> event = prpEvent;
targetDocument->domWindow()->dispatchEvent(event);
if (m_needsCheckingNullEvents) {
if (isNullEvent(event.get()))
stopUpdating();
else
m_needsCheckingNullEvents = false;
}
}
void DeviceSensorEventController::startUpdating()
{
if (m_isActive)
return;
if (hasLastData() && !m_timer.isActive()) {
// Make sure to fire the data as soon as possible.
m_timer.startOneShot(0, FROM_HERE);
}
registerWithDispatcher();
m_isActive = true;
}
void DeviceSensorEventController::stopUpdating()
{
if (!m_isActive)
return;
if (m_timer.isActive())
m_timer.stop();
unregisterWithDispatcher();
m_isActive = false;
}
void DeviceSensorEventController::pageVisibilityChanged()
{
if (!m_hasEventListener)
return;
if (page()->visibilityState() == PageVisibilityStateVisible)
startUpdating();
else
stopUpdating();
}
} // namespace WebCore
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef DeviceSensorEventController_h
#define DeviceSensorEventController_h
#include "core/events/Event.h"
#include "core/page/PageLifecycleObserver.h"
#include "platform/Timer.h"
namespace WebCore {
class Document;
class DeviceSensorEventController : public PageLifecycleObserver {
public:
void startUpdating();
void stopUpdating();
protected:
explicit DeviceSensorEventController(Page*);
virtual ~DeviceSensorEventController();
void dispatchDeviceEvent(const PassRefPtrWillBeRawPtr<Event>);
virtual bool hasLastData() = 0;
virtual PassRefPtrWillBeRawPtr<Event> getLastEvent() = 0;
virtual void registerWithDispatcher() = 0;
virtual void unregisterWithDispatcher() = 0;
virtual bool isNullEvent(Event*) = 0;
virtual Document* document() = 0;
bool m_hasEventListener;
private:
// Inherited from PageLifecycleObserver.
virtual void pageVisibilityChanged() OVERRIDE FINAL;
void fireDeviceEvent(Timer<DeviceSensorEventController>*);
bool m_isActive;
bool m_needsCheckingNullEvents;
Timer<DeviceSensorEventController> m_timer;
};
} // namespace WebCore
#endif // DeviceSensorEventController_h
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "core/frame/DeviceSensorEventDispatcher.h"
namespace WebCore {
DeviceSensorEventDispatcher::DeviceSensorEventDispatcher()
: m_needsPurge(false)
, m_isDispatching(false)
{
}
DeviceSensorEventDispatcher::~DeviceSensorEventDispatcher()
{
}
void DeviceSensorEventDispatcher::addController(DeviceSensorEventController* controller)
{
bool wasEmpty = m_controllers.isEmpty();
if (!m_controllers.contains(controller))
m_controllers.append(controller);
if (wasEmpty)
startListening();
}
void DeviceSensorEventDispatcher::removeController(DeviceSensorEventController* controller)
{
// Do not actually remove the controller from the vector, instead zero them out.
// The zeros are removed in these two cases:
// 1. either immediately if we are not dispatching any events,
// 2. or after events to all controllers have dispatched
// (see e.g. DeviceOrientationDispatcher::didChangeDeviceOrientation).
// This is to correctly handle the re-entrancy case when a controller is destroyed
// while the events are still being dispatched.
size_t index = m_controllers.find(controller);
if (index == kNotFound)
return;
m_controllers[index] = 0;
m_needsPurge = true;
if (!m_isDispatching)
purgeControllers();
}
void DeviceSensorEventDispatcher::purgeControllers()
{
ASSERT(m_needsPurge);
size_t i = 0;
while (i < m_controllers.size()) {
if (!m_controllers[i]) {
m_controllers[i] = m_controllers.last();
m_controllers.removeLast();
} else {
++i;
}
}
m_needsPurge = false;
if (m_controllers.isEmpty())
stopListening();
}
} // namespace WebCore
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef DeviceSensorEventDispatcher_h
#define DeviceSensorEventDispatcher_h
#include "wtf/Vector.h"
namespace WebCore {
class DeviceSensorEventController;
class DeviceSensorEventDispatcher {
protected:
DeviceSensorEventDispatcher();
virtual ~DeviceSensorEventDispatcher();
void addController(DeviceSensorEventController*);
void removeController(DeviceSensorEventController*);
void purgeControllers();
virtual void startListening() = 0;
virtual void stopListening() = 0;
Vector<DeviceSensorEventController*> m_controllers;
bool m_needsPurge;
bool m_isDispatching;
};
} // namespace WebCore
#endif // DeviceSensorEventDispatcher_h
......@@ -5,7 +5,6 @@
#include "config.h"
#include "modules/device_light/DeviceLightDispatcher.h"
#include "core/frame/DeviceSensorEventDispatcher.h"
#include "modules/device_light/DeviceLightController.h"
#include "public/platform/Platform.h"
......
......@@ -17,19 +17,8 @@ GamepadDispatcher& GamepadDispatcher::instance()
return gamepadDispatcher;
}
void GamepadDispatcher::addClient(NavigatorGamepad* client)
{
addController(client);
}
void GamepadDispatcher::removeClient(NavigatorGamepad* client)
{
removeController(client);
}
void GamepadDispatcher::sampleGamepads(blink::WebGamepads& gamepads)
{
ASSERT(!m_controllers.isEmpty());
blink::Platform::current()->sampleGamepads(gamepads);
}
......@@ -53,18 +42,12 @@ void GamepadDispatcher::didDisconnectGamepad(unsigned index, const blink::WebGam
void GamepadDispatcher::dispatchDidConnectOrDisconnectGamepad(unsigned index, const blink::WebGamepad& gamepad, bool connected)
{
{
TemporaryChange<bool> changeIsDispatching(m_isDispatching, true);
// Don't fire controllers removed or added during event dispatch.
size_t size = m_controllers.size();
for (size_t i = 0; i < size; ++i) {
if (m_controllers[i])
static_cast<NavigatorGamepad*>(m_controllers[i])->didConnectOrDisconnectGamepad(index, gamepad, connected);
}
}
ASSERT(index < blink::WebGamepads::itemsLengthCap);
ASSERT(connected == gamepad.connected);
if (m_needsPurge)
purgeControllers();
m_latestChange.pad = gamepad;
m_latestChange.index = index;
notifyControllers();
}
void GamepadDispatcher::startListening()
......
......@@ -5,11 +5,12 @@
#ifndef GamepadDispatcher_h
#define GamepadDispatcher_h
#include "core/frame/DeviceSensorEventDispatcher.h"
#include "core/frame/DeviceEventDispatcherBase.h"
#include "platform/heap/Handle.h"
#include "public/platform/WebGamepad.h"
#include "public/platform/WebGamepadListener.h"
namespace blink {
class WebGamepad;
class WebGamepads;
}
......@@ -17,24 +18,34 @@ namespace WebCore {
class NavigatorGamepad;
class GamepadDispatcher : public DeviceSensorEventDispatcher, public blink::WebGamepadListener {
class GamepadDispatcher : public DeviceEventDispatcherBase, public blink::WebGamepadListener {
public:
static GamepadDispatcher& instance();
void addClient(NavigatorGamepad*);
void removeClient(NavigatorGamepad*);
void sampleGamepads(blink::WebGamepads&);
struct ConnectionChange {
blink::WebGamepad pad;
unsigned index;
};
const ConnectionChange& latestConnectionChange() const { return m_latestChange; }
private:
GamepadDispatcher();
virtual ~GamepadDispatcher();
// WebGamepadListener
virtual void didConnectGamepad(unsigned index, const blink::WebGamepad&) OVERRIDE;
virtual void didDisconnectGamepad(unsigned index, const blink::WebGamepad&) OVERRIDE;
void dispatchDidConnectOrDisconnectGamepad(unsigned index, const blink::WebGamepad&, bool connected);
// DeviceEventDispatcherBase
virtual void startListening() OVERRIDE;
virtual void stopListening() OVERRIDE;
void dispatchDidConnectOrDisconnectGamepad(unsigned index, const blink::WebGamepad&, bool connected);
ConnectionChange m_latestChange;
};
} // namespace WebCore
......
......@@ -129,11 +129,8 @@ void NavigatorGamepad::trace(Visitor* visitor)
WillBeHeapSupplement<Navigator>::trace(visitor);
}
void NavigatorGamepad::didConnectOrDisconnectGamepad(unsigned index, const blink::WebGamepad& webGamepad, bool connected)
void NavigatorGamepad::didUpdateData()
{
ASSERT(index < blink::WebGamepads::itemsLengthCap);
ASSERT(connected == webGamepad.connected);
// We should stop listening once we detached.
ASSERT(window());
......@@ -144,23 +141,24 @@ void NavigatorGamepad::didConnectOrDisconnectGamepad(unsigned index, const blink
if (window()->document()->activeDOMObjectsAreStopped() || window()->document()->activeDOMObjectsAreSuspended())
return;
const GamepadDispatcher::ConnectionChange& change = GamepadDispatcher::instance().latestConnectionChange();
if (!m_gamepads)
m_gamepads = GamepadList::create();
Gamepad* gamepad = m_gamepads->item(index);
Gamepad* gamepad = m_gamepads->item(change.index);
if (!gamepad)
gamepad = Gamepad::create();
sampleGamepad(index, *gamepad, webGamepad);
m_gamepads->set(index, gamepad);
sampleGamepad(change.index, *gamepad, change.pad);
m_gamepads->set(change.index, gamepad);
const AtomicString& eventName = connected ? EventTypeNames::gamepadconnected : EventTypeNames::gamepaddisconnected;
RefPtrWillBeRawPtr<GamepadEvent> event = GamepadEvent::create(eventName, false, true, gamepad);
window()->dispatchEvent(event);
const AtomicString& eventName = change.pad.connected ? EventTypeNames::gamepadconnected : EventTypeNames::gamepaddisconnected;
window()->dispatchEvent(GamepadEvent::create(eventName, false, true, gamepad));
}
NavigatorGamepad::NavigatorGamepad(LocalFrame* frame)
: DOMWindowProperty(frame)
, DeviceSensorEventController(frame ? frame->page() : 0)
, DeviceEventControllerBase(frame ? frame->page() : 0)
, DOMWindowLifecycleObserver(frame ? frame->domWindow() : 0)
{
}
......@@ -188,12 +186,12 @@ void NavigatorGamepad::willDetachGlobalObjectFromFrame()
void NavigatorGamepad::registerWithDispatcher()
{
GamepadDispatcher::instance().addClient(this);
GamepadDispatcher::instance().addController(this);
}
void NavigatorGamepad::unregisterWithDispatcher()
{
GamepadDispatcher::instance().removeClient(this);
GamepadDispatcher::instance().removeController(this);
}
bool NavigatorGamepad::hasLastData()
......@@ -202,25 +200,6 @@ bool NavigatorGamepad::hasLastData()
return false;
}
PassRefPtrWillBeRawPtr<Event> NavigatorGamepad::getLastEvent()
{
// This is called only when hasLastData() is true.
ASSERT_NOT_REACHED();
return nullptr;
}
bool NavigatorGamepad::isNullEvent(Event*)
{
// This is called only when hasLastData() is true.
ASSERT_NOT_REACHED();
return false;
}
Document* NavigatorGamepad::document()
{
return window() ? window()->document() : 0;
}
static bool isGamepadEvent(const AtomicString& eventType)
{
return eventType == EventTypeNames::gamepadconnected || eventType == EventTypeNames::gamepaddisconnected;
......@@ -235,10 +214,13 @@ void NavigatorGamepad::didAddEventListener(DOMWindow*, const AtomicString& event
}
}
void NavigatorGamepad::didRemoveEventListener(DOMWindow*, const AtomicString& eventType)
void NavigatorGamepad::didRemoveEventListener(DOMWindow* window, const AtomicString& eventType)
{
if (isGamepadEvent(eventType))
if (isGamepadEvent(eventType)
&& !window->hasEventListeners(EventTypeNames::gamepadconnected)
&& !window->hasEventListeners(EventTypeNames::gamepaddisconnected)) {
m_hasEventListener = false;
}
}
void NavigatorGamepad::didRemoveAllEventListeners(DOMWindow*)
......
......@@ -28,7 +28,7 @@
#include "core/frame/DOMWindowLifecycleObserver.h"
#include "core/frame/DOMWindowProperty.h"
#include "core/frame/DeviceSensorEventController.h"
#include "core/frame/DeviceEventControllerBase.h"
#include "platform/Supplementable.h"
#include "platform/heap/Handle.h"
#include "public/platform/WebGamepads.h"
......@@ -40,11 +40,12 @@ class WebGamepads;
namespace WebCore {
class Document;
class GamepadList;
class Navigator;
class WebKitGamepadList;
class NavigatorGamepad FINAL : public NoBaseWillBeGarbageCollectedFinalized<NavigatorGamepad>, public WillBeHeapSupplement<Navigator>, public DOMWindowProperty, public DeviceSensorEventController, public DOMWindowLifecycleObserver {
class NavigatorGamepad FINAL : public NoBaseWillBeGarbageCollectedFinalized<NavigatorGamepad>, public WillBeHeapSupplement<Navigator>, public DOMWindowProperty, public DeviceEventControllerBase, public DOMWindowLifecycleObserver {
WILL_BE_USING_GARBAGE_COLLECTED_MIXIN(NavigatorGamepad);
public:
static NavigatorGamepad* from(Document&);
......@@ -70,13 +71,11 @@ private:
virtual void willDestroyGlobalObjectInFrame() OVERRIDE;
virtual void willDetachGlobalObjectFromFrame() OVERRIDE;
// DeviceSensorEventController
// DeviceEventControllerBase
virtual void registerWithDispatcher() OVERRIDE;
virtual void unregisterWithDispatcher() OVERRIDE;
virtual bool hasLastData() OVERRIDE;
virtual PassRefPtrWillBeRawPtr<Event> getLastEvent() OVERRIDE;
virtual bool isNullEvent(Event*) OVERRIDE;
virtual Document* document() OVERRIDE;
virtual void didUpdateData() OVERRIDE;
// DOMWindowLifecycleObserver
virtual void didAddEventListener(DOMWindow*, const AtomicString&) OVERRIDE;
......
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