Bug 1338095 - [Mortar] Implement "View bookmark" button in PDF viewer. f=lchang, f=lochang, r=evelyn
authorRex Lee <rexboy@mozilla.com>
Wed, 22 Feb 2017 19:51:52 +0800
changeset 346693 24a43c8ae12278b79e7d0673fb9b5c824534161d
parent 346692 9679b42696bd36b7e3e2521525a19151a818fdfe
child 346694 a7f51c857549fa3d89d421308ea9a89237452887
push id38580
push userehung@mozilla.com
push dateThu, 09 Mar 2017 10:06:26 +0000
treeherderautoland@24a43c8ae122 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersevelyn
bugs1338095
milestone55.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 1338095 - [Mortar] Implement "View bookmark" button in PDF viewer. f=lchang, f=lochang, r=evelyn MozReview-Commit-ID: 2W0Fqy9KGuP
browser/extensions/mortar/host/common/ppapi-runtime.jsm
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
browser/extensions/mortar/host/pdf/ppapi-content-sandbox.js
--- a/browser/extensions/mortar/host/common/ppapi-runtime.jsm
+++ b/browser/extensions/mortar/host/common/ppapi-runtime.jsm
@@ -6,16 +6,17 @@
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/ctypes.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://ppapi.js/opengles2-utils.jsm");
+Cu.importGlobalProperties(['URL']);
 
 const PP_OK = 0;
 const PP_OK_COMPLETIONPENDING = -1;
 const PP_ERROR_FAILED = -2;
 const PP_ERROR_ABORTED = -3;
 const PP_ERROR_BADARGUMENT = -4;
 const PP_ERROR_BADRESOURCE = -5;
 const PP_ERROR_NOINTERFACE = -6;
