Commit ef0975b0 authored by Navid Zolghadr's avatar Navid Zolghadr Committed by Commit Bot

Use async targeting to find drag and drop targets

Add callback supports to the RenderWidgetTargeter
and use the async targetting in drag and drop code
and register a callback to send drag events using
the callback. Note that since drag and drop events
aren't WebInputEvent it is not possible to use
FindTargetAndDispatch function of RenderWidgetTargeter
at this time.

Bug: 804633
Change-Id: I2ecea2b70d7ea619169685a3a7b531355ddc0d59
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1596840
Commit-Queue: Navid Zolghadr <nzolghadr@chromium.org>
Reviewed-by: default avatarSadrul Chowdhury <sadrul@chromium.org>
Cr-Commit-Position: refs/heads/master@{#665239}
parent 678c372a
......@@ -1389,6 +1389,13 @@ RenderWidgetHostInputEventRouter::GetRenderWidgetHostAtPoint(
.view->GetRenderWidgetHost());
}
void RenderWidgetHostInputEventRouter::GetRenderWidgetHostAtPointAsynchronously(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& point,
RenderWidgetTargeter::RenderWidgetHostAtPointCallback callback) {
event_targeter_->FindTargetAndCallback(root_view, point, std::move(callback));
}
RenderWidgetTargetResult
RenderWidgetHostInputEventRouter::FindTouchscreenGestureEventTarget(
RenderWidgetHostViewBase* root_view,
......@@ -1770,6 +1777,15 @@ RenderWidgetHostInputEventRouter::GetRenderWidgetTargeterForTests() {
return event_targeter_.get();
}
RenderWidgetTargetResult
RenderWidgetHostInputEventRouter::FindTargetSynchronouslyAtLocation(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& location) {
gfx::PointF transformed_pt; // This is already in the result
return FindViewAtLocation(root_view, location, gfx::PointF() /* not used */,
viz::EventSource::MOUSE, &transformed_pt);
}
RenderWidgetTargetResult
RenderWidgetHostInputEventRouter::FindTargetSynchronously(
RenderWidgetHostViewBase* root_view,
......
......@@ -135,6 +135,17 @@ class CONTENT_EXPORT RenderWidgetHostInputEventRouter
const gfx::PointF& point,
gfx::PointF* transformed_point);
// Finds the RenderWidgetHostImpl inside the |root_view| at |point| where
// |point| is with respect to |root_view|'s coordinates. If a RWHI is found,
// it is passed along with the coordinate of the point with
// respect to the RWHI's coordinates to the callback function. If
// |root_view| is nullptr or RWHI is not found, the callback is called with
// nullptr and no location.
void GetRenderWidgetHostAtPointAsynchronously(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& point,
RenderWidgetTargeter::RenderWidgetHostAtPointCallback callback);
// RenderWidgetTargeter::Delegate:
RenderWidgetHostViewBase* FindViewFromFrameSinkId(
const viz::FrameSinkId& frame_sink_id) const override;
......@@ -312,6 +323,10 @@ class CONTENT_EXPORT RenderWidgetHostInputEventRouter
const RenderWidgetHostViewBase* view);
// RenderWidgetTargeter::Delegate:
RenderWidgetTargetResult FindTargetSynchronouslyAtLocation(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& location) override;
RenderWidgetTargetResult FindTargetSynchronously(
RenderWidgetHostViewBase* root_view,
const blink::WebInputEvent& event) override;
......
......@@ -22,17 +22,6 @@ namespace content {
namespace {
bool MergeEventIfPossible(const blink::WebInputEvent& event,
ui::WebScopedInputEvent* blink_event) {
if (!blink::WebInputEvent::IsTouchEventType(event.GetType()) &&
!blink::WebInputEvent::IsGestureEventType(event.GetType()) &&
ui::CanCoalesce(event, **blink_event)) {
ui::Coalesce(event, blink_event->get());
return true;
}
return false;
}
gfx::PointF ComputeEventLocation(const blink::WebInputEvent& event) {
if (blink::WebInputEvent::IsMouseEventType(event.GetType()) ||
event.GetType() == blink::WebInputEvent::kMouseWheel) {
......@@ -118,7 +107,24 @@ RenderWidgetTargetResult::RenderWidgetTargetResult(
RenderWidgetTargetResult::~RenderWidgetTargetResult() = default;
RenderWidgetTargeter::TargetingRequest::TargetingRequest() = default;
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
base::WeakPtr<RenderWidgetHostViewBase> root_view,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency) {
this->root_view = std::move(root_view);
this->location = ComputeEventLocation(event);
this->event = ui::WebInputEventTraits::Clone(event);
this->latency = latency;
}
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
base::WeakPtr<RenderWidgetHostViewBase> root_view,
const gfx::PointF& location,
RenderWidgetHostAtPointCallback callback) {
this->root_view = std::move(root_view);
this->location = location;
this->callback = std::move(callback);
}
RenderWidgetTargeter::TargetingRequest::TargetingRequest(
TargetingRequest&& request) = default;
......@@ -128,6 +134,68 @@ operator=(TargetingRequest&&) = default;
RenderWidgetTargeter::TargetingRequest::~TargetingRequest() = default;
void RenderWidgetTargeter::TargetingRequest::RunCallback(
RenderWidgetHostViewBase* target,
base::Optional<gfx::PointF> point) {
if (!callback.is_null()) {
std::move(callback).Run(target ? target->GetWeakPtr() : nullptr, point);
}
}
bool RenderWidgetTargeter::TargetingRequest::MergeEventIfPossible(
const blink::WebInputEvent& new_event) {
if (event && !blink::WebInputEvent::IsTouchEventType(new_event.GetType()) &&
!blink::WebInputEvent::IsGestureEventType(new_event.GetType()) &&
ui::CanCoalesce(new_event, *event.get())) {
ui::Coalesce(new_event, event.get());
return true;
}
return false;
}
void RenderWidgetTargeter::TargetingRequest::StartQueueingTimeTracker() {
tracker =
std::make_unique<TracingUmaTracker>("Event.AsyncTargeting.TimeInQueue");
}
void RenderWidgetTargeter::TargetingRequest::StopQueueingTimeTracker() {
if (tracker)
tracker->Stop();
}
bool RenderWidgetTargeter::TargetingRequest::IsWebInputEventRequest() const {
return !!event;
}
const blink::WebInputEvent& RenderWidgetTargeter::TargetingRequest::GetEvent()
const {
return *event.get();
}
RenderWidgetHostViewBase* RenderWidgetTargeter::TargetingRequest::GetRootView()
const {
return root_view.get();
}
gfx::PointF RenderWidgetTargeter::TargetingRequest::GetLocation() const {
return location;
}
viz::FrameSinkId
RenderWidgetTargeter::TargetingRequest::GetExpectedFrameSinkId() const {
return expected_frame_sink_id;
}
void RenderWidgetTargeter::TargetingRequest::SetExpectedFrameSinkId(
const viz::FrameSinkId& id) {
expected_frame_sink_id = id;
}
const ui::LatencyInfo& RenderWidgetTargeter::TargetingRequest::GetLatency()
const {
return latency;
}
RenderWidgetTargeter::RenderWidgetTargeter(Delegate* delegate)
: trace_id_(base::RandUint64()),
is_viz_hit_testing_debug_enabled_(
......@@ -152,35 +220,48 @@ void RenderWidgetTargeter::FindTargetAndDispatch(
static_cast<const blink::WebGestureEvent&>(event).SourceDevice() ==
blink::WebGestureDevice::kTouchpad)));
if (request_in_flight_) {
if (!requests_.empty()) {
auto& request = requests_.back();
if (MergeEventIfPossible(event, &request.event))
if (request.MergeEventIfPossible(event))
return;
}
TargetingRequest request;
request.root_view = root_view->GetWeakPtr();
request.event = ui::WebInputEventTraits::Clone(event);
request.latency = latency;
request.tracker =
std::make_unique<TracingUmaTracker>("Event.AsyncTargeting.TimeInQueue");
TargetingRequest request(root_view->GetWeakPtr(), event, latency);
ResolveTargetingRequest(std::move(request));
}
void RenderWidgetTargeter::FindTargetAndCallback(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& point,
RenderWidgetHostAtPointCallback callback) {
TargetingRequest request(root_view->GetWeakPtr(), point, std::move(callback));
ResolveTargetingRequest(std::move(request));
}
void RenderWidgetTargeter::ResolveTargetingRequest(TargetingRequest request) {
if (request_in_flight_) {
request.StartQueueingTimeTracker();
requests_.push(std::move(request));
return;
}
RenderWidgetTargetResult result =
delegate_->FindTargetSynchronously(root_view, event);
const gfx::PointF event_location = ComputeEventLocation(event);
RenderWidgetTargetResult result;
if (request.IsWebInputEventRequest()) {
result = delegate_->FindTargetSynchronously(request.GetRootView(),
request.GetEvent());
} else {
result = delegate_->FindTargetSynchronouslyAtLocation(
request.GetRootView(), request.GetLocation());
}
RenderWidgetHostViewBase* target = result.view;
auto* event_ptr = &event;
async_depth_ = 0;
if (result.should_query_view) {
TRACE_EVENT_WITH_FLOW2(
"viz,benchmark", "Event.Pipeline", TRACE_ID_GLOBAL(trace_id_),
TRACE_EVENT_FLAG_FLOW_OUT, "step", "QueryClient(Start)",
"event_location", event_location.ToString());
"event_location", request.GetLocation().ToString());
// TODO(kenrb, sadrul): When all event types support asynchronous hit
// testing, we should be able to have FindTargetSynchronously return the
......@@ -190,17 +271,15 @@ void RenderWidgetTargeter::FindTargetAndDispatch(
// root_view and the original event location for the initial query.
// Do not compare hit test results if we are forced to do async hit testing
// by HitTestQuery.
QueryClient(root_view, root_view, *event_ptr, latency, event_location,
nullptr, gfx::PointF());
QueryClient(std::move(request));
} else {
FoundTarget(root_view, target, *event_ptr, latency, result.target_location,
result.latched_target, viz::FrameSinkId());
FoundTarget(target, result.target_location, result.latched_target,
&request);
// Verify the event targeting results from surface layer viz hit testing if
// --use-viz-hit-test-surface-layer is enabled.
if (result.should_verify_result && !target->IsRenderWidgetHostViewGuest()) {
QueryAndVerifyClient(root_view, root_view, *event_ptr, latency,
event_location, nullptr, gfx::PointF(),
target->GetFrameSinkId());
request.SetExpectedFrameSinkId(target->GetFrameSinkId());
QueryAndVerifyClient(std::move(request));
}
}
}
......@@ -214,17 +293,14 @@ bool RenderWidgetTargeter::HasEventsPendingDispatch() const {
}
void RenderWidgetTargeter::QueryClientInternal(
RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location,
const viz::FrameSinkId& expected_frame_sink_id) {
TargetingRequest request) {
// Async event targeting and verifying use two different queues, so they don't
// block each other.
bool is_verifying = expected_frame_sink_id.is_valid();
bool is_verifying = request.GetExpectedFrameSinkId().is_valid();
DCHECK((!is_verifying && !request_in_flight_) ||
(is_verifying && !verify_request_in_flight_));
......@@ -233,15 +309,14 @@ void RenderWidgetTargeter::QueryClientInternal(
// understand why this happens. https://crbug.com/859492.
// We do not verify hit testing result under this circumstance.
if (!target_client) {
FoundTarget(root_view, target, event, latency, target_location, false,
viz::FrameSinkId());
FoundTarget(target, target_location, false, &request);
return;
}
if (is_verifying) {
verify_request_in_flight_ = true;
verify_request_in_flight_ = std::move(request);
} else {
request_in_flight_ = true;
request_in_flight_ = std::move(request);
async_depth_++;
}
TracingUmaTracker tracker("Event.AsyncTargeting.ResponseTime");
......@@ -250,67 +325,46 @@ void RenderWidgetTargeter::QueryClientInternal(
hit_test_timeout.reset(new OneShotTimeoutMonitor(
base::BindOnce(
&RenderWidgetTargeter::AsyncHitTestTimedOut,
weak_ptr_factory_.GetWeakPtr(), root_view->GetWeakPtr(),
target->GetWeakPtr(), target_location,
weak_ptr_factory_.GetWeakPtr(), target->GetWeakPtr(), target_location,
last_request_target ? last_request_target->GetWeakPtr() : nullptr,
last_target_location, ui::WebInputEventTraits::Clone(event), latency,
expected_frame_sink_id),
last_target_location, is_verifying),
async_hit_test_timeout_delay_));
TRACE_EVENT_WITH_FLOW2(
"viz,benchmark", "Event.Pipeline", TRACE_ID_GLOBAL(trace_id_),
TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT, "step",
"QueryClient", "event", blink::WebInputEvent::GetName(event.GetType()));
"QueryClient", "event_location", request.GetLocation().ToString());
target_client->FrameSinkIdAt(
target_location, trace_id_,
base::BindOnce(
&RenderWidgetTargeter::FoundFrameSinkId,
weak_ptr_factory_.GetWeakPtr(), root_view->GetWeakPtr(),
target->GetWeakPtr(), ui::WebInputEventTraits::Clone(event), latency,
weak_ptr_factory_.GetWeakPtr(), target->GetWeakPtr(),
is_verifying ? ++last_verify_request_id_ : ++last_request_id_,
target_location, std::move(tracker), expected_frame_sink_id));
target_location, std::move(tracker), is_verifying));
}
void RenderWidgetTargeter::QueryClient(
RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location) {
QueryClientInternal(root_view, target, event, latency, target_location,
last_request_target, last_target_location,
viz::FrameSinkId());
void RenderWidgetTargeter::QueryClient(TargetingRequest request) {
auto* target = request.GetRootView();
auto target_location = request.GetLocation();
QueryClientInternal(target, target_location, nullptr, gfx::PointF(),
std::move(request));
}
void RenderWidgetTargeter::QueryAndVerifyClient(
RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location,
const viz::FrameSinkId& expected_frame_sink_id) {
void RenderWidgetTargeter::QueryAndVerifyClient(TargetingRequest request) {
if (verify_request_in_flight_) {
TargetingRequest request;
request.root_view = root_view->GetWeakPtr();
request.event = ui::WebInputEventTraits::Clone(event);
request.latency = latency;
request.expected_frame_sink_id = expected_frame_sink_id;
verify_requests_.push(std::move(request));
return;
}
QueryClientInternal(root_view, target, event, latency, target_location,
last_request_target, last_target_location,
expected_frame_sink_id);
auto* target = request.GetRootView();
auto target_location = request.GetLocation();
QueryClientInternal(target, target_location, nullptr, gfx::PointF(),
std::move(request));
}
void RenderWidgetTargeter::FlushEventQueue(bool is_verifying) {
bool events_being_flushed = false;
bool& request_in_flight =
base::Optional<TargetingRequest>& request_in_flight =
is_verifying ? verify_request_in_flight_ : request_in_flight_;
auto* requests = is_verifying ? &verify_requests_ : &requests_;
while (!request_in_flight && !requests->empty()) {
......@@ -318,11 +372,10 @@ void RenderWidgetTargeter::FlushEventQueue(bool is_verifying) {
requests->pop();
// The root-view has gone away. Ignore this event, and try to process the
// next event.
if (!request.root_view) {
if (!request.GetRootView()) {
continue;
}
if (request.tracker)
request.tracker->Stop();
request.StopQueueingTimeTracker();
// Only notify the delegate once that the current event queue is being
// flushed. Once all the events are flushed, notify the delegate again.
if (!is_verifying && !events_being_flushed) {
......@@ -330,13 +383,9 @@ void RenderWidgetTargeter::FlushEventQueue(bool is_verifying) {
events_being_flushed = true;
}
if (is_verifying) {
QueryAndVerifyClient(request.root_view.get(), request.root_view.get(),
*request.event, request.latency,
ComputeEventLocation(*request.event), nullptr,
gfx::PointF(), request.expected_frame_sink_id);
QueryAndVerifyClient(std::move(request));
} else {
FindTargetAndDispatch(request.root_view.get(), *request.event,
request.latency);
ResolveTargetingRequest(std::move(request));
}
}
if (!is_verifying)
......@@ -344,25 +393,24 @@ void RenderWidgetTargeter::FlushEventQueue(bool is_verifying) {
}
void RenderWidgetTargeter::FoundFrameSinkId(
base::WeakPtr<RenderWidgetHostViewBase> root_view,
base::WeakPtr<RenderWidgetHostViewBase> target,
ui::WebScopedInputEvent event,
const ui::LatencyInfo& latency,
uint32_t request_id,
const gfx::PointF& target_location,
TracingUmaTracker tracker,
const viz::FrameSinkId& expected_frame_sink_id,
const bool is_verification_request,
const viz::FrameSinkId& frame_sink_id,
const gfx::PointF& transformed_location) {
if (expected_frame_sink_id.is_valid()) {
if (is_verification_request) {
tracker.Stop();
} else {
tracker.StopAndRecord();
}
uint32_t last_id = expected_frame_sink_id.is_valid() ? last_verify_request_id_
: last_request_id_;
bool in_flight = expected_frame_sink_id.is_valid() ? verify_request_in_flight_
: request_in_flight_;
uint32_t last_id =
is_verification_request ? last_verify_request_id_ : last_request_id_;
bool in_flight = is_verification_request
? verify_request_in_flight_.has_value()
: request_in_flight_.has_value();
if (request_id != last_id || !in_flight) {
// This is a response to a request that already timed out, so the event
// should have already been dispatched. Mark the renderer as responsive
......@@ -371,15 +419,20 @@ void RenderWidgetTargeter::FoundFrameSinkId(
return;
}
if (expected_frame_sink_id.is_valid()) {
verify_request_in_flight_ = false;
TargetingRequest request = is_verification_request
? std::move(verify_request_in_flight_.value())
: std::move(request_in_flight_.value());
if (request.GetExpectedFrameSinkId().is_valid()) {
verify_request_in_flight_.reset();
async_verify_hit_test_timeout_.reset(nullptr);
} else {
request_in_flight_ = false;
request_in_flight_.reset();
async_hit_test_timeout_.reset(nullptr);
if (is_viz_hit_testing_debug_enabled_ &&
event->GetType() == blink::WebInputEvent::Type::kMouseDown) {
if (is_viz_hit_testing_debug_enabled_ && request.IsWebInputEventRequest() &&
request.GetEvent().GetType() ==
blink::WebInputEvent::Type::kMouseDown) {
hit_test_async_queried_debug_queue_.push_back(target->GetFrameSinkId());
}
}
......@@ -400,46 +453,45 @@ void RenderWidgetTargeter::FoundFrameSinkId(
TRACE_EVENT_FLAG_FLOW_IN, "step", "FoundTarget");
}
FoundTarget(root_view.get(), view, *event, latency, transformed_location,
false, expected_frame_sink_id);
FoundTarget(view, transformed_location, false, &request);
} else {
QueryClientInternal(root_view.get(), view, *event, latency,
transformed_location, target.get(), target_location,
expected_frame_sink_id);
QueryClientInternal(view, transformed_location, target.get(),
target_location, std::move(request));
}
}
void RenderWidgetTargeter::FoundTarget(
RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const base::Optional<gfx::PointF>& target_location,
bool latched_target,
const viz::FrameSinkId& expected_frame_sink_id) {
TargetingRequest* request) {
DCHECK(request);
if (SiteIsolationPolicy::UseDedicatedProcessesForAllSites() &&
!latched_target && !expected_frame_sink_id.is_valid()) {
!latched_target && !request->GetExpectedFrameSinkId().is_valid()) {
UMA_HISTOGRAM_COUNTS_100("Event.AsyncTargeting.AsyncClientDepth",
async_depth_);
}
// RenderWidgetHostViewMac can be deleted asynchronously, in which case the
// View will be valid but there will no longer be a RenderWidgetHostImpl.
if (!root_view || !root_view->GetRenderWidgetHost())
if (!request->GetRootView() || !request->GetRootView()->GetRenderWidgetHost())
return;
if (is_viz_hit_testing_debug_enabled_ &&
!hit_test_async_queried_debug_queue_.empty()) {
GetHostFrameSinkManager()->SetHitTestAsyncQueriedDebugRegions(
root_view->GetRootFrameSinkId(), hit_test_async_queried_debug_queue_);
request->GetRootView()->GetRootFrameSinkId(),
hit_test_async_queried_debug_queue_);
hit_test_async_queried_debug_queue_.clear();
}
if (features::IsVizHitTestingSurfaceLayerEnabled() &&
expected_frame_sink_id.is_valid()) {
request->GetExpectedFrameSinkId().is_valid()) {
static const char* kResultsMatchHistogramName =
"Event.VizHitTestSurfaceLayer.ResultsMatch";
bool results_match = target->GetFrameSinkId() == expected_frame_sink_id;
bool results_match =
target->GetFrameSinkId() == request->GetExpectedFrameSinkId();
HitTestResultsMatch match_result =
HitTestResultsMatch::kHitTestResultChanged;
if (results_match) {
......@@ -448,10 +500,17 @@ void RenderWidgetTargeter::FoundTarget(
// If the results do not match, it is possible that the hit test data
// changed during verification. We do synchronous hit test again to make
// sure the result is reliable.
RenderWidgetTargetResult result =
delegate_->FindTargetSynchronously(root_view, event);
RenderWidgetTargetResult result;
if (request->IsWebInputEventRequest()) {
result = delegate_->FindTargetSynchronously(request->GetRootView(),
request->GetEvent());
} else {
result = delegate_->FindTargetSynchronouslyAtLocation(
request->GetRootView(), request->GetLocation());
}
if (!result.should_query_view && result.view &&
expected_frame_sink_id == result.view->GetFrameSinkId()) {
request->GetExpectedFrameSinkId() == result.view->GetFrameSinkId()) {
// If the result did not change, it is likely that viz hit test finds
// the wrong target.
match_result = HitTestResultsMatch::kDoNotMatch;
......@@ -465,48 +524,53 @@ void RenderWidgetTargeter::FoundTarget(
FlushEventQueue(true);
return;
}
delegate_->DispatchEventToTarget(root_view, target, event, latency,
if (request->IsWebInputEventRequest()) {
delegate_->DispatchEventToTarget(request->GetRootView(), target,
request->GetEvent(), request->GetLatency(),
target_location);
} else {
request->RunCallback(target, target_location);
}
FlushEventQueue(false);
}
void RenderWidgetTargeter::AsyncHitTestTimedOut(
base::WeakPtr<RenderWidgetHostViewBase> current_request_root_view,
base::WeakPtr<RenderWidgetHostViewBase> current_request_target,
const gfx::PointF& current_target_location,
base::WeakPtr<RenderWidgetHostViewBase> last_request_target,
const gfx::PointF& last_target_location,
ui::WebScopedInputEvent event,
const ui::LatencyInfo& latency,
const viz::FrameSinkId& expected_frame_sink_id) {
const bool is_verification_request) {
DCHECK(request_in_flight_ || verify_request_in_flight_);
TargetingRequest request = is_verification_request
? std::move(verify_request_in_flight_.value())
: std::move(request_in_flight_.value());
// If we time out during a verification, we early out to avoid dispatching
// event to root frame.
if (expected_frame_sink_id.is_valid()) {
verify_request_in_flight_ = false;
if (request.GetExpectedFrameSinkId().is_valid()) {
verify_request_in_flight_.reset();
return;
} else {
request_in_flight_ = false;
request_in_flight_.reset();
}
if (!current_request_root_view)
if (!request.GetRootView())
return;
// Mark view as unresponsive so further events will not be sent to it.
if (current_request_target)
unresponsive_views_.insert(current_request_target.get());
if (current_request_root_view.get() == current_request_target.get()) {
if (request.GetRootView() == current_request_target.get()) {
// When a request to the top-level frame times out then the event gets
// sent there anyway. It will trigger the hung renderer dialog if the
// renderer fails to process it.
FoundTarget(current_request_root_view.get(),
current_request_root_view.get(), *event, latency,
current_target_location, false, viz::FrameSinkId());
FoundTarget(current_request_target.get(), current_target_location, false,
&request);
} else {
FoundTarget(current_request_root_view.get(), last_request_target.get(),
*event, latency, last_target_location, false,
viz::FrameSinkId());
FoundTarget(last_request_target.get(), last_target_location, false,
&request);
}
}
......
......@@ -6,12 +6,12 @@
#define CONTENT_BROWSER_RENDERER_HOST_RENDER_WIDGET_TARGETER_H_
#include <queue>
#include <unordered_set>
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/time/time.h"
#include "components/viz/common/surfaces/frame_sink_id.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/common/content_constants_internal.h"
#include "content/common/content_export.h"
#include "ui/events/blink/web_input_event_traits.h"
......@@ -60,10 +60,18 @@ class TracingUmaTracker;
class RenderWidgetTargeter {
public:
using RenderWidgetHostAtPointCallback =
base::OnceCallback<void(base::WeakPtr<RenderWidgetHostViewBase>,
base::Optional<gfx::PointF>)>;
class Delegate {
public:
virtual ~Delegate() {}
virtual RenderWidgetTargetResult FindTargetSynchronouslyAtLocation(
RenderWidgetHostViewBase* root_view,
const gfx::PointF& location) = 0;
virtual RenderWidgetTargetResult FindTargetSynchronously(
RenderWidgetHostViewBase* root_view,
const blink::WebInputEvent& event) = 0;
......@@ -97,6 +105,13 @@ class RenderWidgetTargeter {
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency);
// Finds the appropriate target inside |root_view| for |point|, and passes the
// target along with the transformed coordinates of the point with respect to
// the target's coordinates.
void FindTargetAndCallback(RenderWidgetHostViewBase* root_view,
const gfx::PointF& point,
RenderWidgetHostAtPointCallback callback);
void ViewWillBeDestroyed(RenderWidgetHostViewBase* view);
bool HasEventsPendingDispatch() const;
......@@ -107,9 +122,65 @@ class RenderWidgetTargeter {
}
size_t num_requests_in_queue_for_testing() { return requests_.size(); }
bool is_request_in_flight_for_testing() { return request_in_flight_; }
bool is_request_in_flight_for_testing() {
return request_in_flight_.has_value();
}
private:
class TargetingRequest {
public:
TargetingRequest(base::WeakPtr<RenderWidgetHostViewBase>,
const blink::WebInputEvent&,
const ui::LatencyInfo&);
TargetingRequest(base::WeakPtr<RenderWidgetHostViewBase>,
const gfx::PointF&,
RenderWidgetHostAtPointCallback);
TargetingRequest(TargetingRequest&& request);
TargetingRequest& operator=(TargetingRequest&& other);
~TargetingRequest();
void RunCallback(RenderWidgetHostViewBase* target,
base::Optional<gfx::PointF> point);
bool MergeEventIfPossible(const blink::WebInputEvent& event);
bool IsWebInputEventRequest() const;
const blink::WebInputEvent& GetEvent() const;
RenderWidgetHostViewBase* GetRootView() const;
gfx::PointF GetLocation() const;
const ui::LatencyInfo& GetLatency() const;
// Queued TragetingRequest
void StartQueueingTimeTracker();
void StopQueueingTimeTracker();
// Verification TargetingRequest
viz::FrameSinkId GetExpectedFrameSinkId() const;
void SetExpectedFrameSinkId(const viz::FrameSinkId& id);
private:
base::WeakPtr<RenderWidgetHostViewBase> root_view;
RenderWidgetHostAtPointCallback callback;
// |location| is in the coordinate space of |root_view| which is
// either set directly when event is null or calculated from the event.
gfx::PointF location;
// |event| if set is in the coordinate space of |root_view|.
ui::WebScopedInputEvent event;
ui::LatencyInfo latency;
// |expected_frame_sink_id| is only valid if the request is for
// verification.
viz::FrameSinkId expected_frame_sink_id;
// To track how long the request has been queued.
std::unique_ptr<TracingUmaTracker> tracker;
DISALLOW_COPY_AND_ASSIGN(TargetingRequest);
};
void ResolveTargetingRequest(TargetingRequest);
// Attempts to target and dispatch all events in the queue. It stops if it has
// to query a client, or if the queue becomes empty.
void FlushEventQueue(bool is_verifying);
......@@ -127,33 +198,17 @@ class RenderWidgetTargeter {
// correctly.
// TODO(sunxd): Remove |expected_frame_sink_id| after verifying synchronous
// hit testing correctness. See https://crbug.com/871996.
void QueryClientInternal(RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
void QueryClientInternal(RenderWidgetHostViewBase* target,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location,
const viz::FrameSinkId& expected_frame_sink_id);
TargetingRequest request);
void QueryClient(RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location);
void QueryClient(TargetingRequest request);
void QueryAndVerifyClient(RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
const gfx::PointF& target_location,
RenderWidgetHostViewBase* last_request_target,
const gfx::PointF& last_target_location,
const viz::FrameSinkId& expected_frame_sink_id);
void QueryAndVerifyClient(TargetingRequest request);
// |event| is in the coordinate space of |root_view|. |target_location|, if
// |target_location|, if
// set, is the location in |target|'s coordinate space.
// |target| is the current target that will be queried using its
// InputTargetClient interface.
......@@ -162,66 +217,44 @@ class RenderWidgetTargeter {
// that new target's coordinate space.
// |expected_frame_sink_id| is the expected hit test result based on
// synchronous event targeting with cc generated data.
void FoundFrameSinkId(base::WeakPtr<RenderWidgetHostViewBase> root_view,
base::WeakPtr<RenderWidgetHostViewBase> target,
ui::WebScopedInputEvent event,
const ui::LatencyInfo& latency,
void FoundFrameSinkId(base::WeakPtr<RenderWidgetHostViewBase> target,
uint32_t request_id,
const gfx::PointF& target_location,
TracingUmaTracker tracker,
const viz::FrameSinkId& expected_frame_sink_id,
const bool is_verification_request,
const viz::FrameSinkId& frame_sink_id,
const gfx::PointF& transformed_location);
// |event| is in the coordinate space of |root_view|. |target_location|, if
// |target_location|, if
// set, is the location in |target|'s coordinate space. If |latched_target| is
// false, we explicitly did hit-testing for this event, instead of using a
// known target.
void FoundTarget(RenderWidgetHostViewBase* root_view,
RenderWidgetHostViewBase* target,
const blink::WebInputEvent& event,
const ui::LatencyInfo& latency,
void FoundTarget(RenderWidgetHostViewBase* target,
const base::Optional<gfx::PointF>& target_location,
bool latched_target,
const viz::FrameSinkId& expected_frame_sink_id);
TargetingRequest* request);
// Callback when the hit testing timer fires, to resume event processing
// without further waiting for a response to the last targeting request.
void AsyncHitTestTimedOut(
base::WeakPtr<RenderWidgetHostViewBase> current_request_root_view,
base::WeakPtr<RenderWidgetHostViewBase> current_request_target,
const gfx::PointF& current_target_location,
base::WeakPtr<RenderWidgetHostViewBase> last_request_target,
const gfx::PointF& last_target_location,
ui::WebScopedInputEvent event,
const ui::LatencyInfo& latency,
const viz::FrameSinkId& expected_frame_sink_id);
const bool is_verification_request);
base::TimeDelta async_hit_test_timeout_delay() {
return async_hit_test_timeout_delay_;
}
struct TargetingRequest {
TargetingRequest();
TargetingRequest(TargetingRequest&& request);
TargetingRequest& operator=(TargetingRequest&& other);
~TargetingRequest();
base::WeakPtr<RenderWidgetHostViewBase> root_view;
ui::WebScopedInputEvent event;
ui::LatencyInfo latency;
viz::FrameSinkId expected_frame_sink_id;
std::unique_ptr<TracingUmaTracker> tracker;
};
bool request_in_flight_ = false;
base::Optional<TargetingRequest> request_in_flight_;
uint32_t last_request_id_ = 0;
std::queue<TargetingRequest> requests_;
// With viz-hit-testing-surface-layer being enabled, we do async hit testing
// for already dispatched events for verification. These verification requests
// should not block normal hit testing requests.
bool verify_request_in_flight_ = false;
base::Optional<TargetingRequest> verify_request_in_flight_;
uint32_t last_verify_request_id_ = 0;
std::queue<TargetingRequest> verify_requests_;
......
......@@ -1232,25 +1232,22 @@ void WebContentsViewAura::OnMouseEvent(ui::MouseEvent* event) {
////////////////////////////////////////////////////////////////////////////////
// WebContentsViewAura, aura::client::DragDropDelegate implementation:
void WebContentsViewAura::OnDragEntered(const ui::DropTargetEvent& event) {
#if defined(OS_WIN)
async_drop_navigation_observer_.reset();
#endif
gfx::PointF transformed_pt;
void WebContentsViewAura::DragEnteredCallback(
const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt) {
if (!target)
return;
RenderWidgetHostImpl* target_rwh =
web_contents_->GetInputEventRouter()->GetRenderWidgetHostAtPoint(
web_contents_->GetRenderViewHost()->GetWidget()->GetView(),
event.location_f(), &transformed_pt);
RenderWidgetHostImpl::From(target->GetRenderWidgetHost());
if (!IsValidDragTarget(target_rwh))
return;
current_rwh_for_drag_ = target_rwh->GetWeakPtr();
current_rvh_for_drag_ =
GetRenderViewHostID(web_contents_->GetRenderViewHost());
current_drop_data_.reset(new DropData());
PrepareDropData(current_drop_data_.get(), event.data());
current_drop_data_.reset(drop_data.release());
current_rwh_for_drag_->FilterDropData(current_drop_data_.get());
blink::WebDragOperationsMask op = ConvertToWeb(event.source_operations());
......@@ -1266,26 +1263,50 @@ void WebContentsViewAura::OnDragEntered(const ui::DropTargetEvent& event) {
if (drag_dest_delegate_)
drag_dest_delegate_->DragInitialize(web_contents_);
DCHECK(transformed_pt.has_value());
gfx::PointF screen_pt(display::Screen::GetScreen()->GetCursorScreenPoint());
current_rwh_for_drag_->DragTargetDragEnter(
*current_drop_data_, transformed_pt, screen_pt, op,
*current_drop_data_, transformed_pt.value(), screen_pt, op,
ui::EventFlagsToWebEventModifiers(event.flags()));
if (drag_dest_delegate_) {
drag_dest_delegate_->OnReceiveDragData(event.data());
drag_dest_delegate_->OnDragEnter();
}
}
int WebContentsViewAura::OnDragUpdated(const ui::DropTargetEvent& event) {
gfx::PointF transformed_pt;
RenderWidgetHostImpl* target_rwh =
web_contents_->GetInputEventRouter()->GetRenderWidgetHostAtPoint(
void WebContentsViewAura::OnDragEntered(const ui::DropTargetEvent& event) {
#if defined(OS_WIN)
async_drop_navigation_observer_.reset();
#endif
std::unique_ptr<DropData> drop_data = std::make_unique<DropData>();
// Calling this here as event.data might become invalid inside the callback.
PrepareDropData(drop_data.get(), event.data());
if (drag_dest_delegate_) {
drag_dest_delegate_->OnReceiveDragData(event.data());
}
web_contents_->GetInputEventRouter()
->GetRenderWidgetHostAtPointAsynchronously(
web_contents_->GetRenderViewHost()->GetWidget()->GetView(),
event.location_f(), &transformed_pt);
event.location_f(),
base::BindOnce(&WebContentsViewAura::DragEnteredCallback,
weak_ptr_factory_.GetWeakPtr(), event,
std::move(drop_data)));
}
void WebContentsViewAura::DragUpdatedCallback(
const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt) {
if (!target)
return;
RenderWidgetHostImpl* target_rwh =
RenderWidgetHostImpl::From(target->GetRenderWidgetHost());
if (!IsValidDragTarget(target_rwh))
return ui::DragDropTypes::DRAG_NONE;
return;
aura::Window* root_window = GetNativeView()->GetRootWindow();
aura::client::ScreenPositionClient* screen_position_client =
......@@ -1307,20 +1328,35 @@ int WebContentsViewAura::OnDragUpdated(const ui::DropTargetEvent& event) {
current_rwh_for_drag_->DragTargetDragLeave(transformed_leave_point,
screen_pt);
}
OnDragEntered(event);
DragEnteredCallback(event, std::move(drop_data), target, transformed_pt);
}
if (!current_drop_data_)
return ui::DragDropTypes::DRAG_NONE;
if (!current_drop_data_) {
return;
}
DCHECK(transformed_pt.has_value());
blink::WebDragOperationsMask op = ConvertToWeb(event.source_operations());
target_rwh->DragTargetDragOver(
transformed_pt, screen_pt, op,
transformed_pt.value(), screen_pt, op,
ui::EventFlagsToWebEventModifiers(event.flags()));
if (drag_dest_delegate_)
drag_dest_delegate_->OnDragOver();
}
int WebContentsViewAura::OnDragUpdated(const ui::DropTargetEvent& event) {
std::unique_ptr<DropData> drop_data = std::make_unique<DropData>();
// Calling this here as event.data might become invalid inside the callback.
PrepareDropData(drop_data.get(), event.data());
web_contents_->GetInputEventRouter()
->GetRenderWidgetHostAtPointAsynchronously(
web_contents_->GetRenderViewHost()->GetWidget()->GetView(),
event.location_f(),
base::BindOnce(&WebContentsViewAura::DragUpdatedCallback,
weak_ptr_factory_.GetWeakPtr(), event,
std::move(drop_data)));
return ConvertFromWeb(current_drag_op_);
}
......@@ -1342,25 +1378,31 @@ void WebContentsViewAura::OnDragExited() {
current_drop_data_.reset();
}
int WebContentsViewAura::OnPerformDrop(const ui::DropTargetEvent& event) {
gfx::PointF transformed_pt;
void WebContentsViewAura::PerformDropCallback(
const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt) {
if (!target)
return;
RenderWidgetHostImpl* target_rwh =
web_contents_->GetInputEventRouter()->GetRenderWidgetHostAtPoint(
web_contents_->GetRenderViewHost()->GetWidget()->GetView(),
event.location_f(), &transformed_pt);
RenderWidgetHostImpl::From(target->GetRenderWidgetHost());
if (!IsValidDragTarget(target_rwh))
return ui::DragDropTypes::DRAG_NONE;
return;
DCHECK(transformed_pt.has_value());
gfx::PointF screen_pt(display::Screen::GetScreen()->GetCursorScreenPoint());
if (target_rwh != current_rwh_for_drag_.get()) {
if (current_rwh_for_drag_)
current_rwh_for_drag_->DragTargetDragLeave(transformed_pt, screen_pt);
OnDragEntered(event);
current_rwh_for_drag_->DragTargetDragLeave(transformed_pt.value(),
screen_pt);
DragEnteredCallback(event, std::move(drop_data), target, transformed_pt);
}
if (!current_drop_data_)
return ui::DragDropTypes::DRAG_NONE;
if (!current_drop_data_) {
return;
}
const int key_modifiers = ui::EventFlagsToWebEventModifiers(event.flags());
#if defined(OS_WIN)
......@@ -1386,16 +1428,29 @@ int WebContentsViewAura::OnPerformDrop(const ui::DropTargetEvent& event) {
async_drop_navigation_observer_ =
std::make_unique<AsyncDropNavigationObserver>(
web_contents_, std::move(current_drop_data_), target_rwh,
transformed_pt, screen_pt, key_modifiers);
return ConvertFromWeb(current_drag_op_);
transformed_pt.value(), screen_pt, key_modifiers);
return;
}
}
#endif
CompleteDrop(target_rwh, *current_drop_data_, transformed_pt, screen_pt,
key_modifiers);
CompleteDrop(target_rwh, *current_drop_data_, transformed_pt.value(),
screen_pt, key_modifiers);
current_drop_data_.reset();
}
int WebContentsViewAura::OnPerformDrop(const ui::DropTargetEvent& event) {
std::unique_ptr<DropData> drop_data = std::make_unique<DropData>();
// Calling this here as event.data might become invalid inside the callback.
PrepareDropData(drop_data.get(), event.data());
web_contents_->GetInputEventRouter()
->GetRenderWidgetHostAtPointAsynchronously(
web_contents_->GetRenderViewHost()->GetWidget()->GetView(),
event.location_f(),
base::BindOnce(&WebContentsViewAura::PerformDropCallback,
weak_ptr_factory_.GetWeakPtr(), event,
std::move(drop_data)));
return ConvertFromWeb(current_drag_op_);
}
......
......@@ -73,6 +73,7 @@ class CONTENT_EXPORT WebContentsViewAura
FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest,
DragDropVirtualFilesOriginateFromRenderer);
FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropUrlData);
FRIEND_TEST_ALL_PREFIXES(WebContentsViewAuraTest, DragDropOnOopif);
class WindowObserver;
......@@ -193,6 +194,19 @@ class CONTENT_EXPORT WebContentsViewAura
void OnDragExited() override;
int OnPerformDrop(const ui::DropTargetEvent& event) override;
void DragEnteredCallback(const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt);
void DragUpdatedCallback(const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt);
void PerformDropCallback(const ui::DropTargetEvent& event,
std::unique_ptr<DropData> drop_data,
base::WeakPtr<RenderWidgetHostViewBase> target,
base::Optional<gfx::PointF> transformed_pt);
// Completes a drop operation by communicating the drop data to the renderer
// process.
void CompleteDrop(RenderWidgetHostImpl* target_rwh,
......@@ -270,11 +284,9 @@ class CONTENT_EXPORT WebContentsViewAura
bool init_rwhv_with_null_parent_for_testing_;
#if defined(OS_WIN)
// Used to ensure that the virtual files retrieval callback bound to this
// Used to ensure the drag and drop callbacks bound to this
// object is canceled when this object is destroyed.
base::WeakPtrFactory<WebContentsViewAura> weak_ptr_factory_{this};
#endif
DISALLOW_COPY_AND_ASSIGN(WebContentsViewAura);
};
......
......@@ -41,9 +41,12 @@
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_sink.h"
......@@ -87,6 +90,12 @@ class WebContentsViewAuraTest : public ContentBrowserTest {
shell()->web_contents());
}
void SetUpOnMainThread() override {
// Setup the server to allow serving separate sites, so we can perform
// cross-process navigation.
host_resolver()->AddRule("*", "127.0.0.1");
}
void SetUpCommandLine(base::CommandLine* cmd) override {
cmd->AppendSwitchASCII(switches::kTouchEventFeatureDetection,
switches::kTouchEventFeatureDetectionEnabled);
......@@ -487,6 +496,59 @@ IN_PROC_BROWSER_TEST_F(WebContentsViewAuraTest,
window->AddChild(shell()->web_contents()->GetContentNativeView());
}
IN_PROC_BROWSER_TEST_F(WebContentsViewAuraTest, DragDropOnOopif) {
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"a.com", "/overlapping_cross_site_iframe.html");
EXPECT_TRUE(NavigateToURL(shell(), url));
WebContentsImpl* contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
WebContentsViewAura* view =
static_cast<WebContentsViewAura*>(contents->GetView());
ui::OSExchangeData data;
{
gfx::PointF point = {10, 10};
ui::DropTargetEvent event(data, point, point, ui::DragDropTypes::DRAG_COPY);
// Simulate drag enter on a surface that is handled by synchronous
// hittesting.
EXPECT_EQ(nullptr, view->current_drop_data_);
view->OnDragEntered(event);
ASSERT_NE(nullptr, view->current_drop_data_);
view->OnPerformDrop(event);
EXPECT_EQ(nullptr, view->current_drop_data_);
}
{
int left =
EvalJs(contents,
"document.getElementById('target').getBoundingClientRect().left")
.ExtractInt();
int top =
EvalJs(contents,
"document.getElementById('target').getBoundingClientRect().top")
.ExtractInt();
gfx::PointF point = {left + 5, top + 5};
ui::DropTargetEvent event(data, point, point, ui::DragDropTypes::DRAG_COPY);
// Simulate drag enter on a surface that is handled by asynchronous
// hittesting.
EXPECT_EQ(nullptr, view->current_drop_data_);
view->OnDragEntered(event);
// Immediately after the function call the drop data on the target view
// should still be null as we need to wait for the blink hittest callback.
EXPECT_EQ(nullptr, view->current_drop_data_);
while (!view->current_drop_data_)
GiveItSomeTime();
view->OnPerformDrop(event);
// Immediately after the function call the drop operation is not done as we
// need to wait for the blink hittest callback.
EXPECT_NE(nullptr, view->current_drop_data_);
while (view->current_drop_data_)
GiveItSomeTime();
}
}
IN_PROC_BROWSER_TEST_F(WebContentsViewAuraTest, ContentWindowClose) {
ASSERT_NO_FATAL_FAILURE(StartTestWithPage("/overscroll_navigation.html"));
......
<html>
<title>Overlapping cross site iframe</title>
This is the main body content.
<iframe id="iframe" style="width:300px; height:300px; position:absolute; left:100px; top:100px"></iframe>
<div id="target" style="background-color: yellow; position: absolute; top: 120px; left: 120px; width: 100px; height: 100px"></div>
<script>
var url = window.location.protocol + '//b.com';
if (window.location.port)
url += ':' + window.location.port;
url += "/cross_site_iframe_factory.html?b()";
document.getElementById('iframe').src = url;
</script>
</html>
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