Bug 1317228 - [Mortar][PDF] Implement mouse and keyboard control in presentation mode. r=evelyn, f=lchang
MozReview-Commit-ID: FLYox0cU7Ok
new file mode 100644
--- /dev/null
+++ b/browser/extensions/mortar/host/pdf/chrome/js/presentation-controller.js
@@ -0,0 +1,105 @@
+'use strict';
+
+class PresentationController {
+ constructor(viewport) {
+ this._viewport = viewport;
+
+ viewport.onFullscreenChange = this._onFullscreenChange.bind(this);
+
+ this._wheelTimestamp = 0;
+ this._wheelDelta = 0;
+ }
+
+ _onFullscreenChange(isFullscreen) {
+ if (isFullscreen) {
+ this._viewport.bindUIEvent('click', this);
+ this._viewport.bindUIEvent('wheel', this);
+ this._viewport.bindUIEvent('mousedown', this, true);
+ this._viewport.bindUIEvent('mousemove', this, true);
+ } else {
+ this._viewport.unbindUIEvent('click', this);
+ this._viewport.unbindUIEvent('wheel', this);
+ this._viewport.unbindUIEvent('mousedown', this, true);
+ this._viewport.unbindUIEvent('mousemove', this, true);
+ }
+ }
+
+ _normalizeWheelEventDelta(evt) {
+ let delta = Math.sqrt(evt.deltaX * evt.deltaX + evt.deltaY * evt.deltaY);
+ let angle = Math.atan2(evt.deltaY, evt.deltaX);
+ if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
+ // All that is left-up oriented has to change the sign.
+ delta = -delta;
+ }
+
+ let MOUSE_PIXELS_PER_LINE = 30;
+ let MOUSE_LINES_PER_PAGE = 30;
+
+ // Converts delta to per-page units
+ if (evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
+ delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
+ } else if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
+ delta /= MOUSE_LINES_PER_PAGE;
+ }
+ return delta;
+ }
+
+ _handleWheel(evt) {
+ let delta = this._normalizeWheelEventDelta(evt);
+
+ let WHEEL_COOLDOWN_TIME = 50;
+ let PAGE_SWITCH_THRESHOLD = 0.1;
+
+ let currentTime = new Date().getTime();
+ let storedTime = this._wheelTimestamp;
+
+ // If we've already switched page, avoid accidentally switching again.
+ if (currentTime - storedTime < WHEEL_COOLDOWN_TIME) {
+ return;
+ }
+ // If the scroll direction changed, reset the accumulated scroll delta.
+ if (this._wheelDelta * delta < 0) {
+ this._wheelTimestamp = this._wheelDelta = 0;
+ }
+
+ this._wheelDelta += delta;
+
+ if (Math.abs(this._wheelDelta) >= PAGE_SWITCH_THRESHOLD) {
+ this._wheelDelta > 0 ? this._viewport.page--
+ : this._viewport.page++;
+ this._wheelDelta = 0;
+ this._wheelTimestamp = currentTime;
+ }
+ }
+
+ handleEvent(evt) {
+ switch(evt.type) {
+ case 'mousedown':
+ // We catch mousedown earlier than runtime to detect if user clicked
+ // on an internal link, by watching changes of page number between
+ // mousedown and mouseup. This is also the main reason we need to invoke
+ // page change on click rather than mousedown event.
+ this._storedPageNum = this._viewport.page;
+ break;
+ case 'mousemove':
+ if (evt.buttons != 0) {
+ // We blocks mousemove when there are buttons clicked to prevent
+ // text selection. Blocking all mousemove rubs the cursor out so
+ // we just block events when there are buttons being pushed.
+ evt.stopImmediatePropagation();
+ }
+ break;
+ case 'click':
+ if (this._storedPageNum != this._viewport.page) {
+ // User may clicked on an internal link already, so we don't do
+ // further page change.
+ return;
+ }
+ this._viewport.page++;
+ break;
+ case 'wheel':
+ this._handleWheel(evt);
+ break;
+ }
+ }
+}
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewer.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewer.js
@@ -2,16 +2,17 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
'use strict';
window.addEventListener('DOMContentLoaded', function() {
let viewport = new Viewport();
let toolbar = new Toolbar(viewport);
+ let presentationController = new PresentationController(viewport);
let passwordPrompt = new PasswordPrompt(viewport);
// Expose the custom viewport object to runtime
window.createCustomViewport = function(actionHandler) {
viewport.registerActionHandler(actionHandler);
return {
addView: viewport.addView.bind(viewport),
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
@@ -45,16 +45,17 @@ class Viewport {
// the document dimension is revealed to move the view.
this._initPosition = null;
this.onProgressChanged = null;
this.onZoomChanged = null;
this.onDimensionChanged = null;
this.onPageChanged = null;
this.onPasswordRequest = null;
+ this.onFullscreenChange = null;
this._viewportController.addEventListener('scroll', this);
this._viewportController.addEventListener('copy', this);
window.addEventListener('resize', this);
}
get zoom() {
return this._zoom;
@@ -436,16 +437,20 @@ class Viewport {
// No need to call "_setZoom" here because we will deal with zooming
// case in the "_setPage" below.
} else {
this._zoom = this._previousZoom;
this._fitting = this._previousFitting;
this._setZoom(this._computeFittingZoom());
}
+ if (typeof this.onFullscreenChange === 'function') {
+ this.onFullscreenChange(fullscreen);
+ }
+
this._fullscreenStatus = fullscreen ? 'fullscreen' : 'none';
// Reset position to the beginning of the current page.
this._setPage(currentPage);
this._refresh();
}, 100);
}
@@ -696,43 +701,43 @@ class Viewport {
getBoundingClientRect() {
return this._canvasContainer.getBoundingClientRect();
}
is(element) {
return element == this._viewportController;
}
- bindUIEvent(type, listener) {
+ bindUIEvent(type, listener, useCapture = false) {
if (type == 'fullscreenchange' || type == 'MozScrolledAreaChanged') {
// These two events won't be bound on a target because they should be
// fully controlled by UI layer, and we'll manually trigger the resize
// event once needed.
return;
}
switch(type) {
case 'resize':
this._runtimeOnResizedListener.push(listener);
break;
default:
- this._getEventTarget(type).addEventListener(type, listener);
+ this._getEventTarget(type).addEventListener(type, listener, useCapture);
}
}
- unbindUIEvent(type, listener) {
+ unbindUIEvent(type, listener, useCapture = false) {
if (type == 'fullscreenchange' || type == 'MozScrolledAreaChanged') {
return;
}
switch(type) {
case 'resize':
this._runtimeOnResizedListener =
this._runtimeOnResizedListener.filter(item => item != listener);
break;
default:
- this._getEventTarget(type).removeEventListener(type, listener);
+ this._getEventTarget(type).removeEventListener(type, listener, useCapture);
}
}
setCursor(cursor) {
this._viewportController.style.cursor = cursor;
}
getScrollOffset() {
@@ -763,11 +768,14 @@ class Viewport {
this._copyToClipboard(message.selectedText);
break;
case 'hashChange':
this._handleHashChange(message.hash);
break;
case 'command':
this._handleCommand(message.name);
break;
+ case 'goToPage':
+ this.page = message.page;
+ break;
}
}
}
--- a/browser/extensions/mortar/host/pdf/chrome/viewer.html
+++ b/browser/extensions/mortar/host/pdf/chrome/viewer.html
@@ -14,16 +14,17 @@
<link rel="stylesheet" href="style/viewer.css">
<script src="js/l20n.js"></script>
<script src="js/polyfill.js"></script>
<script src="js/toolbar.js"></script>
<script src="js/viewport.js"></script>
<script src="js/password-prompt.js"></script>
+ <script src="js/presentation-controller.js"></script>
<script src="js/viewer.js"></script>
</head>
<body tabindex="1" class="loadingInProgress">
<div id="outerContainer">
<div id="sidebarContainer">
<div id="toolbarSidebar">