@@ -1533,22 +1534,39 @@ class PPAPIInstance {
     this.mm = mm;
     this.eventHandlers = 0;
     this.filteringEventHandlers = 0;
     this.throttled_ = false;
     this.cachedImageData = null;
     this.viewport = new PPAPIViewport(this);
     this.selectedText = "";
 
+    this.notifyHashChange(info.url);
+
     this.mm.addMessageListener("ppapi.js:fullscreenchange", (evt) => {
       this.viewport.notify({
         type: "fullscreenChange",
         fullscreen: evt.data.fullscreen
       });
     });
+
+    this.mm.addMessageListener("ppapipdf.js:hashchange", (evt) => {
+      this.notifyHashChange(evt.data.url);
+    });
+  }
+
+  notifyHashChange(url) {
+    let location = new URL(url);
+    if (location.hash) {
+      this.viewport.notify({
+        type: "hashChange",
+        // substring(1) for getting rid of the first '#' character
+        hash: location.hash.substring(1)
+      });
+    }
   }
 
   bindGraphics(graphicsDevice) {
     if (graphicsDevice) {
       let canvas = graphicsDevice.canvas;
 
       // FIXME This size should be adjusted according to devicePixelRatio.
       canvas.style.width = canvas.width;
@@ -1668,16 +1686,19 @@ class PPAPIInstance {
   viewportActionHandler(message) {
     switch(message.type) {
       case 'setFullscreen':
         this.mm.sendAsyncMessage("ppapi.js:setFullscreen", message.fullscreen);
         break;
       case 'save':
         this.mm.sendAsyncMessage("ppapipdf.js:save");
         break;
+      case 'setHash':
+        this.mm.sendAsyncMessage("ppapipdf.js:setHash", message.hash);
+        break;
       case 'viewport':
       case 'rotateClockwise':
       case 'rotateCounterclockwise':
       case 'selectAll':
       case 'getSelectedText':
       case 'getNamedDestination':
       case 'getPasswordComplete':
         let data = PP_Var.fromJSValue(new Dictionary(message), this);
--- a/browser/extensions/mortar/host/pdf/chrome/js/toolbar.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/toolbar.js
@@ -205,16 +205,20 @@ class Toolbar {
         break;
       case 'presentationMode':
       case 'secondaryPresentationMode':
         this._viewport.fullscreen = true;
         break;
       case 'secondaryToolbarToggle':
         this._secondaryToolbar.toggle();
         break;
+      case 'viewBookmark':
+      case 'secondaryViewBookmark':
+        this._viewport.createBookmarkHash();
+        break;
     }
   }
 
   _pageNumberChanged() {
     let newPage = parseFloat(this._elements.pageNumber.value);
     if (!Number.isInteger(newPage) ||
         newPage < 1 || newPage > this._viewport.pageCount) {
       this._elements.pageNumber.value = this._viewport.page + 1;
--- a/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
+++ b/browser/extensions/mortar/host/pdf/chrome/js/viewport.js
@@ -1,16 +1,23 @@
 /* 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';
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
+// When handling Bookmark View, we use Cartesian coordinate system which is
+// different from PDFium engine. Our origin is at bottom-left of every page,
+// while PDFium counts y position continuously from the top of page 1.
+// Moreover, the coordinate used in PDF.js is scaled by 0.75 for some reason,
+// so we keep it here for backward compability.
+const PAGE_COORDINATE_RATIO = 0.75;
+
 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');
 
@@ -29,16 +36,20 @@ class Viewport {
     // if "scroll" event gives us a different value.
     this._runtimePosition = this.getScrollOffset();
 
     // Similar to above. Will notify runtime only if "_notifyRuntimeOfResized()"
     // gets a different value.
     this._runtimeSize = this.getBoundingClientRect();
     this._runtimeOnResizedListener = [];
 
+    // If the document is opened with a bookmarkView hash, we save it until
+    // 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._viewportController.addEventListener('scroll', this);
     this._viewportController.addEventListener('copy', this);
@@ -59,18 +70,17 @@ class Viewport {
     }
   }
 
   get fitting() {
     return this._fitting;
   }
 
   set fitting(newFitting) {
-    let VALID_VALUE = ['none', 'auto', 'page-actual', 'page-width', 'page-fit'];
-    if (!VALID_VALUE.includes(newFitting)) {
+    if (!this._isValidFitting(newFitting)) {
       return;
     }
 
     if (newFitting != this._fitting) {
       this._fitting = newFitting;
       this._setZoom(this._computeFittingZoom());
       this._refresh();
     }
@@ -121,16 +131,21 @@ class Viewport {
     //      we need to worry about though.
     this._fullscreenStatus = 'changing';
     this._doAction({
       type: 'setFullscreen',
       fullscreen: enable
     });
   }
 
+  _isValidFitting(fitting) {
+    let VALID_VALUE = ['none', 'auto', 'page-actual', 'page-width', 'page-fit'];
+    return VALID_VALUE.includes(fitting);
+  }
+
   _getScrollbarWidth() {
     let 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);
@@ -170,17 +185,23 @@ class Viewport {
   _setDocumentDimensions(documentDimensions) {
     this._documentDimensions = documentDimensions;
     this._pageDimensions = documentDimensions.pageDimensions;
     this._setZoom(this._computeFittingZoom());
     this._setPage(this._page);
     if (typeof this.onDimensionChanged === 'function') {
       this.onDimensionChanged();
     }
-    this._refresh();
+
+    if (this._initPosition) {
+      this._jumpToBookmark(this._initPosition);
+      this._initPosition = null;
+    } else {
+      this._refresh();
+    }
   }
 
   _computeFittingZoom(pageIndex) {
     let newZoom = this._zoom;
     let fitting = this._fitting;
 
     if (pageIndex === undefined) {
       pageIndex = this._page;
@@ -482,16 +503,102 @@ class Viewport {
     if (!text) {
       return;
     }
     const gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"]
                                 .getService(Ci.nsIClipboardHelper);
     gClipboardHelper.copyString(text);
   }
 
+  _getPageCoordinate() {
+    let currentPos = this._nextPosition || this.getScrollOffset();
+    let pageDimension = this._pageDimensions[this._page];
+
+    let pageOrigin = {
+      x: pageDimension.x,
+      y: pageDimension.y + pageDimension.height
+    };
+    currentPos.x /= this._zoom;
+    currentPos.y /= this._zoom;
+
+    let pageCoordinate = {
+      x: Math.round((currentPos.x - pageOrigin.x) * PAGE_COORDINATE_RATIO),
+      y: Math.round((pageOrigin.y - currentPos.y) * PAGE_COORDINATE_RATIO)
+    };
+    return pageCoordinate;
+  }
+
+  _getScreenCooridnate(pageNo, pageX, pageY) {
+    let pageDimension = this._pageDimensions[pageNo];
+    // Both pageX and pageY are omittable, and in this case, we assume the most
+    // top and left corner of the page as their default values.
+    pageX = Number.isInteger(pageX) ? pageX : 0;
+    pageY = Number.isInteger(pageY) ? pageY : pageDimension.height *
+                                              PAGE_COORDINATE_RATIO;
+    pageX /= PAGE_COORDINATE_RATIO;
+    pageY /= PAGE_COORDINATE_RATIO;
+
+    let pageOrigin = {
+      x: pageDimension.x,
+      y: pageDimension.y + pageDimension.height
+    };
+
+    return {
+      x: Math.round((pageX - pageOrigin.x) * this._zoom),
+      y: Math.round((pageOrigin.y - pageY) * this._zoom)
+    };
+  }
+
+  /**
+   * @param hash
+   *        contains page and zoom parameters which should be in the following
+   *        format: page={page}&zoom={scale},{x},{y}
+   *        for example the following hashes are valid:
+   *        page=1&zoom=auto,100,100
+   *        page=3&zoom=300,10,-50
+   */
+  _jumpToBookmark(hash) {
+    let params = {};
+    hash.split('&').forEach(param => {
+      let [name, value] = param.split('=');
+      params[name.toLowerCase()] = value.toLowerCase();
+    });
+
+    let pageNo = parseInt(params.page, 10);
+    pageNo = Number.isNaN(pageNo) ? this._page : pageNo;
+    pageNo = Math.max(0, Math.min(this.pageCount - 1, pageNo - 1));
+
+    params.zoom = (typeof(params.zoom) == 'string') ? params.zoom : "";
+    let [scale, pageX, pageY] = params.zoom.split(',');
+    pageX = parseInt(pageX, 10);
+    pageY = parseInt(pageY, 10);
+
+    if (this._isValidFitting(scale)) {
+      this._fitting = scale;
+    } else {
+      this._fitting = 'none';
+      let zoom = parseFloat(scale);
+      zoom = (Number.isNaN(zoom) || zoom <= 0) ? 100 : zoom;
+      this._zoom = zoom / 100;
+    }
+
+    let screenPos = this._getScreenCooridnate(pageNo, pageX, pageY);
+    this._setPosition(screenPos.x, screenPos.y);
+    this._setZoom(this._computeFittingZoom());
+    this._refresh();
+  }
+
+  _handleHashChange(hash) {
+    if (!this._documentDimensions) {
+      this._initPosition = hash;
+    } else {
+      this._jumpToBookmark(hash);
+    }
+  }
+
   verifyPassword(password) {
     this._doAction({
       type: 'getPasswordComplete',
       password: password
     });
   }
 
   handleEvent(evt) {
@@ -545,16 +652,31 @@ class Viewport {
 
   // A handler for delivering messages to runtime.
   registerActionHandler(handler) {
     if (typeof handler === 'function') {
       this._actionHandler = handler;
     }
   }
 
+  createBookmarkHash() {
+    let pagePosition = this._getPageCoordinate();
+    let scale = this._fitting == 'none' ?
+                  Math.round(this._zoom * 100) :
+                  this._fitting;
+    let hash = "page=" + (this._page + 1) +
+               "&zoom=" + scale +
+               "," + pagePosition.x +
+               "," + pagePosition.y;
+    this._doAction({
+      type: 'setHash',
+      hash: hash
+    })
+  }
+
   /***************************/
   /* PPAPIViewport Interface */
   /***************************/
 
   addView(canvas) {
     this._canvasContainer.appendChild(canvas);
   }
 
@@ -626,11 +748,14 @@ class Viewport {
       case 'getPassword':
         this.onPasswordRequest && this.onPasswordRequest();
         break;
       case 'getSelectedTextReply':
         // For now this message is used only by text copy so we handle just
         // that case.
         this._copyToClipboard(message.selectedText);
         break;
+      case 'hashChange':
+        this._handleHashChange(message.hash);
+        break;
     }
   }
 }
--- a/browser/extensions/mortar/host/pdf/chrome/style/viewer.css
+++ b/browser/extensions/mortar/host/pdf/chrome/style/viewer.css
@@ -884,21 +884,16 @@ html[dir='rtl'] .toolbarButton.pageDown:
   outline: none;
   padding-top: 4px;
   text-decoration: none;
 }
 .secondaryToolbarButton.bookmark {
   padding-top: 5px;
 }
 
-.bookmark[href='#'] {
-  opacity: .5;
-  pointer-events: none;
-}
-
 .toolbarButton.bookmark::before,
 .secondaryToolbarButton.bookmark::before {
   content: url(images/toolbarButton-bookmark.png);
 }
 html[dir="ltr"] #viewOutline.toolbarButton::before {
   content: url(images/toolbarButton-viewOutline.png);
 }
 html[dir="rtl"] #viewOutline.toolbarButton::before {
--- a/browser/extensions/mortar/host/pdf/chrome/viewer.html
+++ b/browser/extensions/mortar/host/pdf/chrome/viewer.html
@@ -69,19 +69,19 @@
             <button id="secondaryPrint" class="secondaryToolbarButton print visibleMediumView" title="Print" tabindex="53" data-l10n-id="print">
               <span data-l10n-id="print_label">Print</span>
             </button>
 
             <button id="secondaryDownload" class="secondaryToolbarButton download visibleMediumView" title="Download" tabindex="54" data-l10n-id="download">
               <span data-l10n-id="download_label">Download</span>
             </button>
 
-            <a href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="55" data-l10n-id="bookmark">
+            <button href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="55" data-l10n-id="bookmark">
               <span data-l10n-id="bookmark_label">Current View</span>
-            </a>
+            </button>
 
             <div class="horizontalToolbarSeparator visibleLargeView"></div>
 
             <button id="firstPage" class="secondaryToolbarButton firstPage" title="Go to First Page" tabindex="56" data-l10n-id="first_page">
               <span data-l10n-id="first_page_label">Go to First Page</span>
             </button>
             <button id="lastPage" class="secondaryToolbarButton lastPage" title="Go to Last Page" tabindex="57" data-l10n-id="last_page">
               <span data-l10n-id="last_page_label">Go to Last Page</span>
@@ -130,19 +130,19 @@
 
                 <button id="print" class="toolbarButton print hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print">
                   <span data-l10n-id="print_label">Print</span>
                 </button>
 
                 <button id="download" class="toolbarButton download hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download">
                   <span data-l10n-id="download_label">Download</span>
                 </button>
-                <a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
+                <button target="_blank" id="viewBookmark" class="toolbarButton bookmark hiddenMediumView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
                   <span data-l10n-id="bookmark_label">Current View</span>
-                </a>
+                </button>
 
                 <div class="verticalToolbarSeparator hiddenSmallView"></div>
 
                 <button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools">
                   <span data-l10n-id="tools_label">Tools</span>
                 </button>
               </div>
               <div class="outerCenter">
--- a/browser/extensions/mortar/host/pdf/ppapi-content-sandbox.js
+++ b/browser/extensions/mortar/host/pdf/ppapi-content-sandbox.js
@@ -84,26 +84,37 @@ mm.addMessageListener("ppapi.js:frameLoa
 
   mm.sendAsyncMessage("ppapi.js:createInstance",  { type: "pdf", info },
                       { pluginWindow: containerWindow });
 
   containerWindow.document.addEventListener("fullscreenchange", () => {
     let fullscreen = (containerWindow.document.fullscreenElement == pluginElement);
     mm.sendAsyncMessage("ppapi.js:fullscreenchange", { fullscreen });
   });
+
+  containerWindow.addEventListener("hashchange", () => {
+    let url = containerWindow.location.href;
+    mm.sendAsyncMessage("ppapipdf.js:hashchange", { url });
+  })
 });
 
 mm.addMessageListener("ppapi.js:setFullscreen", ({ data }) => {
   if (data) {
     pluginElement.requestFullscreen();
   } else {
     containerWindow.document.exitFullscreen();
   }
 });
 
+mm.addMessageListener("ppapipdf.js:setHash", ({ data }) => {
+  if (data) {
+    containerWindow.location.hash = data;
+  }
+});
+
 mm.addMessageListener("ppapipdf.js:save", () => {
   let url = containerWindow.document.location;
   let filename = "document.pdf";
   let regex = /[^\/#\?]+\.pdf$/i;
 
   let result = regex.exec(url.hash) ||
                regex.exec(url.search) ||
                regex.exec(url.pathname);