Bug 1317228 - [Mortar][PDF] Implement mouse and keyboard control in presentation mode. r=evelyn, f=lchang draft
authorRex Lee <rexboy@mozilla.com>
Tue, 21 Mar 2017 15:35:38 +0800
changeset 502791 f705011e9b7745551956b9b59119d6693d1e3978
parent 499127 85bd36dd5f580ee94b5191e2776a9709dead4f98
child 503582 49c29b9913feaa0ae3dbf0ccfd75377a52f3a9e2
child 504535 81a8d9accc8164c338f6a80e5f6fa8d39742f752
child 556646 5096b782e34d5da86dded01e05e26f455396a15f
child 557741 74c2c1bc694a5c0e177703aada91dd2556c7c93d
push id50404
push userbmo:rexboy@mozilla.com
push dateWed, 22 Mar 2017 10:43:07 +0000
reviewersevelyn
bugs1317228
milestone55.0a1
Bug 1317228 - [Mortar][PDF] Implement mouse and keyboard control in presentation mode. r=evelyn, f=lchang MozReview-Commit-ID: FLYox0cU7Ok
browser/extensions/mortar/host/pdf/chrome/js/presentation-controller.js
browser/extensions/mortar/host/pdf/chrome/js/viewer.js
browser/extensions/mortar/host/pdf/chrome/js/viewport.js
browser/extensions/mortar/host/pdf/chrome/viewer.html
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">