Bug 1338095 - [Mortar] Implement "View bookmark" button in PDF viewer. f=lchang, f=lochang, r=ehung
MozReview-Commit-ID: 2W0Fqy9KGuP
--- 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);