Bug 1299405 - [jsplugins][UI] Implement presentation mode. r=evelyn
authorLuke Chang <lchang@mozilla.com>
Mon, 09 Jan 2017 14:43:21 +0800
changeset 329906 ae0f6512ab35dea681bc48be51aa15a845621dcc
parent 329905 27346172ee35332e28011fc4143de2bce10eaef8
child 329907 1d88fc311d4b546cf587698a836ff7880003b43f
push id36173
push userryanvm@gmail.com
push dateWed, 18 Jan 2017 15:11:11 +0000
treeherderautoland@ae0f6512ab35 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersevelyn
bugs1299405
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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