Bug 1299405 - [jsplugins][UI] Implement presentation mode. r=evelyn draft
authorLuke Chang <lchang@mozilla.com>
Mon, 09 Jan 2017 14:43:21 +0800
changeset 462921 0884750dadefe2fd9ba24f82fd7755bd6c0b9832
parent 462769 80eac484366ad881c6a10bf81e8d9b8f7a676c75
child 542528 5dc733b90c519028ee2e39f9c26a8b75166ff070
push id41904
push userbmo:lchang@mozilla.com
push dateWed, 18 Jan 2017 07:44:57 +0000
reviewersevelyn
bugs1299405
milestone53.0a1
Bug 1299405 - [jsplugins][UI] Implement presentation mode. r=evelyn MozReview-Commit-ID: DYkul38pL3v
browser/extensions/mortar/host/pdf/chrome/js/toolbar.js
browser/extensions/mortar/host/pdf/chrome/js/viewport.js
browser/extensions/mortar/host/pdf/chrome/style/viewer.css
browser/extensions/mortar/host/pdf/chrome/viewer.html
--- a/browser/extensions/mortar/host/pdf/chrome/js/toolbar.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/toolbar.js
@@ -199,16 +199,20 @@ class Toolbar {
         this._viewport.save();
         break;
       case 'pageRotateCw':
         this._viewport.rotateClockwise();
         break;
       case 'pageRotateCcw':
         this._viewport.rotateCounterClockwise();
         break;
+      case 'presentationMode':
+      case 'secondaryPresentationMode':
+        this._viewport.fullscreen = true;
+        break;
       case 'secondaryToolbarToggle':
         this._secondaryToolbar.toggle();
         break;
     }
   }
 
   _pageNumberChanged() {
     let newPage = parseFloat(this._elements.pageNumber.value);
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
@@ -1,25 +1,24 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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';
 
 class Viewport {
   constructor() {
+    this._viewerContainer = document.getElementById('viewerContainer');
+    this._fullscreenWrapper = document.getElementById('fullscreenWrapper');
     this._canvasContainer = document.getElementById('canvasContainer');
     this._viewportController = document.getElementById('viewportController');
     this._sizer = document.getElementById('sizer');
 
+    this._fullscreenStatus = 'none';
     this._scrollbarWidth = this._getScrollbarWidth();
-    this._hasVisibleScrollbars = {
-      horizontal: false,
-      vertical: false
-    };
 
     this._page = 0;
     this._zoom = 1;
     this._fitting = 'auto';
 
     // Caches the next position set during a series of actions and will be set
     // to scrollbar's position until calling "_refresh".
     this._nextPosition = null;
@@ -92,16 +91,42 @@ class Viewport {
       return;
     }
 
     newPage = Math.max(0, Math.min(pageCount - 1, newPage));
     this._setPage(newPage);
     this._refresh();
   }
 
+  get fullscreen() {
+    return this._fullscreenStatus != 'none';
+  }
+
+  set fullscreen(enable) {
+    if (this._fullscreenStatus == 'changing' ||
+        this._fullscreenStatus == (enable ? 'fullscreen' : 'none')) {
+      return;
+    }
+
+    // The next step after sending "setFullscreen" will happen in the function
+    // "_handleFullscreenChange" triggered by "fullscreenChange" event. The
+    // "_fullscreenStatus" will also be reset there. Note that the viewport
+    // stops refreshing while in the "changing" status to avoid flickers.
+    //
+    // XXX: Since we rely on "fullscreenChange" event to reset the status, the
+    //      viewport might freeze if, for some reason, the event isn't sent back
+    //      and we get stuck in the "changing" status. Not sure if it's the case
+    //      we need to worry about though.
+    this._fullscreenStatus = 'changing';
+    this._doAction({
+      type: 'setFullscreen',
+      fullscreen: enable
+    });
+  }
+
   _getScrollbarWidth() {
     var div = document.createElement('div');
     div.style.visibility = 'hidden';
     div.style.overflow = 'scroll';
     div.style.width = '50px';
     div.style.height = '50px';
     div.style.position = 'absolute';
     document.body.appendChild(div);
@@ -144,29 +169,35 @@ class Viewport {
     this._setZoom(this._computeFittingZoom());
     this._setPage(this._page);
     if (typeof this.onDimensionChanged === 'function') {
       this.onDimensionChanged();
     }
     this._refresh();
   }
 
-  _computeFittingZoom() {
+  _computeFittingZoom(pageIndex) {
     let newZoom = this._zoom;
     let fitting = this._fitting;
 
-    if (fitting == 'none') {
+    if (pageIndex === undefined) {
+      pageIndex = this._page;
+    }
+
+    if (fitting == 'none' || pageIndex < 0 || pageIndex >= this.pageCount) {
       return newZoom;
     }
 
     let FITTING_PADDING = 40;
     let MAX_AUTO_ZOOM = 1.25;
 
-    let page = this._pageDimensions[this._page];
-    let viewportRect = this.getBoundingClientRect();
+    let page = this._pageDimensions[pageIndex];
+    let viewportRect = this.fullscreen ?
+      this._viewerContainer.getBoundingClientRect() :
+      this.getBoundingClientRect();
 
     let pageWidthZoom = (viewportRect.width - FITTING_PADDING) / page.width;
     let pageHeightZoom = viewportRect.height / page.height;
 
     switch (fitting) {
       case 'auto':
         let isLandscape = (page.width > page.height);
         // For pages in landscape mode, fit the page height to the viewer
@@ -284,29 +315,40 @@ class Viewport {
       this.onZoomChanged(this._zoom);
     }
   }
 
   _setPage(newPage) {
     if (newPage < 0 || newPage >= this.pageCount) {
       return;
     }
+
+    if (this.fullscreen) {
+      let pageDimension = this._pageDimensions[newPage];
+      let newZoom = this._computeFittingZoom(newPage);
+
+      this._fullscreenWrapper.style.width =
+        (pageDimension.width * newZoom) + 'px';
+      this._fullscreenWrapper.style.height =
+        (pageDimension.height * newZoom) + 'px';
+
+      if (newZoom != this._zoom) {
+        this._setZoom(newZoom);
+      }
+      this._notifyRuntimeOfResized();
+    }
+
     this._setPosition(
       this._pageDimensions[newPage].x * this._zoom,
       this._pageDimensions[newPage].y * this._zoom
     );
   }
 
   _updateCanvasSize() {
     let hasScrollbars = this._documentHasVisibleScrollbars(this._zoom);
-    if (hasScrollbars.horizontal == this._hasVisibleScrollbars.horizontal &&
-        hasScrollbars.vertical == this._hasVisibleScrollbars.vertical) {
-      return;
-    }
-    this._hasVisibleScrollbars = hasScrollbars;
     this._canvasContainer.style.bottom =
       hasScrollbars.horizontal ? this._scrollbarWidth + 'px' : 0;
     this._canvasContainer.style.right =
       hasScrollbars.vertical ? this._scrollbarWidth + 'px' : 0;
     this._notifyRuntimeOfResized();
   }
 
   _contentSizeChanged() {
@@ -344,16 +386,49 @@ class Viewport {
       if (typeof listener === 'function') {
         listener(evt);
       } else if (typeof listener.handleEvent === 'function') {
         listener.handleEvent(evt);
       }
     });
   }
 
+  _handleFullscreenChange(fullscreen) {
+    // Set status to "changing" again in case it isn't triggered by setter.
+    this._fullscreenStatus = 'changing';
+    this._viewerContainer.classList.toggle('pdfPresentationMode', fullscreen);
+
+    // XXX: DOM elements' size changing hasn't taken place when fullscreenChange
+    //      event is triggered in our setup, so "setTimeout" is necessary to get
+    //      the exact size. The 100ms delay is set based on try-and-error. We
+    //      might need to find a proper way to know when exactly the resizing is
+    //      done.
+    setTimeout(() => {
+      let currentPage = this._page;
+
+      if (fullscreen) {
+        this._previousZoom = this._zoom;
+        this._previousFitting = this._fitting;
+        this._fitting = 'page-fit';
+        // 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());
+      }
+
+      this._fullscreenStatus = fullscreen ? 'fullscreen' : 'none';
+
+      // Reset position to the beginning of the current page.
+      this._setPage(currentPage);
+      this._refresh();
+    }, 100);
+  }
+
   _getEventTarget(type) {
     switch(type) {
       case 'keydown':
       case 'keyup':
       case 'keypress':
         return window;
     }
     return this._viewportController;
@@ -361,16 +436,20 @@ class Viewport {
 
   _doAction(message) {
     if (this._actionHandler) {
       this._actionHandler(message);
     }
   }
 
   _refresh() {
+    if (this._fullscreenStatus == 'changing') {
+      return;
+    }
+
     if (this._nextPosition) {
       this._viewportController.scrollTo(
         this._nextPosition.x, this._nextPosition.y);
       this._nextPosition = null;
     }
 
     this._runtimePosition = this.getScrollOffset();
     this._doAction({
@@ -387,31 +466,38 @@ class Viewport {
         this.onPageChanged(newPage);
       }
     }
   }
 
   handleEvent(evt) {
     switch(evt.type) {
       case 'resize':
-        this._resize();
-        this._notifyRuntimeOfResized();
-        this._refresh();
+        this.invokeResize();
         break;
       case 'scroll':
         this._nextPosition = null;
         let position = this.getScrollOffset();
         if (this._runtimePosition.x != position.x ||
             this._runtimePosition.y != position.y) {
           this._refresh();
         }
         break;
     }
   }
 
+  invokeResize() {
+    if (this._fullscreenStatus == 'changing') {
+      return;
+    }
+    this._resize();
+    this._notifyRuntimeOfResized();
+    this._refresh();
+  }
+
   rotateClockwise() {
     this._doAction({
       type: 'rotateClockwise'
     });
   }
 
   rotateCounterClockwise() {
     this._doAction({
@@ -497,11 +583,14 @@ class Viewport {
   notify(message) {
     switch (message.type) {
       case 'loadProgress':
         this._updateProgress(message.progress);
         break;
       case 'documentDimensions':
         this._setDocumentDimensions(message);
         break;
+      case 'fullscreenChange':
+        this._handleFullscreenChange(message.fullscreen);
+        break;
     }
   }
 }
--- a/browser/extensions/mortar/host/pdf/chrome/style/viewer.css
+++ b/browser/extensions/mortar/host/pdf/chrome/style/viewer.css
@@ -48,40 +48,16 @@ select {
 
 .hidden {
   display: none !important;
 }
 [hidden] {
   display: none !important;
 }
 
-#viewerContainer.pdfPresentationMode:fullscreen {
-  top: 0px;
-  border-top: 2px solid transparent;
-  background-color: #000;
-  width: 100%;
-  height: 100%;
-  overflow: hidden;
-  cursor: none;
-  -moz-user-select: none;
-}
-
-.pdfPresentationMode:fullscreen a:not(.internalLink) {
-  display: none;
-}
-
-.pdfPresentationMode:fullscreen .textLayer > div {
-  cursor: none;
-}
-
-.pdfPresentationMode.pdfPresentationModeControls > *,
-.pdfPresentationMode.pdfPresentationModeControls .textLayer > div {
-  cursor: default;
-}
-
 /* outer/inner center provides horizontal center */
 .outerCenter {
   pointer-events: none;
   position: relative;
 }
 html[dir='ltr'] .outerCenter {
   float: right;
   right: 50%;
@@ -173,33 +149,55 @@ html[dir='ltr'] #sidebarContent {
 html[dir='rtl'] #sidebarContent {
   right: 0;
   box-shadow: inset 1px 0 0 hsla(0,0%,0%,.25);
 }
 
 #viewerContainer {
   overflow: hidden;
   position: absolute;
-  top: var(--toolbar-height);
+  top: calc(var(--toolbar-height) + 2px);
   right: 0;
   bottom: 0;
   left: 0;
   outline: none;
 }
 html[dir='ltr'] #viewerContainer {
   box-shadow: inset 1px 0 0 hsla(0,0%,100%,.05);
 }
 html[dir='rtl'] #viewerContainer {
   box-shadow: inset -1px 0 0 hsla(0,0%,100%,.05);
 }
 
+#viewerContainer.pdfPresentationMode {
+  position: fixed;
+  top: 0;
+  left: 0;
+  background-color: #000;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  cursor: none;
+  -moz-user-select: none;
+  z-index: 99999;
+
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.pdfPresentationMode #fullscreenWrapper {
+  position: relative;
+  overflow: hidden;
+}
+
 #canvasContainer, #viewportController {
   position: absolute;
   left: 0;
-  top: 2px;
+  top: 0;
   right: 0;
   bottom: 0;
   outline: none;
   overflow: hidden;
 }
 
 #canvasContainer canvas {
   display: block;
@@ -207,16 +205,20 @@ html[dir='rtl'] #viewerContainer {
   left: 0;
   top: 0;
 }
 
 #viewportController {
   overflow: auto;
 }
 
+.pdfPresentationMode #viewportController {
+  overflow: hidden;
+}
+
 #sizer {
   margin: 0 auto;
 }
 
 .toolbar {
   position: relative;
   left: 0;
   right: 0;
--- a/browser/extensions/mortar/host/pdf/chrome/viewer.html
+++ b/browser/extensions/mortar/host/pdf/chrome/viewer.html
@@ -209,19 +209,21 @@
                 <div class="glimmer">
                 </div>
               </div>
             </div>
           </div>
         </div>
 
         <div id="viewerContainer">
-          <div id="canvasContainer"></div>
-          <div id="viewportController" tabindex="0">
-            <div id="sizer"></div>
+          <div id="fullscreenWrapper">
+            <div id="canvasContainer"></div>
+            <div id="viewportController" tabindex="0">
+              <div id="sizer"></div>
+            </div>
           </div>
         </div>
 
         <div id="errorWrapper" hidden='true'>
           <div id="errorMessageLeft">
             <span id="errorMessage"></span>
             <button id="errorShowMore" data-l10n-id="error_more_info">
               More Information