Commit bc61eb6a authored by Kevin McNee's avatar Kevin McNee Committed by Commit Bot

PDF viewer: Handle ctrl-wheel zooming with the viewer's pinch zoom mechanism.

When pinch zooming with a Mac trackpad, we generate synthetic ctrl-wheels
so that JS can handle the gesture. We now use these wheel events in the
PDF viewer's gesture detector to create pinch events for the viewer's
pinch zoom mechanism, as we currently do for pinches from touch events.

We also use this for non-synthetic ctrl-wheels. This allows us to have
zooming with the mouse wheel anchor the zoom around the mouse position
instead of the scroll position.

Bug: 715662, 715670
Cq-Include-Trybots: master.tryserver.chromium.linux:closure_compilation
Change-Id: Ia672a266cdd69c3e87d1451548c7470176ed8fd6
Reviewed-on: https://chromium-review.googlesource.com/730648Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatardsinclair <dsinclair@chromium.org>
Reviewed-by: default avatarDavid Bokan <bokan@chromium.org>
Commit-Queue: Kevin McNee <mcnee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#516034}
parent a0ff710d
...@@ -1314,7 +1314,43 @@ IN_PROC_BROWSER_TEST_F(PDFExtensionTest, PostMessageForZeroSizedEmbed) { ...@@ -1314,7 +1314,43 @@ IN_PROC_BROWSER_TEST_F(PDFExtensionTest, PostMessageForZeroSizedEmbed) {
EXPECT_EQ("\"POST_MESSAGE_OK\"", message); EXPECT_EQ("\"POST_MESSAGE_OK\"", message);
} }
// In response to the events sent in |send_events|, ensures the PDF viewer zooms
// in and that the viewer's custom pinch zooming mechanism is used to do so.
void EnsureCustomPinchZoomInvoked(WebContents* guest_contents,
WebContents* contents,
base::OnceClosure send_events) {
ASSERT_TRUE(content::ExecuteScript(
guest_contents,
"var gestureDetector = new GestureDetector(viewer.plugin_); "
"var updatePromise = new Promise(function(resolve) { "
" gestureDetector.addEventListener('pinchupdate', resolve); "
"});"));
zoom::ZoomChangedWatcher zoom_watcher(
contents,
base::BindRepeating(
[](const zoom::ZoomController::ZoomChangedEventData& event) {
return event.new_zoom_level > event.old_zoom_level &&
event.zoom_mode == zoom::ZoomController::ZOOM_MODE_MANUAL &&
!event.can_show_bubble;
}));
std::move(send_events).Run();
bool got_update;
ASSERT_TRUE(content::ExecuteScriptAndExtractBool(
guest_contents,
"updatePromise.then(function(update) { "
" window.domAutomationController.send(!!update); "
"});",
&got_update));
EXPECT_TRUE(got_update);
zoom_watcher.Wait();
}
#if defined(OS_MACOSX) #if defined(OS_MACOSX)
// Test that "smart zoom" (double-tap with two fingers on Mac trackpad) // Test that "smart zoom" (double-tap with two fingers on Mac trackpad)
// is disabled for the PDF viewer. This prevents the viewer's controls from // is disabled for the PDF viewer. This prevents the viewer's controls from
// being scaled off screen (see crbug.com/676668). // being scaled off screen (see crbug.com/676668).
...@@ -1332,6 +1368,51 @@ IN_PROC_BROWSER_TEST_F(PDFExtensionTest, SmartZoomDisabled) { ...@@ -1332,6 +1368,51 @@ IN_PROC_BROWSER_TEST_F(PDFExtensionTest, SmartZoomDisabled) {
EXPECT_TRUE(browser()->PreHandleGestureEvent(GetActiveWebContents(), EXPECT_TRUE(browser()->PreHandleGestureEvent(GetActiveWebContents(),
smart_zoom_event)); smart_zoom_event));
} }
// Ensure that Mac trackpad pinch events are handled by the PDF viewer.
IN_PROC_BROWSER_TEST_F(PDFExtensionTest, TrackpadPinchInvokesCustomZoom) {
GURL test_pdf_url(embedded_test_server()->GetURL("/pdf/test.pdf"));
WebContents* guest_contents = LoadPdfGetGuestContents(test_pdf_url);
ASSERT_TRUE(guest_contents);
base::OnceClosure send_pinch = base::BindOnce(
[](WebContents* guest_contents) {
const gfx::Rect guest_rect = guest_contents->GetContainerBounds();
const gfx::Point mouse_position(guest_rect.width() / 2,
guest_rect.height() / 2);
content::SimulateGesturePinchSequence(guest_contents, mouse_position,
1.23,
blink::kWebGestureDeviceTouchpad);
},
guest_contents);
EnsureCustomPinchZoomInvoked(guest_contents, GetActiveWebContents(),
std::move(send_pinch));
}
#else // !defined(OS_MACOSX)
// Ensure that ctrl-wheel events are handled by the PDF viewer.
IN_PROC_BROWSER_TEST_F(PDFExtensionTest, CtrlWheelInvokesCustomZoom) {
GURL test_pdf_url(embedded_test_server()->GetURL("/pdf/test.pdf"));
WebContents* guest_contents = LoadPdfGetGuestContents(test_pdf_url);
ASSERT_TRUE(guest_contents);
base::OnceClosure send_ctrl_wheel = base::BindOnce(
[](WebContents* guest_contents) {
const gfx::Rect guest_rect = guest_contents->GetContainerBounds();
const gfx::Point mouse_position(guest_rect.width() / 2,
guest_rect.height() / 2);
content::SimulateMouseWheelCtrlZoomEvent(
guest_contents, mouse_position, true,
blink::WebMouseWheelEvent::kPhaseBegan);
},
guest_contents);
EnsureCustomPinchZoomInvoked(guest_contents, GetActiveWebContents(),
std::move(send_ctrl_wheel));
}
#endif // defined(OS_MACOSX) #endif // defined(OS_MACOSX)
IN_PROC_BROWSER_TEST_F(PDFExtensionTest, ContextMenuCoordinates) { IN_PROC_BROWSER_TEST_F(PDFExtensionTest, ContextMenuCoordinates) {
......
...@@ -29,12 +29,30 @@ class GestureDetector { ...@@ -29,12 +29,30 @@ class GestureDetector {
this.element_.addEventListener( this.element_.addEventListener(
'touchcancel', boundOnTouch, {passive: true}); 'touchcancel', boundOnTouch, {passive: true});
this.element_.addEventListener(
'wheel',
/** @type {function(!Event)} */ (this.onWheel_.bind(this)),
{passive: false});
this.pinchStartEvent_ = null; this.pinchStartEvent_ = null;
this.lastTouchTouchesCount_ = 0; this.lastTouchTouchesCount_ = 0;
/** @private {?TouchEvent} */ /** @private {?TouchEvent} */
this.lastEvent_ = null; this.lastEvent_ = null;
/**
* The scale relative to the start of the pinch when handling ctrl-wheels.
* null when there is no ongoing pinch.
* @private {?number}
*/
this.accumulatedWheelScale_ = null;
/**
* A timeout ID from setTimeout used for sending the pinchend event when
* handling ctrl-wheels.
* @private {?number}
*/
this.wheelEndTimeout_ = null;
/** @private {!Map<string, !Array<!Function>>} */ /** @private {!Map<string, !Array<!Function>>} */
this.listeners_ = this.listeners_ =
new Map([['pinchstart', []], ['pinchupdate', []], ['pinchend', []]]); new Map([['pinchstart', []], ['pinchupdate', []], ['pinchend', []]]);
...@@ -133,6 +151,63 @@ class GestureDetector { ...@@ -133,6 +151,63 @@ class GestureDetector {
this.lastEvent_ = event; this.lastEvent_ = event;
} }
/**
* The callback for wheel events on the element.
* @private
* @param {!WheelEvent} event Wheel event on the element.
*/
onWheel_(event) {
// We handle ctrl-wheels to invoke our own pinch zoom. On Mac, synthetic
// ctrl-wheels are created from trackpad pinches. We handle these ourselves
// to prevent the browser's native pinch zoom. We also use our pinch
// zooming mechanism for handling non-synthetic ctrl-wheels. This allows us
// to anchor the zoom around the mouse position instead of the scroll
// position.
if (!event.ctrlKey)
return;
event.preventDefault();
let wheelScale = Math.exp(-event.deltaY / 100);
// Clamp scale changes from the wheel event as they can be
// quite dramatic for non-synthetic ctrl-wheels.
let scale = Math.min(1.25, Math.max(0.75, wheelScale));
let position = {x: event.clientX, y: event.clientY};
if (this.accumulatedWheelScale_ == null) {
this.accumulatedWheelScale_ = 1.0;
this.notify_({type: 'pinchstart', center: position});
}
this.accumulatedWheelScale_ *= scale;
this.notify_({
type: 'pinchupdate',
scaleRatio: scale,
direction: scale > 1.0 ? 'in' : 'out',
startScaleRatio: this.accumulatedWheelScale_,
center: position
});
// We don't get any phase information for the ctrl-wheels, so we don't know
// when the gesture ends. We'll just use a timeout to send the pinch end
// event a short time after the last ctrl-wheel we see.
if (this.wheelEndTimeout_ != null) {
window.clearTimeout(this.wheelEndTimeout_);
this.wheelEndTimeout_ = null;
}
let gestureEndDelayMs = 100;
let endEvent = {
type: 'pinchend',
startScaleRatio: this.accumulatedWheelScale_,
center: position
};
this.wheelEndTimeout_ = window.setTimeout(function(endEvent) {
this.notify_(endEvent);
this.wheelEndTimeout_ = null;
this.accumulatedWheelScale_ = null;
}.bind(this), gestureEndDelayMs, endEvent);
}
/** /**
* Computes the change in scale between this touch event * Computes the change in scale between this touch event
* and a previous one. * and a previous one.
......
...@@ -11,7 +11,8 @@ chrome.test.runTests(function() { ...@@ -11,7 +11,8 @@ chrome.test.runTests(function() {
['touchstart', []], ['touchstart', []],
['touchmove', []], ['touchmove', []],
['touchend', []], ['touchend', []],
['touchcancel', []] ['touchcancel', []],
['wheel', []]
]); ]);
} }
...@@ -42,6 +43,21 @@ chrome.test.runTests(function() { ...@@ -42,6 +43,21 @@ chrome.test.runTests(function() {
} }
} }
class MockWheelEvent {
constructor(deltaY, position, ctrlKey) {
this.type = 'wheel';
this.deltaY = deltaY;
this.clientX = position.clientX;
this.clientY = position.clientY;
this.ctrlKey = ctrlKey;
this.defaultPrevented = false;
}
preventDefault() {
this.defaultPrevented = true;
}
}
class PinchListener { class PinchListener {
constructor(gestureDetector) { constructor(gestureDetector) {
this.lastEvent = null; this.lastEvent = null;
...@@ -154,6 +170,46 @@ chrome.test.runTests(function() { ...@@ -154,6 +170,46 @@ chrome.test.runTests(function() {
chrome.test.succeed(); chrome.test.succeed();
}, },
function testZoomWithWheel() {
let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement);
let pinchListener = new PinchListener(gestureDetector);
// Since the wheel events that the GestureDetector receives are
// individual updates without begin/end events, we need to make sure the
// GestureDetector generates appropriate pinch begin/end events itself.
class PinchSequenceListener {
constructor(gestureDetector) {
this.seenBegin = false;
gestureDetector.addEventListener('pinchstart', function() {
this.seenBegin = true;
}.bind(this));
this.endPromise = new Promise(function(resolve) {
gestureDetector.addEventListener('pinchend', resolve);
});
}
}
let pinchSequenceListener = new PinchSequenceListener(gestureDetector);
let scale = 1.23;
let deltaY = -(100.0 * Math.log(scale));
let position = {clientX: 12, clientY: 34};
stubElement.sendEvent(new MockWheelEvent(deltaY, position, true));
chrome.test.assertTrue(pinchSequenceListener.seenBegin);
let lastEvent = pinchListener.lastEvent;
chrome.test.assertEq('pinchupdate', lastEvent.type);
chrome.test.assertTrue(Math.abs(lastEvent.scaleRatio - scale) < 0.001);
chrome.test.assertEq('in', lastEvent.direction);
chrome.test.assertTrue(
Math.abs(lastEvent.startScaleRatio - scale) < 0.001);
chrome.test.assertEq(
{x: position.clientX, y: position.clientY}, lastEvent.center);
pinchSequenceListener.endPromise.then(chrome.test.succeed);
},
function testIgnoreTouchScrolling() { function testIgnoreTouchScrolling() {
let stubElement = new StubElement(); let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement); let gestureDetector = new GestureDetector(stubElement);
...@@ -177,6 +233,20 @@ chrome.test.runTests(function() { ...@@ -177,6 +233,20 @@ chrome.test.runTests(function() {
chrome.test.succeed(); chrome.test.succeed();
}, },
function testIgnoreWheelScrolling() {
let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement);
let pinchListener = new PinchListener(gestureDetector);
// A wheel event where ctrlKey is false does not indicate zooming.
let scrollingWheelEvent =
new MockWheelEvent(1, {clientX: 0, clientY: 0}, false);
stubElement.sendEvent(scrollingWheelEvent);
chrome.test.assertEq(null, pinchListener.lastEvent);
chrome.test.succeed();
},
function testPreventNativePinchZoom() { function testPreventNativePinchZoom() {
let stubElement = new StubElement(); let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement); let gestureDetector = new GestureDetector(stubElement);
...@@ -214,6 +284,36 @@ chrome.test.runTests(function() { ...@@ -214,6 +284,36 @@ chrome.test.runTests(function() {
chrome.test.succeed(); chrome.test.succeed();
}, },
function testPreventNativeZoomFromWheel() {
let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement);
let pinchListener = new PinchListener(gestureDetector);
// Ensure that the wheel listener is not passive, otherwise the call to
// preventDefault will be ignored. Since listeners could default to being
// passive, we must set the value explicitly.
for (let l of stubElement.listeners.get('wheel')) {
let options = l.options;
chrome.test.assertTrue(!!options &&
typeof(options.passive) == 'boolean');
chrome.test.assertFalse(options.passive);
}
// We should not preventDefault a wheel event where ctrlKey is false as
// that would prevent scrolling, not zooming.
let scrollingWheelEvent =
new MockWheelEvent(1, {clientX: 0, clientY: 0}, false);
stubElement.sendEvent(scrollingWheelEvent);
chrome.test.assertFalse(scrollingWheelEvent.defaultPrevented);
let zoomingWheelEvent =
new MockWheelEvent(1, {clientX: 0, clientY: 0}, true);
stubElement.sendEvent(zoomingWheelEvent);
chrome.test.assertTrue(zoomingWheelEvent.defaultPrevented);
chrome.test.succeed();
},
function testWasTwoFingerTouch() { function testWasTwoFingerTouch() {
let stubElement = new StubElement(); let stubElement = new StubElement();
let gestureDetector = new GestureDetector(stubElement); let gestureDetector = new GestureDetector(stubElement);
......
...@@ -768,6 +768,55 @@ void SimulateMouseWheelEvent(WebContents* web_contents, ...@@ -768,6 +768,55 @@ void SimulateMouseWheelEvent(WebContents* web_contents,
widget_host->ForwardWheelEvent(wheel_event); widget_host->ForwardWheelEvent(wheel_event);
} }
#if !defined(OS_MACOSX)
void SimulateMouseWheelCtrlZoomEvent(WebContents* web_contents,
const gfx::Point& point,
bool zoom_in,
blink::WebMouseWheelEvent::Phase phase) {
blink::WebMouseWheelEvent wheel_event(
blink::WebInputEvent::kMouseWheel, blink::WebInputEvent::kControlKey,
ui::EventTimeStampToSeconds(ui::EventTimeForNow()));
wheel_event.SetPositionInWidget(point.x(), point.y());
wheel_event.delta_y =
(zoom_in ? 1.0 : -1.0) * ui::MouseWheelEvent::kWheelDelta;
wheel_event.wheel_ticks_y = (zoom_in ? 1.0 : -1.0);
wheel_event.has_precise_scrolling_deltas = false;
wheel_event.phase = phase;
RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From(
web_contents->GetRenderViewHost()->GetWidget());
widget_host->ForwardWheelEvent(wheel_event);
}
#endif // !defined(OS_MACOSX)
void SimulateGesturePinchSequence(WebContents* web_contents,
const gfx::Point& point,
float scale,
blink::WebGestureDevice source_device) {
RenderWidgetHostImpl* widget_host = RenderWidgetHostImpl::From(
web_contents->GetRenderViewHost()->GetWidget());
blink::WebGestureEvent pinch_begin(
blink::WebInputEvent::kGesturePinchBegin,
blink::WebInputEvent::kNoModifiers,
ui::EventTimeStampToSeconds(ui::EventTimeForNow()));
pinch_begin.source_device = source_device;
pinch_begin.x = point.x();
pinch_begin.y = point.y();
pinch_begin.global_x = point.x();
pinch_begin.global_y = point.y();
widget_host->ForwardGestureEvent(pinch_begin);
blink::WebGestureEvent pinch_update(pinch_begin);
pinch_update.SetType(blink::WebInputEvent::kGesturePinchUpdate);
pinch_update.data.pinch_update.scale = scale;
widget_host->ForwardGestureEvent(pinch_update);
blink::WebGestureEvent pinch_end(pinch_begin);
pinch_update.SetType(blink::WebInputEvent::kGesturePinchEnd);
widget_host->ForwardGestureEvent(pinch_end);
}
void SimulateGestureScrollSequence(WebContents* web_contents, void SimulateGestureScrollSequence(WebContents* web_contents,
const gfx::Point& point, const gfx::Point& point,
const gfx::Vector2dF& delta) { const gfx::Vector2dF& delta) {
......
...@@ -156,6 +156,20 @@ void SimulateMouseWheelEvent(WebContents* web_contents, ...@@ -156,6 +156,20 @@ void SimulateMouseWheelEvent(WebContents* web_contents,
const gfx::Vector2d& delta, const gfx::Vector2d& delta,
const blink::WebMouseWheelEvent::Phase phase); const blink::WebMouseWheelEvent::Phase phase);
#if !defined(OS_MACOSX)
// Simulate a mouse wheel event with the ctrl modifier set.
void SimulateMouseWheelCtrlZoomEvent(WebContents* web_contents,
const gfx::Point& point,
bool zoom_in,
blink::WebMouseWheelEvent::Phase phase);
#endif // !defined(OS_MACOSX)
// Sends a GesturePinch Begin/Update/End sequence.
void SimulateGesturePinchSequence(WebContents* web_contents,
const gfx::Point& point,
float scale,
blink::WebGestureDevice source_device);
// Sends a simple, three-event (Begin/Update/End) gesture scroll. // Sends a simple, three-event (Begin/Update/End) gesture scroll.
void SimulateGestureScrollSequence(WebContents* web_contents, void SimulateGestureScrollSequence(WebContents* web_contents,
const gfx::Point& point, const gfx::Point& point,
......
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