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 502164 564ee834041bd200af06a71e2a8750d3182da969
parent 499127 85bd36dd5f580ee94b5191e2776a9709dead4f98
child 550088 2e99e9dee8ed1ba1ea0b7526a1ee2e5a9b15ac04
push id50207
push userbmo:rexboy@mozilla.com
push dateTue, 21 Mar 2017 11:03:43 +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,106 @@
+'use strict';
+
+class PresentationController {
+  constructor(viewport) {
+    this._viewportController = document.getElementById('viewportController');
+    this._viewport = viewport;
+
+    // We need to catch mousedown earlier than runtime to detect if user clicked
+    // on an internal link by watching changes of page number.
+    this._viewportController.addEventListener('mousedown', this);
+    document.addEventListener('fullscreenchange', this);
+
+    viewport.onFullscreenChange = this._onFullscreenChange.bind(this);
+
+    this._wheelTimestamp = 0;
+    this._wheelDelta = 0;
+  }
+
+  _onFullscreenChange(isFullscreen) {
+    if (isFullscreen) {
+      window.addEventListener('keydown', this);
+      this._viewportController.addEventListener('click', this);
+      this._viewportController.addEventListener('wheel', this);
+    } else {
+      window.removeEventListener('keydown', this);
+      this._viewportController.removeEventListener('click', this);
+      this._viewportController.removeEventListener('wheel', this);
+    }
+  }
+
+  _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_DOM_DELTA_PIXEL_MODE = 0;
+    let MOUSE_DOM_DELTA_LINE_MODE = 1;
+    let MOUSE_PIXELS_PER_LINE = 30;
+    let MOUSE_LINES_PER_PAGE = 30;
+
+    // Converts delta to per-page units
+    if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
+      delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
+    } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
+      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 'keydown':
+        if (evt.key == 'ArrowLeft' || evt.key == "ArrowUp") {
+          this._viewport.page--;
+        } else if (evt.key == 'ArrowRight' || evt.key == 'ArrowDown') {
+          this._viewport.page++;
+        }
+        break;
+      case 'mousedown':
+        this._storedPageNum = this._viewport.page;
+        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);
   }
 
@@ -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">