diff options
Diffstat (limited to 'chromium/chrome/browser/resources/pdf')
54 files changed, 2808 insertions, 2042 deletions
diff --git a/chromium/chrome/browser/resources/pdf/BUILD.gn b/chromium/chrome/browser/resources/pdf/BUILD.gn index c975fd597e2..02cef40417b 100644 --- a/chromium/chrome/browser/resources/pdf/BUILD.gn +++ b/chromium/chrome/browser/resources/pdf/BUILD.gn @@ -3,6 +3,22 @@ # found in the LICENSE file. import("//third_party/closure_compiler/compile_js.gni") +import("//tools/polymer/html_to_js.gni") + +group("web_components") { + public_deps = [ + ":web_components_local", + "elements:web_components", + ] +} + +html_to_js("web_components_local") { + js_files = [ + "pdf_viewer.js", + "pdf_viewer_pp.js", + "pdf_viewer_shared_style.js", + ] +} group("closure_compile") { deps = [ @@ -32,10 +48,12 @@ js_library("constants") { } js_library("gesture_detector") { + deps = [ "//ui/webui/resources/js/cr:event_target.m" ] } js_library("open_pdf_params_parser") { deps = [ ":constants" ] + externs_list = [ "$externs_path/pending.js" ] } js_library("pdf_scripting_api") { @@ -83,32 +101,61 @@ js_library("toolbar_manager") { ] } -js_library("controller") { +js_library("ink_controller") { deps = [ ":annotation_tool", + ":controller", + ":viewport", + "//ui/webui/resources/js/cr:event_target.m", + ] +} + +js_library("controller") { + deps = [ ":viewport", "elements:viewer-pdf-toolbar", "//ui/webui/resources/js:assert.m", "//ui/webui/resources/js:load_time_data.m", "//ui/webui/resources/js:promise_resolver.m", - "//ui/webui/resources/js:util.m", "//ui/webui/resources/js/cr:event_target.m", ] } +js_library("pdf_viewer_base") { + deps = [ + ":browser_api", + ":constants", + ":controller", + ":metrics", + ":pdf_scripting_api", + ":pdf_viewer_utils", + ":viewport", + ":viewport_scroller", + "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", + "//ui/webui/resources/js:assert.m", + "//ui/webui/resources/js:event_tracker.m", + "//ui/webui/resources/js:load_time_data.m", + "//ui/webui/resources/js:promise_resolver.m", + "//ui/webui/resources/js:util.m", + ] + externs_list = [ "$externs_path/resources_private.js" ] +} + js_library("pdf_viewer") { deps = [ ":bookmark_type", + ":browser_api", ":constants", ":controller", + ":ink_controller", ":metrics", ":navigator", ":pdf_scripting_api", + ":pdf_viewer_base", + ":pdf_viewer_utils", ":toolbar_manager", ":viewport", - ":viewport_scroller", "elements:viewer-error-screen", - "elements:viewer-page-indicator", "elements:viewer-password-screen", "elements:viewer-pdf-toolbar", "elements:viewer-zoom-toolbar", @@ -121,19 +168,39 @@ js_library("pdf_viewer") { externs_list = [ "$externs_path/resources_private.js" ] } -js_library("main_util") { +js_library("pdf_viewer_utils") { deps = [ - ":browser_api", - ":pdf_viewer", + ":controller", + ":viewport", ] } -js_library("main") { - deps = [ ":main_util" ] +js_library("pdf_viewer_pp") { + deps = [ + ":constants", + ":controller", + ":pdf_scripting_api", + ":pdf_viewer_base", + ":pdf_viewer_utils", + ":toolbar_manager", + ":viewport", + "elements:viewer-error-screen", + "elements:viewer-page-indicator", + "elements:viewer-zoom-toolbar", + "//ui/webui/resources/js:assert.m", + "//ui/webui/resources/js:event_tracker.m", + "//ui/webui/resources/js:load_time_data.m", + "//ui/webui/resources/js:promise_resolver.m", + "//ui/webui/resources/js:util.m", + ] + externs_list = [ "$externs_path/resources_private.js" ] } -js_library("main_pp") { - deps = [ ":main_util" ] +js_library("main") { + deps = [ + ":browser_api", + ":pdf_viewer", + ] } js_type_check("pdf_resources") { @@ -144,14 +211,16 @@ js_type_check("pdf_resources") { ":constants", ":controller", ":gesture_detector", + ":ink_controller", ":main", - ":main_pp", - ":main_util", ":metrics", ":navigator", ":open_pdf_params_parser", ":pdf_scripting_api", ":pdf_viewer", + ":pdf_viewer_base", + ":pdf_viewer_pp", + ":pdf_viewer_utils", ":toolbar_manager", ":viewport", ":viewport_scroller", diff --git a/chromium/chrome/browser/resources/pdf/annotation_tool.js b/chromium/chrome/browser/resources/pdf/annotation_tool.js index 5e887b048b7..e8fa6e74d6c 100644 --- a/chromium/chrome/browser/resources/pdf/annotation_tool.js +++ b/chromium/chrome/browser/resources/pdf/annotation_tool.js @@ -9,7 +9,7 @@ * @typedef {{ * tool: string, * size: number, - * color: (string|null), + * color: (string|undefined), * }} */ let AnnotationTool; diff --git a/chromium/chrome/browser/resources/pdf/browser_api.js b/chromium/chrome/browser/resources/pdf/browser_api.js index 53ceeef1da9..5e2fb44cdc3 100644 --- a/chromium/chrome/browser/resources/pdf/browser_api.js +++ b/chromium/chrome/browser/resources/pdf/browser_api.js @@ -28,7 +28,6 @@ function lookupDefaultZoom(streamInfo) { * Returns a promise that will resolve to the initial zoom factor * upon starting the plugin. This may differ from the default zoom * if, for example, the page is zoomed before the plugin is run. - * * @param {!Object} streamInfo The stream object pointing to the data contained * in the PDF. * @return {Promise<number>} A promise that will resolve to the initial zoom @@ -46,9 +45,7 @@ function lookupInitialZoom(streamInfo) { }); } -/** - * A class providing an interface to the browser. - */ +// A class providing an interface to the browser. export class BrowserApi { /** * @param {!Object} streamInfo The stream object which points to the data @@ -90,7 +87,6 @@ export class BrowserApi { /** * Sets the browser zoom. - * * @param {number} zoom The zoom factor to send to the browser. * @return {Promise} A promise that will be resolved when the browser zoom * has been updated. @@ -104,23 +100,17 @@ export class BrowserApi { }); } - /** - * @return {number} The default browser zoom factor. - */ + /** @return {number} The default browser zoom factor. */ getDefaultZoom() { return this.defaultZoom_; } - /** - * @return {number} The initial browser zoom factor. - */ + /** @return {number} The initial browser zoom factor. */ getInitialZoom() { return this.initialZoom_; } - /** - * @return {BrowserApi.ZoomBehavior} How to manage zoom. - */ + /** @return {BrowserApi.ZoomBehavior} How to manage zoom. */ getZoomBehavior() { return this.zoomBehavior_; } @@ -160,7 +150,6 @@ BrowserApi.ZoomBehavior = { /** * Creates a BrowserApi for an extension running as a mime handler. - * * @return {!Promise<!BrowserApi>} A promise to a BrowserApi instance * constructed using the mimeHandlerPrivate API. */ @@ -197,7 +186,6 @@ function createBrowserApiForMimeHandlerView() { /** * Creates a BrowserApi instance for an extension not running as a mime handler. - * * @return {!Promise<!BrowserApi>} A promise to a BrowserApi instance * constructed from the URL. */ diff --git a/chromium/chrome/browser/resources/pdf/constants.js b/chromium/chrome/browser/resources/pdf/constants.js index 70da3e827d7..475061b73ae 100644 --- a/chromium/chrome/browser/resources/pdf/constants.js +++ b/chromium/chrome/browser/resources/pdf/constants.js @@ -21,3 +21,14 @@ export const TwoUpViewAction = { TWO_UP_VIEW_ENABLE: 'two-up-view-enable', TWO_UP_VIEW_DISABLE: 'two-up-view-disable', }; + +/** + * Enumeration of save message request types. Must Match SaveRequestType in + * pdf/out_of_process_instance.h. + * @enum {number} + */ +export const SaveRequestType = { + ANNOTATION: 0, + ORIGINAL: 1, + EDITED: 2, +}; diff --git a/chromium/chrome/browser/resources/pdf/controller.js b/chromium/chrome/browser/resources/pdf/controller.js index 9eccd835645..95c02f1fa0d 100644 --- a/chromium/chrome/browser/resources/pdf/controller.js +++ b/chromium/chrome/browser/resources/pdf/controller.js @@ -6,8 +6,8 @@ import {assert} from 'chrome://resources/js/assert.m.js'; import {NativeEventTarget as EventTarget} from 'chrome://resources/js/cr/event_target.m.js'; import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js'; -import {$} from 'chrome://resources/js/util.m.js'; +import {SaveRequestType} from './constants.js'; import {PartialPoint, Point, Viewport} from './viewport.js'; /** @typedef {{ type: string }} */ @@ -45,25 +45,6 @@ let EmailMessageData; */ export let PrintPreviewParams; -// Note: Redefining this type here, to work around the fact that ink externs -// are only available on Chrome OS, so the targets that contain them cannot be -// built on other platforms. -// TODO (rbpotter): Break InkController into its own file that is only included -// on Chrome OS. - -/** - * @typedef {{ - * setAnnotationTool: function(AnnotationTool):void, - * viewportChanged: function():void, - * saveDocument: function():!Promise, - * undo: function():void, - * redo: function():void, - * load: function(string, !ArrayBuffer):!Promise, - * viewport: !Viewport, - * }} - */ -let ViewerInkHostElement; - /** * Creates a cryptographically secure pseudorandom 128-bit token. * @return {string} The generated token as a hex string. @@ -105,12 +86,13 @@ export class ContentController { /** * Requests that the current document be saved. - * @param {boolean} requireResult whether a response is required, otherwise - * the controller may save the document to disk internally. + * @param {!SaveRequestType} requestType The type of save request. If + * ANNOTATION, a response is required, otherwise the controller may save + * the document to disk internally. * @return {Promise<{fileName: string, dataToSave: ArrayBuffer}>} * @abstract */ - save(requireResult) {} + save(requestType) {} /** * Loads PDF document from `data` activates UI. @@ -128,110 +110,9 @@ export class ContentController { unload() {} } -/** - * Controller for annotation mode, on Chrome OS only. Fires the following events - * from its event target: - * has-unsaved-changes: Fired to indicate there are ink annotations that have - * not been saved. - * set-annotation-undo-state: Contains information about whether undo or redo - * options are available. - */ -export class InkController extends ContentController { - /** @param {!Viewport} viewport */ - constructor(viewport) { - super(); - - /** @private {!Viewport} */ - this.viewport_ = viewport; - - /** @private {?ViewerInkHostElement} */ - this.inkHost_ = null; - - /** @private {!EventTarget} */ - this.eventTarget_ = new EventTarget(); - - /** @type {?AnnotationTool} */ - this.tool_ = null; - } - - /** @return {!EventTarget} */ - getEventTarget() { - return this.eventTarget_; - } - - /** @param {AnnotationTool} tool */ - setAnnotationTool(tool) { - this.tool_ = tool; - if (this.inkHost_) { - this.inkHost_.setAnnotationTool(tool); - } - } - - /** @override */ - rotateClockwise() { - // TODO(dstockwell): implement rotation - } - - /** @override */ - rotateCounterclockwise() { - // TODO(dstockwell): implement rotation - } - - /** @override */ - setTwoUpView(enableTwoUpView) { - // TODO(dstockwell): Implement two up view. - } - - /** @override */ - viewportChanged() { - this.inkHost_.viewportChanged(); - } - - /** @override */ - save(requireResult) { - return this.inkHost_.saveDocument(); - } - - /** @override */ - undo() { - this.inkHost_.undo(); - } - - /** @override */ - redo() { - this.inkHost_.redo(); - } - - /** @override */ - load(filename, data) { - if (!this.inkHost_) { - const inkHost = document.createElement('viewer-ink-host'); - $('content').appendChild(inkHost); - this.inkHost_ = /** @type {!ViewerInkHostElement} */ (inkHost); - this.inkHost_.viewport = this.viewport_; - inkHost.addEventListener('stroke-added', e => { - this.eventTarget_.dispatchEvent(new CustomEvent('has-unsaved-changes')); - }); - inkHost.addEventListener('undo-state-changed', e => { - this.eventTarget_.dispatchEvent( - new CustomEvent('set-annotation-undo-state', {detail: e.detail})); - }); - } - return this.inkHost_.load(filename, data); - } - - /** @override */ - unload() { - this.inkHost_.remove(); - this.inkHost_ = null; - } -} - -/** - * PDF plugin controller, responsible for communicating with the embedded plugin - * element. Dispatches a 'plugin-message' event containing the message from the - * plugin, if a message type not handled by this controller is received. - */ +// PDF plugin controller, responsible for communicating with the embedded plugin +// element. Dispatches a 'plugin-message' event containing the message from the +// plugin, if a message type not handled by this controller is received. export class PluginController extends ContentController { /** * @param {!HTMLEmbedElement} plugin @@ -407,11 +288,15 @@ export class PluginController extends ContentController { } /** @override */ - save(requireResult) { + save(requestType) { const resolver = new PromiseResolver(); const newToken = createToken(); this.pendingTokens_.set(newToken, resolver); - this.postMessage_({type: 'save', token: newToken, force: requireResult}); + this.postMessage_({ + type: 'save', + token: newToken, + saveRequestType: requestType, + }); return resolver.promise; } @@ -420,6 +305,7 @@ export class PluginController extends ContentController { const url = URL.createObjectURL(new Blob([data])); this.plugin_.removeAttribute('headers'); this.plugin_.setAttribute('stream-url', url); + this.plugin_.setAttribute('has-edits', ''); this.plugin_.style.display = 'block'; try { await this.getLoadedCallback_(); @@ -476,7 +362,6 @@ export class PluginController extends ContentController { /** * Handles the pdf file buffer received from the plugin. - * * @param {!SaveDataMessageData} messageData data of the message event. * @private */ diff --git a/chromium/chrome/browser/resources/pdf/elements/BUILD.gn b/chromium/chrome/browser/resources/pdf/elements/BUILD.gn index 1980c0a3421..0eaa9db89f2 100644 --- a/chromium/chrome/browser/resources/pdf/elements/BUILD.gn +++ b/chromium/chrome/browser/resources/pdf/elements/BUILD.gn @@ -14,6 +14,7 @@ js_type_check("closure_compile") { ":viewer-page-selector", ":viewer-password-screen", ":viewer-pdf-toolbar", + ":viewer-pdf-toolbar-new", ":viewer-toolbar-dropdown", ":viewer-zoom-button", ":viewer-zoom-toolbar", @@ -67,7 +68,10 @@ js_library("viewer-page-selector") { } js_library("viewer-password-screen") { - deps = [ "//ui/webui/resources/cr_elements/cr_input:cr_input.m" ] + deps = [ + "//ui/webui/resources/cr_elements/cr_input:cr_input.m", + "//ui/webui/resources/js:load_time_data.m", + ] } js_library("viewer-pdf-toolbar") { @@ -76,10 +80,21 @@ js_library("viewer-pdf-toolbar") { ":viewer-page-selector", ":viewer-toolbar-dropdown", "..:annotation_tool", + "..:constants", + "//ui/webui/resources/cr_elements/cr_action_menu:cr_action_menu.m", + "//ui/webui/resources/js:assert.m", + "//ui/webui/resources/js:load_time_data.m", + "//ui/webui/resources/js:promise_resolver.m", ] externs_list = [ "$externs_path/pending.js" ] } +js_library("viewer-pdf-toolbar-new") { + deps = [ + "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled", + ] +} + js_library("viewer-pen-options") { } @@ -109,6 +124,7 @@ html_to_js("web_components") { "viewer-page-selector.js", "viewer-password-screen.js", "viewer-pdf-toolbar.js", + "viewer-pdf-toolbar-new.js", "viewer-toolbar-dropdown.js", "viewer-zoom-button.js", "viewer-zoom-toolbar.js", diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark.html b/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark.html index ec39a68550e..8cf60ea1aa7 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-bookmark.html @@ -47,13 +47,13 @@ <paper-ripple></paper-ripple> <cr-icon-button id="expand" iron-icon="cr:chevron-right" on-click="toggleChildren_"></cr-icon-button> - <span id="title" tabindex="0">{{bookmark.title}}</span> + <span id="title" tabindex="0">[[bookmark.title]]</span> </div> <!-- dom-if will stamp the complex bookmark tree lazily as individual nodes are opened. --> <template is="dom-if" if="[[childrenShown_]]"> <template is="dom-repeat" items="[[bookmark.children]]"> - <viewer-bookmark bookmark="{{item}}" depth="[[childDepth_]]"> + <viewer-bookmark bookmark="[[item]]" depth="[[childDepth_]]"> </viewer-bookmark> </template> </template> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.html b/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.html index 03047141d77..e2c210fbd08 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.html @@ -1,10 +1,10 @@ <style include="cr-hidden-style"></style> <cr-dialog id="dialog" no-cancel> - <div slot="title">[[strings.errorDialogTitle]]</div> - <div slot="body">[[strings.pageLoadFailed]]</div> + <div slot="title">$i18n{errorDialogTitle}</div> + <div slot="body">$i18n{pageLoadFailed}</div> <div slot="button-container" hidden$="[[!reloadFn]]"> <cr-button class="action-button" on-click="reload"> - [[strings.pageReload]] + $i18n{pageReload} </cr-button> </div> </cr-dialog> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.js b/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.js index d3383b06e25..d5702f659d4 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-error-screen.js @@ -15,8 +15,6 @@ Polymer({ properties: { reloadFn: Function, - - strings: Object, }, show() { diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.html b/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.html index 1906119a962..61293db59aa 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.html @@ -1,13 +1,13 @@ <style include="cr-hidden-style"></style> <cr-dialog id="dialog" no-cancel> - <div slot="title">[[strings.annotationFormWarningTitle]]</div> - <div slot="body">[[strings.annotationFormWarningDetail]]</div> + <div slot="title">$i18n{annotationFormWarningTitle}</div> + <div slot="body">$i18n{annotationFormWarningDetail}</div> <div slot="button-container"> <cr-button class="cancel-button" on-click="onCancel"> - [[strings.annotationFormWarningKeepEditing]] + $i18n{annotationFormWarningKeepEditing} </cr-button> <cr-button class="action-button" on-click="onAction"> - [[strings.annotationFormWarningDiscard]] + $i18n{annotationFormWarningDiscard} </cr-button> </div> </cr-dialog> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.js b/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.js index d0cbd6d1de8..900c8d45b1e 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-form-warning.js @@ -14,10 +14,6 @@ Polymer({ _template: html`{__html_template__}`, - properties: { - strings: Object, - }, - /** @private {PromiseResolver} */ resolver_: null, diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js index fa208811e90..6c8b184407f 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-ink-host.js @@ -3,8 +3,9 @@ // found in the LICENSE file. import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + import {PDFMetrics} from '../metrics.js'; -import {Viewport} from '../viewport.js'; +import {PAGE_SHADOW, Viewport} from '../viewport.js'; /** @enum {string} */ const State = { @@ -62,7 +63,6 @@ Polymer({ /** * Whether we should suppress pointer events due to a gesture, * eg. pinch-zoom. - * * @private {boolean} */ pointerGesture_: false, @@ -96,19 +96,10 @@ Polymer({ /** * Dispatches a pointer event to Ink. - * * @param {PointerEvent} e */ dispatchPointerEvent_(e) { - // TODO(dstockwell) come up with a solution to propagate e.timeStamp. - this.ink_.dispatchPointerEvent(e.type, { - pointerId: e.pointerId, - pointerType: e.pointerType, - clientX: e.clientX, - clientY: e.clientY, - pressure: e.pressure, - buttons: e.buttons, - }); + this.ink_.dispatchPointerEvent(e); }, /** @param {TouchEvent} e */ @@ -135,10 +126,10 @@ Polymer({ // A multi-touch gesture has started with the active pointer. Cancel // the active pointer and suppress further events until it is released. this.pointerGesture_ = true; - this.ink_.dispatchPointerEvent('pointercancel', { + this.ink_.dispatchPointerEvent(new PointerEvent('pointercancel', { pointerId: this.activePointer_.pointerId, pointerType: this.activePointer_.pointerType, - }); + })); } return; } @@ -241,7 +232,7 @@ Polymer({ // color. await new Promise(resolve => setTimeout(resolve)); this.ink_.setOutOfBoundsColor(BACKGROUND_COLOR); - const spacing = Viewport.PAGE_SHADOW.top + Viewport.PAGE_SHADOW.bottom; + const spacing = PAGE_SHADOW.top + PAGE_SHADOW.bottom; this.ink_.setPageSpacing(spacing); this.style.visibility = 'visible'; }, @@ -256,8 +247,8 @@ Polymer({ const zoom = viewport.getZoom(); const documentWidth = viewport.getDocumentDimensions().width * zoom; // Adjust for page shadows. - const y = pos.y - Viewport.PAGE_SHADOW.top * zoom; - let x = pos.x - Viewport.PAGE_SHADOW.left * zoom; + const y = pos.y - PAGE_SHADOW.top * zoom; + let x = pos.x - PAGE_SHADOW.left * zoom; // Center the document if the width is smaller than the viewport. if (documentWidth < size.width) { x += (documentWidth - size.width) / 2; diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-page-indicator.html b/chromium/chrome/browser/resources/pdf/elements/viewer-page-indicator.html index 966a10c3661..5504e761500 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-page-indicator.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-page-indicator.html @@ -34,5 +34,5 @@ width: 0; } </style> - <div id="text">{{label}}</div> + <div id="text">[[label]]</div> <div id="triangle-end"></div> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.html b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.html index 1402b838971..002a23ad3fc 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.html @@ -3,6 +3,8 @@ color: #fff; display: flex; font-size: 0.81rem; + + --page-selector-spacing: 4px; } :host ::selection { @@ -11,12 +13,10 @@ #pageselector::part(input), #pagelength { - box-sizing: content-box; - padding: 0 3px; /* --page-length-digits is set through JavaScript. 1px is added because * the unit 'ch' does not provide exact whole number pixels, and * therefore seems to have 1px-off bugginess. */ - width: calc(var(--page-length-digits) * 1ch + 1px); + width: calc(max(2, var(--page-length-digits)) * 1ch + 1px); } #pageselector { @@ -29,13 +29,18 @@ #pageselector::part(input) { background: rgba(0, 0, 0, 0.5); + box-sizing: content-box; caret-color: var(--cr-input-color); - text-align: end; + padding: 0 var(--page-selector-spacing); + } + + #divider { + margin: 0 var(--page-selector-spacing); } </style> <cr-input id="pageselector" value="[[pageNo]]" on-mouseup="select" on-value-changed="onInputValueChange_" on-change="pageNoCommitted" - aria-label$="{{strings.labelPageNumber}}"> + aria-label$="$i18n{labelPageNumber}"> </cr-input> - / - <span id="pagelength">{{docLength}}</span> + <span id="divider">/</span> + <span id="pagelength">[[docLength]]</span> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.js b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.js index 5eda31ee514..6b8b55ad3dc 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-page-selector.js @@ -26,8 +26,6 @@ Polymer({ type: Number, value: 1, }, - - strings: Object, }, /** @return {!CrInputElement} */ diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.html b/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.html index 43c0249bffa..b61cdb5434a 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.html @@ -4,20 +4,19 @@ } </style> <cr-dialog id="dialog" no-cancel> - <div slot="title">[[strings.passwordDialogTitle]]</div> + <div slot="title">$i18n{passwordDialogTitle}</div> <div slot="body"> - <div id="message">[[strings.passwordPrompt]]</div> + <div id="message">$i18n{passwordPrompt}</div> <cr-input id="password" type="password" - error-message="[[getErrorMessage_(strings.passwordInvalid, - invalid)]]" + error-message="[[getErrorMessage_(invalid)]]" invalid="[[invalid]]" autofocus> </cr-input> </div> <div slot="button-container"> <cr-button id="submit" class="action-button" on-click="submit"> - [[strings.passwordSubmit]] + $i18n{passwordSubmit} </cr-button> </div> </cr-dialog> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.js b/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.js index 79cd849c56b..7bc4507d700 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-password-screen.js @@ -8,6 +8,7 @@ import 'chrome://resources/cr_elements/cr_input/cr_input.m.js'; import 'chrome://resources/cr_elements/shared_style_css.m.js'; import 'chrome://resources/cr_elements/shared_vars_css.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; Polymer({ @@ -17,8 +18,6 @@ Polymer({ properties: { invalid: Boolean, - - strings: Object, }, get active() { @@ -57,10 +56,9 @@ Polymer({ * Returns |message| if input is invalid, otherwise empty string. * This avoids setting the error message (which announces to screen readers) * when there is no error. - * @param {string} message * @return {string} */ - getErrorMessage_(message) { - return this.invalid ? message : ''; + getErrorMessage_() { + return this.invalid ? loadTimeData.getString('passwordInvalid') : ''; } }); diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.html b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.html new file mode 100644 index 00000000000..075635defbb --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.html @@ -0,0 +1,10 @@ +<style> + :host { + --pdf-toolbar-background-color: rgb(50, 54, 57); + background-color: var(--pdf-toolbar-background-color); + box-shadow: var(--cr-elevation-2); + color: white; + height: 48px; + } +</style> +New PDF Viewer toolbar will appear here. diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.js b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.js new file mode 100644 index 00000000000..06e1d118df0 --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar-new.js @@ -0,0 +1,17 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +class ViewerPdfToolbarNewElement extends PolymerElement { + static get is() { + return 'viewer-pdf-toolbar-new'; + } + + static get template() { + return html`{__html_template__}`; + } +} +customElements.define( + ViewerPdfToolbarNewElement.is, ViewerPdfToolbarNewElement); diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.html b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.html index 6a8daca8ea7..51c7bc66cb5 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.html @@ -1,4 +1,9 @@ <style include="cr-hidden-style"> + :host { + --pdf-toolbar-background-color: rgb(50, 54, 57); + --pdf-toolbar-text-color: rgb(241, 241, 241); + } + :host ::selection { background: rgba(255, 255, 255, 0.3); } @@ -36,7 +41,7 @@ } cr-icon-button { - --cr-icon-button-fill-color: rgb(241, 241, 241); + --cr-icon-button-fill-color: var(--pdf-toolbar-text-color); margin: 6px; } @@ -54,8 +59,8 @@ } #toolbar { - @apply --shadow-elevation-2dp; - background-color: rgb(50, 54, 57); + background-color: var(--pdf-toolbar-background-color); + box-shadow: var(--cr-elevation-2); position: relative; } @@ -67,7 +72,7 @@ #toolbar, #annotations-bar { - color: rgb(241, 241, 241); + color: var(--pdf-toolbar-text-color); display: flex; height: 48px; padding: 0 16px; @@ -90,7 +95,7 @@ #pen, #highlighter { - --dropdown-open-background: rgb(50, 54, 57); + --dropdown-open-background: var(--pdf-viewer-background-color); } #eraser { @@ -138,10 +143,6 @@ #rotate-left { display: none; } - - #pageselector-container { - flex: 2; - } } @media(max-width: 450px) { @@ -156,40 +157,55 @@ display: none; } } + + cr-action-menu::part(dialog) { + position: fixed; + top: 48px; + } </style> <div id="toolbar"> <div id="aligner"> - <span id="title" title="{{docTitle}}"> - <span>{{docTitle}}</span> + <span id="title" title="[[docTitle]]"> + <span>[[docTitle]]</span> </span> <div id="pageselector-container"> <viewer-page-selector id="pageselector" class="invisible" - doc-length="{{docLength}}" page-no="{{pageNo}}" - strings="{{strings}}"> + doc-length="[[docLength]]" page-no="[[pageNo]]"> </viewer-page-selector> </div> <div id="buttons" class="invisible"> - <template is="dom-if" if="[[pdfAnnotationsEnabled]]"> +<if expr="chromeos"> + <template is="dom-if" if="[[pdfAnnotationsEnabled_]]"> <cr-icon-button id="annotate" iron-icon="pdf:create" disabled="[[!annotationAvailable]]" on-click="toggleAnnotation" - aria-label$="{{strings.tooltipAnnotate}}" - title$="{{strings.tooltipAnnotate}}"></cr-icon-button> + aria-label$="$i18n{tooltipAnnotate}" + title="$i18n{tooltipAnnotate}"></cr-icon-button> </template> +</if> <cr-icon-button id="rotate-right" iron-icon="pdf:rotate-right" disabled="[[annotationMode]]" on-click="rotateRight" - aria-label$="{{strings.tooltipRotateCW}}" - title$="{{strings.tooltipRotateCW}}"></cr-icon-button> + aria-label$="$i18n{tooltipRotateCW}" + title="$i18n{tooltipRotateCW}"></cr-icon-button> <cr-icon-button id="download" iron-icon="cr:file-download" - on-click="download" aria-label$="{{strings.tooltipDownload}}" - title$="{{strings.tooltipDownload}}"></cr-icon-button> + on-click="onDownloadClick_" aria-label$="$i18n{tooltipDownload}" + aria-haspopup$="[[downloadHasPopup_]]" + title="$i18n{tooltipDownload}"></cr-icon-button> + <cr-action-menu id="downloadMenu"> + <button class="dropdown-item" on-click="onDownloadEditedClick_"> + $i18n{downloadEdited} + </button> + <button class="dropdown-item" on-click="onDownloadOriginalClick_"> + $i18n{downloadOriginal} + </button> + </cr-action-menu> <cr-icon-button id="print" iron-icon="cr:print" on-click="print" - hidden="[[!printingEnabled]]" title$="{{strings.tooltipPrint}}" - aria-label$="{{strings.tooltipPrint}}"></cr-icon-button> + hidden="[[!printingEnabled_]]" title="$i18n{tooltipPrint}" + aria-label$="$i18n{tooltipPrint}"></cr-icon-button> <viewer-toolbar-dropdown id="bookmarks" selected @@ -197,7 +213,7 @@ hidden$="[[!bookmarks.length]]" open-icon="pdf:bookmark" closed-icon="pdf:bookmark-border" - header="{{strings.bookmarks}}"> + header="$i18n{bookmarks}"> <template is="dom-repeat" items="[[bookmarks]]"> <viewer-bookmark bookmark="[[item]]" depth="0"></viewer-bookmark> </template> @@ -211,6 +227,7 @@ </div> </div> +<if expr="chromeos"> <div id="annotations-bar" hidden> <viewer-toolbar-dropdown id="pen" selected$="[[isAnnotationTool_('pen', annotationTool.tool)]]" @@ -220,7 +237,7 @@ closed-icon="pdf:marker" dropdown-centered hide-header - header$="{{strings.annotationPen}}" + header="$i18n{annotationPen}" style="--pen-tip-fill: #000000"> <viewer-pen-options selected-color="#000000" @@ -239,7 +256,7 @@ closed-icon="pdf:highlighter" dropdown-centered hide-header - header$="{{strings.annotationHighlighter}}" + header="$i18n{annotationHighlighter}" style="--pen-tip-fill: #ffbc00"> <viewer-pen-options selected-color="#ffbc00" @@ -253,18 +270,19 @@ <cr-icon-button id="eraser" selected$="[[isAnnotationTool_('eraser', annotationTool.tool)]]" on-click="annotationToolClicked_" iron-icon="pdf:eraser" - aria-label$="{{strings.annotationEraser}}" - title$="{{strings.annotationEraser}}"></cr-icon-button> + aria-label$="$i18n{annotationEraser}" + title="$i18n{annotationEraser}"></cr-icon-button> <div id="annotation-separator"></div> <cr-icon-button id="undo" disabled="[[!canUndoAnnotation]]" iron-icon="pdf:undo" on-click="undo" - aria-label$="{{strings.annotationUndo}}" - title$="{{strings.annotationUndo}}"></cr-icon-button> + aria-label$="$i18n{annotationUndo}" + title="$i18n{annotationUndo}"></cr-icon-button> <cr-icon-button id="redo" disabled="[[!canRedoAnnotation]]" iron-icon="pdf:redo" on-click="redo" - aria-label$="{{strings.annotationRedo}}" - title$="{{strings.annotationRedo}}"></cr-icon-button> + aria-label$="$i18n{annotationRedo}" + title="$i18n{annotationRedo}"></cr-icon-button> </div> +</if> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.js b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.js index 9fb821a0ac4..defc20f57bb 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pdf-toolbar.js @@ -10,13 +10,19 @@ import './icons.js'; import './viewer-bookmark.js'; import './viewer-page-selector.js'; import './viewer-toolbar-dropdown.js'; - // <if expr="chromeos"> import './viewer-pen-options.js'; + // </if> +import {AnchorAlignment} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js'; +import {assert} from 'chrome://resources/js/assert.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; +import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js'; import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + import {Bookmark} from '../bookmark_type.js'; +import {SaveRequestType} from '../constants.js'; Polymer({ is: 'viewer-pdf-toolbar', @@ -29,10 +35,7 @@ Polymer({ * example the PDF is encrypted or password protected. Note, this is * true regardless of whether the feature flag is enabled. */ - annotationAvailable: { - type: Boolean, - value: true, - }, + annotationAvailable: Boolean, /** Whether the viewer is currently in annotation mode. */ annotationMode: { @@ -45,7 +48,6 @@ Polymer({ /** @type {?AnnotationTool} */ annotationTool: { type: Object, - value: null, notify: true, }, @@ -74,6 +76,15 @@ Polymer({ /** The title of the PDF document. */ docTitle: String, + hasEdits: Boolean, + + hasEnteredAnnotationMode: Boolean, + + isFormFieldFocused: { + type: Boolean, + observer: 'onFormFieldFocusedChanged_', + }, + /** The current loading progress of the PDF document (0 - 100). */ loadProgress: { type: Number, @@ -89,24 +100,52 @@ Polymer({ /** The number of the page being viewed (1-based). */ pageNo: Number, - /** Whether the PDF Annotations feature is enabled. */ - pdfAnnotationsEnabled: { + /** + * Whether the PDF Annotations feature is enabled. + * @private + */ + pdfAnnotationsEnabled_: { type: Boolean, value: false, }, - /** Whether the Printing feature is enabled. */ - printingEnabled: { + /** + * Whether the Printing feature is enabled. + * @private + */ + printingEnabled_: { type: Boolean, value: false, }, - strings: Object, + /** @private */ + downloadHasPopup_: { + type: String, + computed: 'computeDownloadHasPopup_(' + + 'pdfFormSaveEnabled_, hasEdits, hasEnteredAnnotationMode)', + }, + + strings: { + type: Object, + observer: 'onStringsSet_', + }, + + /** + * Whether the PDF Form save feature is enabled. + * @private + */ + pdfFormSaveEnabled_: { + type: Boolean, + value: false, + }, }, /** @type {?Object} */ animation_: null, + /** @private {?PromiseResolver<boolean>} */ + waitForFormFocusChange_: null, + /** * @param {number} newProgress * @param {number} oldProgress @@ -119,7 +158,9 @@ Polymer({ this.$.pageselector.classList.toggle('invisible', !loaded); this.$.buttons.classList.toggle('invisible', !loaded); this.$.progress.style.opacity = loaded ? 0 : 1; + // <if expr="chromeos"> this.$['annotations-bar'].hidden = !loaded || !this.annotationMode; + // </if> } }, @@ -168,7 +209,8 @@ Polymer({ /** @return {boolean} Whether the toolbar should be kept open. */ shouldKeepOpen() { return this.$.bookmarks.dropdownOpen || this.loadProgress < 100 || - this.$.pageselector.isActive() || this.annotationMode; + this.$.pageselector.isActive() || this.annotationMode || + this.$.downloadMenu.open; }, /** @return {boolean} Whether a dropdown was open and was hidden. */ @@ -178,6 +220,7 @@ Polymer({ this.$.bookmarks.toggleDropdown(); result = true; } + // <if expr="chromeos"> if (this.$.pen.dropdownOpen) { this.$.pen.toggleDropdown(); result = true; @@ -186,6 +229,7 @@ Polymer({ this.$.highlighter.toggleDropdown(); result = true; } + // </if> return result; }, @@ -198,8 +242,67 @@ Polymer({ this.fire('rotate-right'); }, - download() { - this.fire('save'); + /** @private */ + showDownloadMenu_() { + this.$.downloadMenu.showAt(this.$.download, { + anchorAlignmentX: AnchorAlignment.CENTER, + anchorAlignmentY: AnchorAlignment.AFTER_END, + noOffset: true, + }); + // For tests + this.fire('download-menu-shown-for-testing'); + }, + + /** @private */ + onDownloadClick_() { + this.waitForEdits_().then(hasEdits => { + if (hasEdits) { + this.showDownloadMenu_(); + } else { + this.fire('save', SaveRequestType.ORIGINAL); + } + }); + }, + + /** + * @return {!Promise<boolean>} Promise that resolves with true if the PDF has + * edits and/or annotations, and false otherwise. + * @private + */ + waitForEdits_() { + if (this.hasEditsToSave_()) { + return Promise.resolve(true); + } + if (!this.isFormFieldFocused || !this.pdfFormSaveEnabled_) { + return Promise.resolve(false); + } + this.waitForFormFocusChange_ = new PromiseResolver(); + return this.waitForFormFocusChange_.promise; + }, + + /** @private */ + onFormFieldFocusedChanged_() { + if (!this.waitForFormFocusChange_) { + return; + } + + this.waitForFormFocusChange_.resolve(this.hasEdits); + this.waitForFormFocusChange_ = null; + }, + + /** @private */ + onDownloadOriginalClick_() { + this.fire('save', SaveRequestType.ORIGINAL); + this.$.downloadMenu.close(); + }, + + /** @private */ + onDownloadEditedClick_() { + this.fire( + 'save', + this.hasEnteredAnnotationMode ? SaveRequestType.ANNOTATION : + SaveRequestType.EDITED); + this.$.downloadMenu.close(); }, print() { @@ -255,7 +358,7 @@ Polymer({ const tool = element.id; const options = element.querySelector('viewer-pen-options') || { selectedSize: 1, - selectedColor: null, + selectedColor: undefined, }; const attributeStyleMap = element.attributeStyleMap; attributeStyleMap.set('--pen-tip-fill', options.selectedColor); @@ -278,4 +381,32 @@ Polymer({ isAnnotationTool_(toolName) { return !!this.annotationTool && this.annotationTool.tool === toolName; }, + + /** + * @return {boolean} + * @private + */ + hasEditsToSave_() { + return this.hasEnteredAnnotationMode || + (this.pdfFormSaveEnabled_ && this.hasEdits); + }, + + /** + * @return {string} The value for the aria-haspopup attribute for the download + * button. + * @private + */ + computeDownloadHasPopup_() { + return this.hasEditsToSave_() ? 'menu' : 'false'; + }, + + /** @private */ + onStringsSet_() { + assert(this.strings); + + this.pdfAnnotationsEnabled_ = + loadTimeData.getBoolean('pdfAnnotationsEnabled'); + this.pdfFormSaveEnabled_ = loadTimeData.getBoolean('pdfFormSaveEnabled'); + this.printingEnabled_ = loadTimeData.getBoolean('printingEnabled'); + }, }); diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.html b/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.html index b44bf89f9c9..e98c1c20513 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.html @@ -65,8 +65,8 @@ on-pointerdown="blurOnPointerDown"> </template> <cr-icon-button id="expand" iron-icon="cr:expand-more" tabindex="3" - on-click="toggleExpanded_" aria-label$="[[strings.annotationExpand]]" - title$="[[strings.annotationExpand]]"></cr-icon-button> + on-click="toggleExpanded_" aria-label$="$i18n{annotationExpand}" + title$="$i18n{annotationExpand}"></cr-icon-button> </div> <div id="separator"></div> <div id="sizes" on-change="sizeChanged_"> @@ -74,7 +74,7 @@ <input type="radio" name="size" value="[[item.size]]" checked$="[[equal_(selectedSize, item.size)]]" tabindex="2" style="--item-size: [[item.size]]" - title$="{{lookup_(strings, item.name)}}" + title="[[lookup_(strings, item.name)]]" aria-label$="[[lookup_(strings, item.name)]]" on-pointerdown="blurOnPointerDown"> </template> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.js b/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.js index 0cc12c94384..fd7e6e8661d 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-pen-options.js @@ -52,50 +52,62 @@ const sizes = [ {name: 'annotationSize20', size: 1}, ]; -/** - * Displays a set of radio buttons to select from - * a predefined list of colors and sizes. - */ +// Displays a set of radio buttons to select from a predefined list of colors +// and sizes. Polymer({ is: 'viewer-pen-options', _template: html`{__html_template__}`, properties: { + /** @private */ expanded_: { type: Boolean, value: false, }, + selectedSize: { type: Number, value: 0.250, notify: true, }, + selectedColor: { type: String, value: '#000000', notify: true, }, + + /** @private */ sizes_: { type: Array, value: sizes, }, + + /** @private */ colors_: { type: Array, value: colors, }, + strings: Object, }, - /** @type {Array<!Animation>} */ + /** @private {Array<!Animation>} */ expandAnimations_: null, - /** @param {Event} e */ + /** + * @param {!Event} e + * @private + */ sizeChanged_(e) { this.selectedSize = Number(e.target.value); }, - /** @param {Event} e */ + /** + * @param {!Event} e + * @private + */ colorChanged_(e) { this.selectedColor = e.target.value; }, @@ -125,6 +137,7 @@ Polymer({ /** * Updates the state of the UI to reflect the current value of `expanded`. * Starts or reverses animations and enables/disable controls. + * @private */ updateExpandedState_() { const colors = this.$.colors; @@ -176,19 +189,16 @@ Polymer({ animation.play(); } for (const input of colors.querySelectorAll('input:nth-child(n+8)')) { - if (this.expanded_) { - input.removeAttribute('disabled'); - } else { - input.setAttribute('disabled', ''); - } + input.toggleAttribute('disabled', !this.expanded_); } }, /** * Used to determine equality in computed bindings. - * * @param {*} a * @param {*} b + * @return {boolean} Whether a === b + * @private */ equal_(a, b) { return a === b; @@ -196,10 +206,10 @@ Polymer({ /** * Used to lookup a string in a computed binding. - * * @param {Object} strings * @param {string} name * @return {string} + * @private */ lookup_(strings, name) { return strings ? strings[name] : ''; @@ -209,6 +219,7 @@ Polymer({ * Used to remove focus when clicking or tapping on a styled input * element. This is a workaround until we can use the :focus-visible * pseudo selector. + * @param {!Event} e */ blurOnPointerDown(e) { const target = e.target; diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown.html b/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown.html index 8c6e7640bde..ff49695f629 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-toolbar-dropdown.html @@ -27,23 +27,17 @@ #dropdown { background-color: var(--cr-menu-background-color); border-radius: 4px; + box-shadow: var(--cr-menu-shadow); color: var(--cr-primary-text-color); overflow-y: hidden; padding-bottom: 2px; width: var(--dropdown-width); } - @media (prefers-color-scheme: light) { - #dropdown { - @apply --shadow-elevation-2dp; - } - } - @media (prefers-color-scheme: dark) { #dropdown { background-image: linear-gradient(var(--cr-menu-background-sheen), var(--cr-menu-background-sheen)); - box-shadow: var(--cr-menu-shadow); } } @@ -89,7 +83,7 @@ <div id="container"> <div id="dropdown" style="display: none"> <template is="dom-if" if="[[!hideHeader]]"> - <h1>{{header}}</h1> + <h1>[[header]]</h1> </template> <div id="scroll-container"> <slot></slot> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-button.html b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-button.html index f51075c5cab..ad97fce8a23 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-button.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-button.html @@ -35,11 +35,10 @@ --cr-icon-button-fill-color: var(--paper-grey-700); --cr-icon-button-icon-size: 20px; --cr-icon-button-size: 36px; - --cr-icon-button-fill-color-focus: rgb(242, 242, 242); background-color: rgb(242, 242, 242); border-radius: 50%; + box-shadow: var(--cr-elevation-1); overflow: visible; - @apply --shadow-elevation-2dp; } cr-icon-button([disabled]) { @@ -48,7 +47,6 @@ :host-context([is-print-preview]) cr-icon-button { --cr-icon-button-fill-color: white; - --cr-icon-button-fill-color-focus: var(--google-grey-600); --cr-icon-button-size: 32px; background-color: var(--google-grey-600); } @@ -62,17 +60,16 @@ @media (prefers-color-scheme: dark) { :host-context([is-print-preview]) cr-icon-button { --cr-icon-button-fill-color: var(--google-grey-200); - --cr-icon-button-fill-color-focus: var(--google-grey-900); background-color: var(--google-grey-900); } } :host([keyboard-navigation-active]) cr-icon-button:focus { - @apply --shadow-elevation-6dp; + box-shadow: var(--cr-elevation-4); } cr-icon-button:active { - @apply --shadow-elevation-8dp; + box-shadow: var(--cr-elevation-5); } </style> <div id="wrapper"> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.html b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.html index ab6a4d461ca..341f6bbf4c7 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.html +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.html @@ -67,7 +67,7 @@ <!-- TODO(crbug.com/51472): Change icons for two-up-view-button --> <!-- once they are finalized. --> <viewer-zoom-button id="two-up-view-button" delay="100" - disabled="[[annotationMode]]" hidden$="[[!twoUpViewEnabled]]" + disabled="[[annotationMode]]" hidden$="[[!twoUpViewEnabled_]]" on-fabclick="twoUpViewToggle_" keyboard-navigation-active="[[keyboardNavigationActive_]]" icons="pdf:create pdf:eraser"></viewer-zoom-button> diff --git a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.js b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.js index d58712f7562..f3bab8c69e1 100644 --- a/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.js +++ b/chromium/chrome/browser/resources/pdf/elements/viewer-zoom-toolbar.js @@ -8,7 +8,9 @@ import './icons.js'; import './viewer-zoom-button.js'; import {assert} from 'chrome://resources/js/assert.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + import {FittingType, TwoUpViewAction} from '../constants.js'; /** @@ -32,19 +34,25 @@ Polymer({ properties: { /** Whether the viewer is currently in annotation mode. */ - annotationMode: { + annotationMode: Boolean, + + isPrintPreview: { type: Boolean, value: false, }, - isPrintPreview: Boolean, + strings: { + type: Object, + observer: 'onStringsSet_', + }, - twoUpViewEnabled: Boolean, + /** @private */ + twoUpViewEnabled_: Boolean, /** @private */ fitButtonDelay_: { type: Number, - computed: 'computeFitButtonDelay_(twoUpViewEnabled)', + computed: 'computeFitButtonDelay_(twoUpViewEnabled_)', }, /** @private */ @@ -55,12 +63,11 @@ Polymer({ }, /** - * @param {boolean} twoUpViewEnabled Whether two-up view button is enabled. - * @return {number} Delay for :qthe fit button. + * @return {number} Delay for the fit button. * @private */ - computeFitButtonDelay_(twoUpViewEnabled) { - return twoUpViewEnabled ? 150 : 100; + computeFitButtonDelay_() { + return this.twoUpViewEnabled_ ? 150 : 100; }, listeners: { @@ -106,20 +113,27 @@ Polymer({ /** * Change button tooltips to match any changes to localized strings. - * @param {!{tooltipFitToPage: string, - * tooltipFitToWidth: string, - * tooltipTwoUpViewEnable: string, - * tooltipTwoUpViewDisable: string, - * tooltipZoomIn: string, - * tooltipZoomOut: string}} strings + * @private */ - setStrings(strings) { + onStringsSet_() { + const strings = + /** + * @type {{tooltipFitToPage: string, + * tooltipFitToWidth: string, + * tooltipTwoUpViewEnable: string, + * tooltipTwoUpViewDisable: string, + * tooltipZoomIn: string, + * tooltipZoomOut: string}} + */ + (this.strings); this.$['fit-button'].tooltips = [strings.tooltipFitToPage, strings.tooltipFitToWidth]; this.$['two-up-view-button'].tooltips = [strings.tooltipTwoUpViewEnable, strings.tooltipTwoUpViewDisable]; this.$['zoom-in-button'].tooltips = [strings.tooltipZoomIn]; this.$['zoom-out-button'].tooltips = [strings.tooltipZoomOut]; + this.twoUpViewEnabled_ = + loadTimeData.getBoolean('pdfTwoUpViewEnabled') && !this.isPrintPreview; }, /** Handle clicks of the fit-button. */ @@ -175,7 +189,7 @@ Polymer({ * @private */ twoUpViewToggle_: function() { - assert(this.twoUpViewEnabled); + assert(this.twoUpViewEnabled_); const twoUpViewAction = this.$['two-up-view-button'].activeIndex === TWO_UP_VIEW_DISABLED_STATE ? TwoUpViewAction.TWO_UP_VIEW_ENABLE : @@ -184,16 +198,12 @@ Polymer({ this.fire('two-up-view-changed', twoUpViewAction); }, - /** - * Handle clicks of the zoom-in-button. - */ + /** Handle clicks of the zoom-in-button. */ zoomIn() { this.fire('zoom-in'); }, - /** - * Handle clicks of the zoom-out-button. - */ + /** Handle clicks of the zoom-out-button. */ zoomOut() { this.fire('zoom-out'); }, diff --git a/chromium/chrome/browser/resources/pdf/gesture_detector.js b/chromium/chrome/browser/resources/pdf/gesture_detector.js index e8a0db4e6a3..6755d76b9b0 100644 --- a/chromium/chrome/browser/resources/pdf/gesture_detector.js +++ b/chromium/chrome/browser/resources/pdf/gesture_detector.js @@ -2,34 +2,43 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import {NativeEventTarget as EventTarget} from 'chrome://resources/js/cr/event_target.m.js'; + /** - * A class that listens for touch events and produces events when these - * touches form gestures (e.g. pinching). + * @typedef {{ + * center: !{x: number, y: number}, + * direction: (string|undefined), + * scaleRatio: (?number|undefined), + * startScaleRatio: (?number|undefined), + * }} */ +export let PinchEventDetail; + +// A class that listens for touch events and produces events when these +// touches form gestures (e.g. pinching). export class GestureDetector { /** - * @param {!Element} element The element to monitor for touch gestures. + * @param {!EventTarget|!Element} element The element to monitor for touch + * gestures. */ constructor(element) { - /** @private {!Element} */ - this.element_ = element; - - this.element_.addEventListener( + element.addEventListener( 'touchstart', /** @type {function(!Event)} */ (this.onTouchStart_.bind(this)), {passive: true}); const boundOnTouch = /** @type {function(!Event)} */ (this.onTouch_.bind(this)); - this.element_.addEventListener('touchmove', boundOnTouch, {passive: true}); - this.element_.addEventListener('touchend', boundOnTouch, {passive: true}); - this.element_.addEventListener( - 'touchcancel', boundOnTouch, {passive: true}); + element.addEventListener('touchmove', boundOnTouch, {passive: true}); + element.addEventListener('touchend', boundOnTouch, {passive: true}); + element.addEventListener('touchcancel', boundOnTouch, {passive: true}); - this.element_.addEventListener( + element.addEventListener( 'wheel', /** @type {function(!Event)} */ (this.onWheel_.bind(this)), {passive: false}); + document.addEventListener( + 'contextmenu', e => this.handleContextMenuEvent_(e)); this.pinchStartEvent_ = null; this.lastTouchTouchesCount_ = 0; @@ -52,24 +61,17 @@ export class GestureDetector { */ this.wheelEndTimeout_ = null; - /** @private {!Map<string, !Array<!Function>>} */ - this.listeners_ = - new Map([['pinchstart', []], ['pinchupdate', []], ['pinchend', []]]); + /** @private {!EventTarget} */ + this.eventTarget_ = new EventTarget(); } - /** - * Add a |listener| to be notified of |type| events. - * - * @param {string} type The event type to be notified for. - * @param {!Function} listener The callback. - */ - addEventListener(type, listener) { - if (this.listeners_.has(type)) { - this.listeners_.get(type).push(listener); - } + /** @return {!EventTarget} */ + getEventTarget() { + return this.eventTarget_; } /** + * Public for tests. * @return {boolean} True if the last touch start was a two finger touch. */ wasTwoFingerTouch() { @@ -77,22 +79,17 @@ export class GestureDetector { } /** - * Call the relevant listeners with the given |pinchEvent|. - * - * @param {!Object} pinchEvent The event to notify the listeners of. + * Call the relevant listeners with the given |PinchEventDetail|. + * @param {string} type The type of pinch event. + * @param {!PinchEventDetail} detail The event to notify the listeners of. * @private */ - notify_(pinchEvent) { - const listeners = this.listeners_.get(pinchEvent.type); - - for (const l of listeners) { - l(pinchEvent); - } + notify_(type, detail) { + this.eventTarget_.dispatchEvent(new CustomEvent(type, {detail})); } /** * The callback for touchstart events on the element. - * * @param {!TouchEvent} event Touch event on the element. * @private */ @@ -104,12 +101,11 @@ export class GestureDetector { this.pinchStartEvent_ = event; this.lastEvent_ = event; - this.notify_({type: 'pinchstart', center: GestureDetector.center_(event)}); + this.notify_('pinchstart', {center: GestureDetector.center_(event)}); } /** * The callback for touch move, end, and cancel events on the element. - * * @param {!TouchEvent} event Touch event on the element. * @private */ @@ -126,14 +122,12 @@ export class GestureDetector { const startScaleRatio = GestureDetector.pinchScaleRatio_(lastEvent, this.pinchStartEvent_); const center = GestureDetector.center_(lastEvent); - const endEvent = { - type: 'pinchend', - startScaleRatio: startScaleRatio, - center: center - }; this.pinchStartEvent_ = null; this.lastEvent_ = null; - this.notify_(endEvent); + this.notify_('pinchend', { + startScaleRatio: startScaleRatio, + center: center, + }); return; } @@ -141,12 +135,11 @@ export class GestureDetector { const startScaleRatio = GestureDetector.pinchScaleRatio_(event, this.pinchStartEvent_); const center = GestureDetector.center_(event); - this.notify_({ - type: 'pinchupdate', + this.notify_('pinchupdate', { scaleRatio: scaleRatio, direction: scaleRatio > 1.0 ? 'in' : 'out', startScaleRatio: startScaleRatio, - center: center + center: center, }); this.lastEvent_ = event; @@ -154,7 +147,6 @@ export class GestureDetector { /** * The callback for wheel events on the element. - * * @param {!WheelEvent} event Wheel event on the element. * @private */ @@ -179,16 +171,15 @@ export class GestureDetector { if (this.accumulatedWheelScale_ == null) { this.accumulatedWheelScale_ = 1.0; - this.notify_({type: 'pinchstart', center: position}); + this.notify_('pinchstart', {center: position}); } this.accumulatedWheelScale_ *= scale; - this.notify_({ - type: 'pinchupdate', + this.notify_('pinchupdate', { scaleRatio: scale, direction: scale > 1.0 ? 'in' : 'out', startScaleRatio: this.accumulatedWheelScale_, - center: position + center: position, }); // We don't get any phase information for the ctrl-wheels, so we don't know @@ -200,21 +191,35 @@ export class GestureDetector { } const gestureEndDelayMs = 100; const endEvent = { - type: 'pinchend', startScaleRatio: this.accumulatedWheelScale_, - center: position + center: position, }; - this.wheelEndTimeout_ = window.setTimeout(function(endEvent) { - this.notify_(endEvent); + this.wheelEndTimeout_ = window.setTimeout(() => { + this.notify_('pinchend', endEvent); this.wheelEndTimeout_ = null; this.accumulatedWheelScale_ = null; - }.bind(this), gestureEndDelayMs, endEvent); + }, gestureEndDelayMs); + } + + /** + * @param {!Event} e The context menu event + * @private + */ + handleContextMenuEvent_(e) { + // Stop Chrome from popping up the context menu on long press. We need to + // make sure the start event did not have 2 touches because we don't want + // to block two finger tap opening the context menu. We check for + // firesTouchEvents in order to not block the context menu on right click. + const capabilities = + /** @type {{ sourceCapabilities: Object }} */ (e).sourceCapabilities; + if (capabilities.firesTouchEvents && !this.wasTwoFingerTouch()) { + e.preventDefault(); + } } /** * Computes the change in scale between this touch event * and a previous one. - * * @param {!TouchEvent} event Latest touch event on the element. * @param {!TouchEvent} prevEvent A previous touch event on the element. * @return {?number} The ratio of the scale of this event and the @@ -229,7 +234,6 @@ export class GestureDetector { /** * Computes the distance between fingers. - * * @param {!TouchEvent} event Touch event with at least 2 touch points. * @return {number} Distance between touch[0] and touch[1]. * @private @@ -244,9 +248,8 @@ export class GestureDetector { /** * Computes the midpoint between fingers. - * * @param {!TouchEvent} event Touch event with at least 2 touch points. - * @return {!Object} Midpoint between touch[0] and touch[1]. + * @return {!{x: number, y: number}} Midpoint between touch[0] and touch[1]. * @private */ static center_(event) { diff --git a/chromium/chrome/browser/resources/pdf/index.css b/chromium/chrome/browser/resources/pdf/index.css index 1f6605930ec..20fd3b62cda 100644 --- a/chromium/chrome/browser/resources/pdf/index.css +++ b/chromium/chrome/browser/resources/pdf/index.css @@ -13,53 +13,3 @@ body { line-height: 154%; margin: 0; } - -viewer-page-indicator { - opacity: 0; - visibility: hidden; - z-index: 2; -} - -viewer-pdf-toolbar { - position: fixed; - width: 100%; - z-index: 4; -} - -#content { - height: 100%; - position: fixed; - width: 100%; - z-index: 1; -} - - -viewer-ink-host, -#plugin { - height: 100%; - position: absolute; - width: 100%; -} - -#sizer { - position: absolute; - z-index: 0; -} - -@media(max-height: 250px) { - viewer-pdf-toolbar { - display: none; - } -} - -@media(max-height: 200px) { - viewer-zoom-toolbar { - display: none; - } -} - -@media(max-width: 300px) { - viewer-zoom-toolbar { - display: none; - } -} diff --git a/chromium/chrome/browser/resources/pdf/index.html b/chromium/chrome/browser/resources/pdf/index.html index 6a78136126d..bce88410976 100644 --- a/chromium/chrome/browser/resources/pdf/index.html +++ b/chromium/chrome/browser/resources/pdf/index.html @@ -1,5 +1,6 @@ <!doctype html> -<html $i18n{a11yenhanced}> +<html dir="$i18n{textdirection}" lang="$i18n{language}" + $i18n{pdfViewerUpdateEnabledAttribute}> <head> <meta charset="utf-8"> @@ -7,22 +8,8 @@ <link rel="stylesheet" href="index.css"> </head> <body> - -<viewer-pdf-toolbar id="toolbar" hidden></viewer-pdf-toolbar> - -<div id="sizer"></div> -<viewer-password-screen id="password-screen"></viewer-password-screen> - -<viewer-zoom-toolbar id="zoom-toolbar" hidden></viewer-zoom-toolbar> - -<viewer-error-screen id="error-screen"></viewer-error-screen> - -<if expr="chromeos"> -<viewer-form-warning id="form-warning"></viewer-form-warning> -</if> - -<div id="content"></div> - +<pdf-viewer id="viewer"></pdf-viewer> </body> <script type="module" src="main.js"></script> +<script type="module" src="pdf_viewer.js"></script> </html> diff --git a/chromium/chrome/browser/resources/pdf/index_pp.html b/chromium/chrome/browser/resources/pdf/index_pp.html index 5c44ada5d73..fd18cd50280 100644 --- a/chromium/chrome/browser/resources/pdf/index_pp.html +++ b/chromium/chrome/browser/resources/pdf/index_pp.html @@ -1,5 +1,5 @@ <!doctype html> -<html $i18n{a11yenhanced}> +<html is-print-preview dir="$i18n{textdirection}" lang="$i18n{language}"> <head> <meta charset="utf-8"> @@ -7,16 +7,11 @@ <link rel="stylesheet" href="index.css"> </head> <body> - -<div id="sizer"></div> -<viewer-zoom-toolbar id="zoom-toolbar" hidden></viewer-zoom-toolbar> - -<viewer-page-indicator id="page-indicator"></viewer-page-indicator> - -<viewer-error-screen id="error-screen"></viewer-error-screen> - -<div id="content"></div> - +<pdf-viewer-pp id="viewer"></pdf-viewer-pp> </body> -<script type="module" src="main_pp.js"></script> +<script type="module" src="main.js"></script> +<!-- The line below refers to pdf_viewer_pp.js, which is served under the path + pdf_viewer.js so that the PDF viewer and Print Preview viewer can use the + same main.js source. main.js imports pdf_viewer.js. --> +<script type="module" src="pdf_viewer.js"></script> </html> diff --git a/chromium/chrome/browser/resources/pdf/ink/externs.js b/chromium/chrome/browser/resources/pdf/ink/externs.js deleted file mode 100644 index 8af66dfc91f..00000000000 --- a/chromium/chrome/browser/resources/pdf/ink/externs.js +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/** - * @fileoverview We don't rely on any of these types but they are missing - * from the Ink extern file currently and need to be defined somewhere. - * @externs - */ - -var ink = {}; // eslint-disable-line no-var -ink.proto.scene_change = {}; - -/** @constructor */ -ink.proto.Snapshot = function() {}; - -/** @constructor */ -ink.proto.Command = function() {}; - -/** @constructor */ -ink.proto.MutationPacket = function() {}; - -/** @constructor */ -ink.proto.SetCallbackFlags = function() {}; - -/** @constructor */ -ink.proto.ToolEvent = function() {}; - -/** @constructor */ -ink.Box = function() {}; - -/** @constructor */ -ink.Model = function() {}; - -/** @constructor */ -ink.ElementListener = function() {}; - -/** @constructor */ -ink.proto.scene_change.SceneChangeEvent = function() {}; - -const goog = {}; -goog.events = {}; -goog.disposable = {}; -goog.math = {}; -goog.ui = {}; -goog.html = {}; -goog.proto2 = {}; - -/** @constructor */ -goog.events.EventTarget = function() {}; - -/** @interface */ -goog.disposable.IDisposable = function() {}; - -/** @interface */ -goog.events.Listenable = function() {}; - -/** @constructor */ -goog.math.Size = function() {}; - -/** @constructor */ -goog.events.Event = function() {}; - -/** @constructor */ -goog.proto2.Message = function() {}; - -/** - * @extends {goog.events.EventTarget} - * @constructor - */ -goog.ui.Component = function() {}; - -/** @constructor */ -goog.html.SafeUrl = function() {}; diff --git a/chromium/chrome/browser/resources/pdf/ink/index.html b/chromium/chrome/browser/resources/pdf/ink/index.html index 61d5e7e077b..07f64d813e6 100644 --- a/chromium/chrome/browser/resources/pdf/ink/index.html +++ b/chromium/chrome/browser/resources/pdf/ink/index.html @@ -1,5 +1,5 @@ <!DOCTYPE html> -<script src="glcore_wasm_bootstrap_compiled.js"></script> +<script src="ink_loader.js"></script> <script src="ink_lib_binary.js"></script> <script src="ink_api.js"></script> <style> diff --git a/chromium/chrome/browser/resources/pdf/ink/ink_api.js b/chromium/chrome/browser/resources/pdf/ink/ink_api.js index c394b630077..5d4a4d72a83 100644 --- a/chromium/chrome/browser/resources/pdf/ink/ink_api.js +++ b/chromium/chrome/browser/resources/pdf/ink/ink_api.js @@ -3,36 +3,19 @@ // found in the LICENSE file. /** - * @typedef {{ - * canUndo: boolean, - * canRedo: boolean, - * }} - */ -let UndoState; - -/** * Wraps the Ink component with an API that can be called * across an IFrame boundary. */ class InkAPI { - /** @param {!ink.embed.EmbedComponent} embed */ - constructor(embed) { - this.embed_ = embed; - this.brush_ = ink.BrushModel.getInstance(embed); + /** @param {!drawings.Canvas} canvas */ + constructor(canvas) { + this.canvas_ = canvas; this.camera_ = null; } - /** @param {function(!UndoState)} listener */ + /** @param {function(!drawings.UndoState)} listener */ addUndoStateListener(listener) { - /** @param {!ink.UndoStateChangeEvent} e */ - function wrapper(e) { - listener({ - canUndo: e.getCanUndo(), - canRedo: e.getCanRedo(), - }); - } - - this.embed_.addEventListener(ink.UndoStateChangeEvent.EVENT_TYPE, wrapper); + this.canvas_.addUndoRedoListener(listener); } /** @@ -42,109 +25,70 @@ class InkAPI { // We change the type from ArrayBuffer to Uint8Array due to the consequences // of the buffer being passed across the iframe boundary. This realm has a // different ArrayBuffer constructor than `buffer`. - // TODO(dstockwell): Update Ink to allow Uint8Array here. - this.embed_.setPDF( - /** @type {!ArrayBuffer} */ ( - /** @type {!*} */ (new Uint8Array(buffer)))); + this.canvas_.setPDF(new Uint8Array(buffer)); } /** - * @return {!Promise<Uint8Array>} + * @return {!Uint8Array} */ getPDF() { - return this.embed_.getPDF(); + return this.canvas_.getPDF(); } /** * @return {!Uint8Array} */ getPDFDestructive() { - return this.embed_.getPDFDestructive(); + return this.canvas_.getPDFDestructive(); } async setCamera(camera) { this.camera_ = camera; - this.embed_.setCamera(camera); + this.canvas_.setCamera(camera); // Wait for the next task to avoid a race where Ink drops the camera value // when the canvas is rotated in low-latency mode. - setTimeout(() => this.embed_.setCamera(this.camera_), 0); + setTimeout(() => this.canvas_.setCamera(this.camera_), 0); } /** @param {AnnotationTool} tool */ setAnnotationTool(tool) { - const shape = { - eraser: 'MAGIC_ERASE', - pen: 'INKPEN', - highlighter: 'SMART_HIGHLIGHTER_TOOL', - }[tool.tool]; - this.brush_.setShape(shape); - if (tool.tool !== 'eraser') { - this.brush_.setColor(/** @type {string} */ (tool.color)); - } - this.brush_.setStrokeWidth(tool.size); + this.canvas_.setTool(tool); } flush() { - return new Promise(resolve => this.embed_.flush(resolve)); + return this.canvas_.flush(); } /** @param {string} hexColor */ setOutOfBoundsColor(hexColor) { - this.embed_.setOutOfBoundsColor(ink.Color.fromString(hexColor)); + this.canvas_.setOutOfBoundsColor(hexColor); } /** @param {string} url */ setBorderImage(url) { - this.embed_.setBorderImage(url); + this.canvas_.setBorderImage(url); } /** @param {number} spacing in points */ setPageSpacing(spacing) { - this.embed_.setVerticalPageLayout(spacing); + this.canvas_.setVerticalPageLayout(spacing); } - dispatchPointerEvent(type, init) { - const engine = document.querySelector('#ink-engine'); - const match = engine.style.transform.match(/(\d+)deg/); - const rotation = match ? Number(match[1]) : 0; - let offsetX = init.clientX; - let offsetY = init.clientY; - // If Ink's canvas has been re-orientated away from 0, we must transform - // the event's offsetX and offsetY to correspond with the rotation and - // offset applied. - if ([90, 180, 270].includes(rotation)) { - const width = window.innerWidth; - const height = window.innerHeight; - const matrix = new DOMMatrix(); - matrix.translateSelf(width / 2, height / 2); - matrix.rotateSelf(0, 0, -rotation); - matrix.translateSelf(-width / 2, -height / 2); - const result = matrix.transformPoint({x: offsetX, y: offsetY}); - offsetX = result.x - engine.offsetLeft; - offsetY = result.y - engine.offsetTop; - } - - const event = new PointerEvent(type, init); - // Ink uses offsetX and offsetY, but we can only override them, not pass - // as part of the init. - Object.defineProperty(event, 'offsetX', {value: offsetX}); - Object.defineProperty(event, 'offsetY', {value: offsetY}); - engine.dispatchEvent(event); + dispatchPointerEvent(evt) { + this.canvas_.dispatchInput(evt); } undo() { - this.embed_.undo(); + this.canvas_.undo(); } redo() { - this.embed_.redo(); + this.canvas_.redo(); } } /** @return {Promise<InkAPI>} */ window.initInk = async function() { - const config = new ink.embed.Config(); - const embed = await ink.embed.EmbedComponent.execute(config); - embed.assignFlag(ink.proto.Flag.ENABLE_HOST_CAMERA_CONTROL, true); - return new InkAPI(embed); + const canvas = await drawings.Canvas.execute(document.body); + return new InkAPI(canvas); }; diff --git a/chromium/chrome/browser/resources/pdf/ink_controller.js b/chromium/chrome/browser/resources/pdf/ink_controller.js new file mode 100644 index 00000000000..276b6ab686b --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/ink_controller.js @@ -0,0 +1,128 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {NativeEventTarget as EventTarget} from 'chrome://resources/js/cr/event_target.m.js'; + +import {ContentController} from './controller.js'; +import {Viewport} from './viewport.js'; + +// Note: Redefining this type here, to work around the fact that ink externs +// are only available on Chrome OS, so the targets that contain them cannot be +// built on other platforms. + +/** + * @typedef {{ + * setAnnotationTool: function(AnnotationTool):void, + * viewportChanged: function():void, + * saveDocument: function():!Promise, + * undo: function():void, + * redo: function():void, + * load: function(string, !ArrayBuffer):!Promise, + * viewport: !Viewport, + * }} + */ +let ViewerInkHostElement; + +// Controller for annotation mode, on Chrome OS only. Fires the following events +// from its event target: +// has-unsaved-changes: Fired to indicate there are ink annotations that have +// not been saved. +// set-annotation-undo-state: Contains information about whether undo or redo +// options are available. +export class InkController extends ContentController { + /** + * @param {!Viewport} viewport + * @param {!HTMLDivElement} contentElement + */ + constructor(viewport, contentElement) { + super(); + + /** @private {!Viewport} */ + this.viewport_ = viewport; + + /** @private {!HTMLDivElement} */ + this.contentElement_ = contentElement; + + /** @private {?ViewerInkHostElement} */ + this.inkHost_ = null; + + /** @private {!EventTarget} */ + this.eventTarget_ = new EventTarget(); + + /** @type {?AnnotationTool} */ + this.tool_ = null; + } + + /** @return {!EventTarget} */ + getEventTarget() { + return this.eventTarget_; + } + + /** @param {AnnotationTool} tool */ + setAnnotationTool(tool) { + this.tool_ = tool; + if (this.inkHost_) { + this.inkHost_.setAnnotationTool(tool); + } + } + + /** @override */ + rotateClockwise() { + // TODO(dstockwell): implement rotation + } + + /** @override */ + rotateCounterclockwise() { + // TODO(dstockwell): implement rotation + } + + /** @override */ + setTwoUpView(enableTwoUpView) { + // TODO(dstockwell): Implement two up view. + } + + /** @override */ + viewportChanged() { + this.inkHost_.viewportChanged(); + } + + /** @override */ + save(requestType) { + return this.inkHost_.saveDocument(); + } + + /** @override */ + undo() { + this.inkHost_.undo(); + } + + /** @override */ + redo() { + this.inkHost_.redo(); + } + + /** @override */ + load(filename, data) { + if (!this.inkHost_) { + const inkHost = document.createElement('viewer-ink-host'); + this.contentElement_.appendChild(inkHost); + this.inkHost_ = /** @type {!ViewerInkHostElement} */ (inkHost); + this.inkHost_.viewport = this.viewport_; + inkHost.addEventListener('stroke-added', e => { + this.eventTarget_.dispatchEvent(new CustomEvent('has-unsaved-changes')); + }); + inkHost.addEventListener('undo-state-changed', e => { + this.eventTarget_.dispatchEvent( + new CustomEvent('set-annotation-undo-state', {detail: e.detail})); + }); + } + return this.inkHost_.load(filename, data); + } + + /** @override */ + unload() { + this.inkHost_.remove(); + this.inkHost_ = null; + } +} diff --git a/chromium/chrome/browser/resources/pdf/main.js b/chromium/chrome/browser/resources/pdf/main.js index 28e1726b30b..4b65231c650 100644 --- a/chromium/chrome/browser/resources/pdf/main.js +++ b/chromium/chrome/browser/resources/pdf/main.js @@ -2,16 +2,75 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import './elements/viewer-error-screen.js'; -import './elements/viewer-password-screen.js'; -import './elements/viewer-pdf-toolbar.js'; -import './elements/viewer-zoom-toolbar.js'; -import './elements/shared-vars.js'; -// <if expr="chromeos"> -import './elements/viewer-ink-host.js'; -import './elements/viewer-form-warning.js'; -// </if> - -import {main} from './main_util.js'; +import './pdf_viewer.js'; + +import {BrowserApi, createBrowserApi} from './browser_api.js'; + +/** + * Stores any pending messages received which should be passed to the + * PDFViewer when it is created. + * @type Array + */ +const pendingMessages = []; + +/** + * Handles events that are received prior to the PDFViewer being created. + * @param {Object} message A message event received. + */ +function handleScriptingMessage(message) { + pendingMessages.push(message); +} + +/** + * Initialize the global PDFViewer and pass any outstanding messages to it. + * @param {!BrowserApi} browserApi + */ +function initViewer(browserApi) { + // PDFViewer will handle any messages after it is created. + window.removeEventListener('message', handleScriptingMessage, false); + const viewer = document.querySelector('#viewer'); + viewer.init(browserApi); + while (pendingMessages.length > 0) { + viewer.handleScriptingMessage(pendingMessages.shift()); + } + window.viewer = viewer; +} + +/** + * Determine if the content settings allow PDFs to execute javascript. + * @param {!BrowserApi} browserApi + * @return {!Promise<!BrowserApi>} + */ +function configureJavaScriptContentSetting(browserApi) { + return new Promise((resolve, reject) => { + chrome.contentSettings.javascript.get( + { + 'primaryUrl': browserApi.getStreamInfo().originalUrl, + 'secondaryUrl': window.location.origin + }, + (result) => { + browserApi.getStreamInfo().javascript = result.setting; + resolve(browserApi); + }); + }); +} + +/** + * Entrypoint for starting the PDF viewer. This function obtains the browser + * API for the PDF and initializes the PDF Viewer. + */ +function main() { + // Set up an event listener to catch scripting messages which are sent prior + // to the PDFViewer being created. + window.addEventListener('message', handleScriptingMessage, false); + let chain = createBrowserApi(); + + // Content settings may not be present in test environments. + if (chrome.contentSettings) { + chain = chain.then(configureJavaScriptContentSetting); + } + + chain.then(initViewer); +} main(); diff --git a/chromium/chrome/browser/resources/pdf/main_pp.js b/chromium/chrome/browser/resources/pdf/main_pp.js deleted file mode 100644 index b21153858b0..00000000000 --- a/chromium/chrome/browser/resources/pdf/main_pp.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import './elements/viewer-error-screen.js'; -import './elements/viewer-page-indicator.js'; -import './elements/viewer-zoom-toolbar.js'; -import './elements/shared-vars.js'; - -import {main} from './main_util.js'; - -main(); diff --git a/chromium/chrome/browser/resources/pdf/main_util.js b/chromium/chrome/browser/resources/pdf/main_util.js deleted file mode 100644 index a0585f05ff4..00000000000 --- a/chromium/chrome/browser/resources/pdf/main_util.js +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import {BrowserApi, createBrowserApi} from './browser_api.js'; -import {PDFViewer} from './pdf_viewer.js'; - - -/** - * Global PDFViewer object, accessible for testing. - * - * @type Object - */ -window.viewer = null; - - -/** - * Stores any pending messages received which should be passed to the - * PDFViewer when it is created. - * - * @type Array - */ -const pendingMessages = []; - -/** - * Handles events that are received prior to the PDFViewer being created. - * - * @param {Object} message A message event received. - */ -function handleScriptingMessage(message) { - pendingMessages.push(message); -} -/** - * Initialize the global PDFViewer and pass any outstanding messages to it. - * - * @param {!BrowserApi} browserApi - */ -function initViewer(browserApi) { - // PDFViewer will handle any messages after it is created. - window.removeEventListener('message', handleScriptingMessage, false); - window.viewer = new PDFViewer(browserApi); - while (pendingMessages.length > 0) { - window.viewer.handleScriptingMessage(pendingMessages.shift()); - } -} - -/** - * Determine if the content settings allow PDFs to execute javascript. - * - * @param {!BrowserApi} browserApi - * @return {!Promise<!BrowserApi>} - */ -function configureJavaScriptContentSetting(browserApi) { - return new Promise((resolve, reject) => { - chrome.contentSettings.javascript.get( - { - 'primaryUrl': browserApi.getStreamInfo().originalUrl, - 'secondaryUrl': window.location.origin - }, - (result) => { - browserApi.getStreamInfo().javascript = result.setting; - resolve(browserApi); - }); - }); -} - -/** - * Entrypoint for starting the PDF viewer. This function obtains the browser - * API for the PDF and constructs a PDFViewer object with it. - */ -export function main() { - // Set up an event listener to catch scripting messages which are sent prior - // to the PDFViewer being created. - window.addEventListener('message', handleScriptingMessage, false); - let chain = createBrowserApi(); - - // Content settings may not be present in test environments. - if (chrome.contentSettings) { - chain = chain.then(configureJavaScriptContentSetting); - } - - chain.then(initViewer); -} diff --git a/chromium/chrome/browser/resources/pdf/metrics.js b/chromium/chrome/browser/resources/pdf/metrics.js index d1ca0a8270b..42384c4b0db 100644 --- a/chromium/chrome/browser/resources/pdf/metrics.js +++ b/chromium/chrome/browser/resources/pdf/metrics.js @@ -4,13 +4,10 @@ import {FittingType, TwoUpViewAction} from './constants.js'; -/** - * Handles events specific to the PDF viewer and logs the corresponding metrics. - */ +// Handles events specific to the PDF viewer and logs the corresponding metrics. export class PDFMetrics { /** * Records when the zoom mode is changed to fit a FittingType. - * * @param {FittingType} fittingType the new FittingType. */ static recordFitTo(fittingType) { @@ -25,7 +22,6 @@ export class PDFMetrics { /** * Records when the two up view mode is enabled or disabled. - * * @param {TwoUpViewAction} twoUpViewAction the new TwoUpViewAction. */ static recordTwoUpView(twoUpViewAction) { @@ -36,8 +32,18 @@ export class PDFMetrics { } /** + * Records zoom in and zoom out actions. + * @param {boolean} isZoomIn True when the action is zooming in, false when + * the action is zooming out. + */ + static recordZoomAction(isZoomIn) { + PDFMetrics.record( + isZoomIn ? PDFMetrics.UserAction.ZOOM_IN : + PDFMetrics.UserAction.ZOOM_OUT); + } + + /** * Records the given action to chrome.metricsPrivate. - * * @param {PDFMetrics.UserAction} action */ static record(action) { @@ -84,18 +90,15 @@ PDFMetrics.firstActionRecorded_ = new Set(); * The *_FIRST values are recorded automaticlly, * eg. PDFMetrics.record(...ROTATE) will also record ROTATE_FIRST * on the first instance. - * * @enum {number} */ PDFMetrics.UserAction = { - /** - * Recorded when the document is first loaded. This event serves as - * denominator to determine percentages of documents in which an action was - * taken as well as average number of each action per document. - */ + // Recorded when the document is first loaded. This event serves as + // denominator to determine percentages of documents in which an action was + // taken as well as average number of each action per document. DOCUMENT_OPENED: 0, - /** Recorded when the document is rotated clockwise or counter-clockwise. */ + // Recorded when the document is rotated clockwise or counter-clockwise. ROTATE_FIRST: 1, ROTATE: 2, @@ -105,26 +108,24 @@ PDFMetrics.UserAction = { FIT_TO_PAGE_FIRST: 5, FIT_TO_PAGE: 6, - /** Recorded when the bookmarks panel is opened. */ + // Recorded when the bookmarks panel is opened. OPEN_BOOKMARKS_PANEL_FIRST: 7, OPEN_BOOKMARKS_PANEL: 8, - /** Recorded when a bookmark is followed. */ + // Recorded when a bookmark is followed. FOLLOW_BOOKMARK_FIRST: 9, FOLLOW_BOOKMARK: 10, - /** Recorded when the page selection is used to navigate to another page. */ + // Recorded when the page selection is used to navigate to another page. PAGE_SELECTOR_NAVIGATE_FIRST: 11, PAGE_SELECTOR_NAVIGATE: 12, - /** Recorded when the user triggers a save of the document. */ + // Recorded when the user triggers a save of the document. SAVE_FIRST: 13, SAVE: 14, - /** - * Recorded when the user triggers a save of the document and the document - * has been modified by annotations. - */ + // Recorded when the user triggers a save of the document and the document + // has been modified by annotations. SAVE_WITH_ANNOTATION_FIRST: 15, SAVE_WITH_ANNOTATION: 16, @@ -137,39 +138,47 @@ PDFMetrics.UserAction = { EXIT_ANNOTATION_MODE_FIRST: 21, EXIT_ANNOTATION_MODE: 22, - /** Recorded when a pen stroke is made. */ + // Recorded when a pen stroke is made. ANNOTATE_STROKE_TOOL_PEN_FIRST: 23, ANNOTATE_STROKE_TOOL_PEN: 24, - /** Recorded when an eraser stroke is made. */ + // Recorded when an eraser stroke is made. ANNOTATE_STROKE_TOOL_ERASER_FIRST: 25, ANNOTATE_STROKE_TOOL_ERASER: 26, - /** Recorded when a highlighter stroke is made. */ + // Recorded when a highlighter stroke is made. ANNOTATE_STROKE_TOOL_HIGHLIGHTER_FIRST: 27, ANNOTATE_STROKE_TOOL_HIGHLIGHTER: 28, - /** Recorded when a stroke is made using touch. */ + // Recorded when a stroke is made using touch. ANNOTATE_STROKE_DEVICE_TOUCH_FIRST: 29, ANNOTATE_STROKE_DEVICE_TOUCH: 30, - /** Recorded when a stroke is made using mouse. */ + // Recorded when a stroke is made using mouse. ANNOTATE_STROKE_DEVICE_MOUSE_FIRST: 31, ANNOTATE_STROKE_DEVICE_MOUSE: 32, - /** Recorded when a stroke is made using pen. */ + // Recorded when a stroke is made using pen. ANNOTATE_STROKE_DEVICE_PEN_FIRST: 33, ANNOTATE_STROKE_DEVICE_PEN: 34, - /** Recorded when two-up view mode is enabled. */ + // Recorded when two-up view mode is enabled. TWO_UP_VIEW_ENABLE_FIRST: 35, TWO_UP_VIEW_ENABLE: 36, - /** Recorded when two-up view mode is disabled. */ + // Recorded when two-up view mode is disabled. TWO_UP_VIEW_DISABLE_FIRST: 37, TWO_UP_VIEW_DISABLE: 38, - NUMBER_OF_ACTIONS: 39, + // Recorded when zoom in button is clicked. + ZOOM_IN_FIRST: 39, + ZOOM_IN: 40, + + // Recorded when zoom out button is clicked. + ZOOM_OUT_FIRST: 41, + ZOOM_OUT: 42, + + NUMBER_OF_ACTIONS: 43, }; // Map from UserAction to the 'FIRST' action. These metrics are recorded @@ -252,4 +261,12 @@ PDFMetrics.firstMap_ = new Map([ PDFMetrics.UserAction.TWO_UP_VIEW_DISABLE, PDFMetrics.UserAction.TWO_UP_VIEW_DISABLE_FIRST, ], + [ + PDFMetrics.UserAction.ZOOM_IN, + PDFMetrics.UserAction.ZOOM_IN_FIRST, + ], + [ + PDFMetrics.UserAction.ZOOM_OUT, + PDFMetrics.UserAction.ZOOM_OUT_FIRST, + ], ]); diff --git a/chromium/chrome/browser/resources/pdf/navigator.js b/chromium/chrome/browser/resources/pdf/navigator.js index e24d3e8ff02..8aa2c1e3fd6 100644 --- a/chromium/chrome/browser/resources/pdf/navigator.js +++ b/chromium/chrome/browser/resources/pdf/navigator.js @@ -5,10 +5,8 @@ import {OpenPdfParamsParser} from './open_pdf_params_parser.js'; import {Viewport} from './viewport.js'; -/** - * NavigatorDelegate for calling browser-specific functions to do the actual - * navigating. - */ +// NavigatorDelegate for calling browser-specific functions to do the actual +// navigating. export class NavigatorDelegate { /** * @param {number} tabId The tab ID of the PDF viewer or -1 if the viewer is @@ -63,7 +61,7 @@ export class NavigatorDelegate { } } -/** Navigator for navigating to links inside or outside the PDF. */ +// Navigator for navigating to links inside or outside the PDF. export class PdfNavigator { /** * @param {string} originalUrl The original page URL. diff --git a/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js b/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js index 08ef73a94db..858a62b47fd 100644 --- a/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js +++ b/chromium/chrome/browser/resources/pdf/open_pdf_params_parser.js @@ -4,10 +4,8 @@ import {FittingType} from './constants.js'; -/** - * Parses the open pdf parameters passed in the url to set initial viewport - * settings for opening the pdf. - */ +// Parses the open pdf parameters passed in the url to set initial viewport +// settings for opening the pdf. export class OpenPdfParamsParser { /** * @param {function(string):void} getNamedDestinationCallback @@ -24,7 +22,6 @@ export class OpenPdfParamsParser { /** * Parse zoom parameter of open PDF parameters. The PDF should be opened at * the specified zoom level. - * * @param {string} paramValue zoom value. * @return {Object} Map with zoom parameters (zoom and position). * @private @@ -57,7 +54,6 @@ export class OpenPdfParamsParser { /** * Parse view parameter of open PDF parameters. The PDF should be opened at * the specified fitting type mode and position. - * * @param {string} paramValue view value. * @return {Object} Map with view parameters (view and viewPosition). * @private @@ -95,57 +91,35 @@ export class OpenPdfParamsParser { } /** - * Parse the parameters encoded in the fragment of a URL into a dictionary. - * + * Parse the parameters encoded in the fragment of a URL. * @param {string} url to parse - * @return {Object} Key-value pairs of URL parameters + * @return {!URLSearchParams} * @private */ parseUrlParams_(url) { - const params = {}; - - const paramIndex = url.search('#'); - if (paramIndex === -1) { - return params; - } - - const paramTokens = url.substring(paramIndex + 1).split('&'); - if ((paramTokens.length === 1) && (paramTokens[0].search('=') === -1)) { - // Handle the case of http://foo.com/bar#NAMEDDEST. This is not - // explicitly mentioned except by example in the Adobe - // "PDF Open Parameters" document. - params['nameddest'] = paramTokens[0]; - return params; - } - - for (const paramToken of paramTokens) { - const keyValueSplit = paramToken.split('='); - if (keyValueSplit.length !== 2) { - continue; + const hash = new URL(url).hash; + const params = new URLSearchParams(hash.substring(1)); + + // Handle the case of http://foo.com/bar#NAMEDDEST. This is not + // explicitly mentioned except by example in the Adobe + // "PDF Open Parameters" document. + if (Array.from(params).length === 1) { + const key = Array.from(params.keys())[0]; + if (params.get(key) === '') { + params.append('nameddest', key); + params.delete(key); } - params[keyValueSplit[0]] = keyValueSplit[1]; } return params; } /** - * Parse PDF url parameters used for controlling the state of UI. These need - * to be available when the UI is being initialized, rather than when the PDF - * is finished loading. - * * @param {string} url that needs to be parsed. - * @return {Object} parsed url parameters. + * @return {boolean} Whether the toolbar UI element should be shown. */ - getUiUrlParams(url) { - const params = this.parseUrlParams_(url); - const uiParams = {toolbar: true}; - - if ('toolbar' in params && params['toolbar'] === '0') { - uiParams.toolbar = false; - } - - return uiParams; + shouldShowToolbar(url) { + return this.parseUrlParams_(url).get('toolbar') !== '0'; } /** @@ -153,7 +127,6 @@ export class OpenPdfParamsParser { * and specify actions to be performed when opening pdf files. * See http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/ * pdfs/pdf_open_parameters.pdf for details. - * * @param {string} url that needs to be parsed. * @param {Function} callback function to be called with viewport info. */ @@ -163,25 +136,30 @@ export class OpenPdfParamsParser { const urlParams = this.parseUrlParams_(url); - if ('page' in urlParams) { + if (urlParams.has('page')) { // |pageNumber| is 1-based, but goToPage() take a zero-based page number. - const pageNumber = parseInt(urlParams['page'], 10); + const pageNumber = parseInt(urlParams.get('page'), 10); if (!Number.isNaN(pageNumber) && pageNumber > 0) { params['page'] = pageNumber - 1; } } - if ('view' in urlParams) { - Object.assign(params, this.parseViewParam_(urlParams['view'])); + if (urlParams.has('view')) { + Object.assign( + params, + this.parseViewParam_(/** @type {string} */ (urlParams.get('view')))); } - if ('zoom' in urlParams) { - Object.assign(params, this.parseZoomParam_(urlParams['zoom'])); + if (urlParams.has('zoom')) { + Object.assign( + params, + this.parseZoomParam_(/** @type {string} */ (urlParams.get('zoom')))); } - if (params.page === undefined && 'nameddest' in urlParams) { + if (params.page === undefined && urlParams.has('nameddest')) { this.outstandingRequests_.push({callback: callback, params: params}); - this.getNamedDestinationCallback_(urlParams['nameddest']); + this.getNamedDestinationCallback_( + /** @type {string} */ (urlParams.get('nameddest'))); } else { callback(params); } @@ -190,7 +168,6 @@ export class OpenPdfParamsParser { /** * This is called when a named destination is received and the page number * corresponding to the request for which a named destination is passed. - * * @param {number} pageNumber The page corresponding to the named destination * requested. */ diff --git a/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js index 8cae862e05d..6c2cad0de82 100644 --- a/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js +++ b/chromium/chrome/browser/resources/pdf/pdf_scripting_api.js @@ -4,26 +4,27 @@ /** * Turn a dictionary received from postMessage into a key event. - * * @param {Object} dict A dictionary representing the key event. - * @return {!Event} A key event. + * @return {!KeyboardEvent} A key event. */ export function DeserializeKeyEvent(dict) { - const e = document.createEvent('Event'); - e.initEvent('keydown', true, true); - e.keyCode = dict.keyCode; - e.code = dict.code; - e.shiftKey = dict.shiftKey; - e.ctrlKey = dict.ctrlKey; - e.altKey = dict.altKey; - e.metaKey = dict.metaKey; + const e = new KeyboardEvent('keydown', { + bubbles: true, + cancelable: true, + key: dict.key, + code: dict.code, + keyCode: dict.keyCode, + shiftKey: dict.shiftKey, + ctrlKey: dict.ctrlKey, + altKey: dict.altKey, + metaKey: dict.metaKey, + }); e.fromScriptingAPI = true; return e; } /** * Turn a key event into a dictionary which can be sent over postMessage. - * * @param {Event} event A key event. * @return {Object} A dictionary representing the key event. */ @@ -31,6 +32,7 @@ export function SerializeKeyEvent(event) { return { keyCode: event.keyCode, code: event.code, + key: event.key, shiftKey: event.shiftKey, ctrlKey: event.ctrlKey, altKey: event.altKey, @@ -49,11 +51,8 @@ export const LoadState = { FAILED: 'failed' }; -/** - * Create a new PDFScriptingAPI. This provides a scripting interface to - * the PDF viewer so that it can be customized by things like print preview. - * - */ +// Provides a scripting interface to the PDF viewer so that it can be customized +// by things like print preview. export class PDFScriptingAPI { /** * @param {Window} window the window of the page containing the pdf viewer. @@ -134,7 +133,6 @@ export class PDFScriptingAPI { * Send a message to the extension. If messages try to get sent before there * is a plugin element set, then we queue them up and send them later (this * can happen in print preview). - * * @param {Object} message The message to send. * @private */ @@ -149,7 +147,6 @@ export class PDFScriptingAPI { /** * Sets the plugin element containing the PDF viewer. The element will usually * be passed into the PDFScriptingAPI constructor but may also be set later. - * * @param {Object} plugin the plugin element containing the PDF viewer. */ setPlugin(plugin) { @@ -168,7 +165,6 @@ export class PDFScriptingAPI { /** * Sets the callback which will be run when the PDF viewport changes. - * * @param {Function} callback the callback to be called. */ setViewportChangedCallback(callback) { @@ -198,7 +194,6 @@ export class PDFScriptingAPI { /** * Resets the PDF viewer into print preview mode. - * * @param {string} url the url of the PDF to load. * @param {boolean} grayscale whether or not to display the PDF in grayscale. * @param {Array<number>} pageNumbers an array of the page numbers. @@ -215,16 +210,13 @@ export class PDFScriptingAPI { }); } - /** - * Hide the toolbars after a delay. - */ + /** Hide the toolbars after a delay. */ hideToolbars() { this.sendMessage_({type: 'hideToolbars'}); } /** * Load a page into the document while in print preview mode. - * * @param {string} url the url of the pdf page to load. * @param {number} index the index of the page to load. */ @@ -248,7 +240,6 @@ export class PDFScriptingAPI { /** * Get the selected text in the document. The callback will be called with the * text that is selected. May only be called after document load. - * * @param {Function} callback a callback to be called with the selected text. * @return {boolean} true if the function is successful, false if there is an * outstanding request for selected text that has not been answered. @@ -262,16 +253,13 @@ export class PDFScriptingAPI { return true; } - /** - * Print the document. May only be called after document load. - */ + /** Print the document. May only be called after document load. */ print() { this.sendMessage_({type: 'print'}); } /** * Send a key event to the extension. - * * @param {Event} keyEvent the key event to send to the extension. */ sendKeyEvent(keyEvent) { @@ -293,7 +281,6 @@ export class PDFScriptingAPI { * iframe which is navigated to the PDF viewer extension and 2) a scripting * interface which provides access to various features of the viewer for use * by print preview and accessibility. - * * @param {string} src the source URL of the PDF to load initially. * @param {string} baseUrl the base URL of the PDF viewer * @return {!HTMLIFrameElement} the iframe element containing the PDF viewer. diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer.html b/chromium/chrome/browser/resources/pdf/pdf_viewer.html new file mode 100644 index 00000000000..a56670db71c --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer.html @@ -0,0 +1,61 @@ +<style include="pdf-viewer-shared-style"> + viewer-pdf-toolbar, + viewer-pdf-toolbar-new { + position: fixed; + width: 100%; + z-index: 4; + } + + viewer-ink-host { + height: 100%; + position: absolute; + width: 100%; + } + + @media(max-height: 250px) { + viewer-pdf-toolbar { + display: none; + } + } +</style> + +<template is="dom-if" if="[[!pdfViewerUpdateEnabled_]]"> + <viewer-pdf-toolbar id="toolbar" strings="[[strings]]" + annotation-available="[[annotationAvailable_]]" + bookmarks="[[bookmarks_]]" doc-title="[[title_]]" + has-edits="[[hasEdits_]]" + has-entered-annotation-mode="[[hasEnteredAnnotationMode_]]" + is-form-field-focused="[[isFormFieldFocused_]]" + on-save="onToolbarSave_" on-print="onPrint_" + on-annotation-mode-toggled="onAnnotationModeToggled_" + on-annotation-tool-changed="onAnnotationToolChanged_" + on-rotate-right="rotateClockwise" on-undo="onUndo_" on-redo="onRedo_" + hidden> + </viewer-pdf-toolbar> +</template> +<template is="dom-if" if="[[pdfViewerUpdateEnabled_]]"> + <viewer-pdf-toolbar-new id="toolbar" hidden> + </viewer-pdf-toolbar-new> +</template> + +<div id="sizer"></div> +<viewer-password-screen id="password-screen" + on-password-submitted="onPasswordSubmitted_"> +</viewer-password-screen> + +<viewer-zoom-toolbar id="zoom-toolbar" strings="[[strings]]" + annotation-mode="[[annotationMode_]]" + on-fit-to-changed="onFitToChanged" + on-two-up-view-changed="onTwoUpViewChanged_" + on-zoom-in="onZoomIn" on-zoom-out="onZoomOut" + hidden> +</viewer-zoom-toolbar> + +<viewer-error-screen id="error-screen"></viewer-error-screen> + +<if expr="chromeos"> +<viewer-form-warning id="form-warning"></viewer-form-warning> +</if> + +<div id="content"></div> + diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer.js b/chromium/chrome/browser/resources/pdf/pdf_viewer.js index e6fc90845ab..e23b4b41e48 100644 --- a/chromium/chrome/browser/resources/pdf/pdf_viewer.js +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer.js @@ -2,46 +2,38 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import './elements/viewer-error-screen.js'; +import './elements/viewer-password-screen.js'; +import './elements/viewer-pdf-toolbar.js'; +import './elements/viewer-pdf-toolbar-new.js'; +import './elements/shared-vars.js'; +// <if expr="chromeos"> +import './elements/viewer-ink-host.js'; +import './elements/viewer-form-warning.js'; +// </if> +import './pdf_viewer_shared_style.js'; + import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js'; -import {EventTracker} from 'chrome://resources/js/event_tracker.m.js'; import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; -import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js'; -import {$, hasKeyModifiers, isRTL} from 'chrome://resources/js/util.m.js'; +import {hasKeyModifiers, isRTL} from 'chrome://resources/js/util.m.js'; +import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; import {Bookmark} from './bookmark_type.js'; import {BrowserApi} from './browser_api.js'; -import {FittingType, TwoUpViewAction} from './constants.js'; -import {ContentController, InkController, MessageData, PluginController, PrintPreviewParams} from './controller.js'; -import {FitToChangedEvent} from './elements/viewer-zoom-toolbar.js'; -import {GestureDetector} from './gesture_detector.js'; +import {FittingType, SaveRequestType, TwoUpViewAction} from './constants.js'; import {PDFMetrics} from './metrics.js'; import {NavigatorDelegate, PdfNavigator} from './navigator.js'; import {OpenPdfParamsParser} from './open_pdf_params_parser.js'; import {DeserializeKeyEvent, LoadState, SerializeKeyEvent} from './pdf_scripting_api.js'; +import {PDFViewerBaseElement} from './pdf_viewer_base.js'; +import {DestinationMessageData, DocumentDimensionsMessageData, shouldIgnoreKeyEvents} from './pdf_viewer_utils.js'; import {ToolbarManager} from './toolbar_manager.js'; -import {LayoutOptions, Point, Viewport} from './viewport.js'; -import {ViewportScroller} from './viewport_scroller.js'; -import {ZoomManager} from './zoom_manager.js'; +import {Point} from './viewport.js'; -/** - * @typedef {{ - * source: Object, - * origin: string, - * data: !MessageData, - * }} - */ -let MessageObject; +// <if expr="chromeos"> +import {InkController} from './ink_controller.js'; +// </if> -/** - * @typedef {{ - * type: string, - * height: number, - * width: number, - * layoutOptions: (!LayoutOptions|undefined), - * pageDimensions: Array - * }} - */ -let DocumentDimensionsMessageData; /** * @typedef {{ @@ -55,17 +47,6 @@ let NavigateMessageData; /** * @typedef {{ * type: string, - * page: number, - * x: number, - * y: number, - * zoom: number - * }} - */ -let DestinationMessageData; - -/** - * @typedef {{ - * type: string, * title: string, * bookmarks: !Array<!Bookmark>, * canSerializeDocument: boolean, @@ -82,22 +63,9 @@ let MetadataMessageData; */ let RequiredSaveResult; -/** @return {number} Width of a scrollbar in pixels */ -function getScrollbarWidth() { - const 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); - const result = div.offsetWidth - div.clientWidth; - div.parentNode.removeChild(div); - return result; -} - /** * Return the filename component of a URL, percent decoded if possible. + * Exported for tests. * @param {string} url The URL to get the filename from. * @return {string} The filename component. */ @@ -116,270 +84,187 @@ export function getFilenameFromURL(url) { } } -/** - * Whether keydown events should currently be ignored. Events are ignored when - * an editable element has focus, to allow for proper editing controls. - * @param {Element} activeElement The currently selected DOM node. - * @return {boolean} True if keydown events should be ignored. - */ -export function shouldIgnoreKeyEvents(activeElement) { - while (activeElement.shadowRoot != null && - activeElement.shadowRoot.activeElement != null) { - activeElement = activeElement.shadowRoot.activeElement; +class PDFViewerElement extends PDFViewerBaseElement { + static get is() { + return 'pdf-viewer'; } - return ( - activeElement.isContentEditable || - (activeElement.tagName === 'INPUT' && activeElement.type !== 'radio') || - activeElement.tagName === 'TEXTAREA'); -} + static get template() { + return html`{__html_template__}`; + } -/** - * Creates a new PDFViewer. There should only be one of these objects per - * document. - */ -export class PDFViewer { - /** - * @param {!BrowserApi} browserApi An object providing an API to the browser. - */ - constructor(browserApi) { - /** @private {!BrowserApi} */ - this.browserApi_ = browserApi; + static get properties() { + return { + annotationAvailable_: { + type: Boolean, + computed: 'computeAnnotationAvailable_(' + + 'hadPassword_, rotated_, canSerializeDocument_)', + }, - /** @private {string} */ - this.originalUrl_ = this.browserApi_.getStreamInfo().originalUrl; + annotationMode_: { + type: Boolean, + value: false, + }, - /** @private {string} */ - this.javascript_ = this.browserApi_.getStreamInfo().javascript || 'block'; + bookmarks_: Array, - /** @private {!LoadState} */ - this.loadState_ = LoadState.LOADING; + hasEdits_: { + type: Boolean, + value: false, + }, - /** @private {?Object} */ - this.parentWindow_ = null; + hasEnteredAnnotationMode_: { + type: Boolean, + value: false, + }, - /** @private {?string} */ - this.parentOrigin_ = null; + rotated_: Boolean, - /** @private {boolean} */ - this.isFormFieldFocused_ = false; + hadPassword_: Boolean, - /** @private {number} */ - this.beepCount_ = 0; + canSerializeDocument_: Boolean, - /** @private {!Array} */ - this.delayedScriptingMessages_ = []; + title_: String, - /** @private {!PromiseResolver} */ - this.loaded_; + isFormFieldFocused_: Boolean, - /** @private {boolean} */ - this.initialLoadComplete_ = false; + /** @private */ + pdfViewerUpdateEnabled_: { + type: Boolean, + value: function() { + return document.documentElement.hasAttribute( + 'pdf-viewer-update-enabled'); + }, + }, + }; + } + constructor() { + super(); + + // Polymer properties /** @private {boolean} */ - this.isPrintPreview_ = location.origin === 'chrome://print'; - document.documentElement.toggleAttribute( - 'is-print-preview', this.isPrintPreview_); + this.annotationAvailable_; /** @private {boolean} */ - this.isPrintPreviewLoadingFinished_ = false; + this.annotationMode_ = false; + + /** @private {!Array<!Bookmark>} */ + this.bookmarks_ = []; /** @private {boolean} */ - this.isUserInitiatedEvent_ = true; + this.hasEdits_ = false; /** @private {boolean} */ this.hasEnteredAnnotationMode_ = false; /** @private {boolean} */ + this.rotated_ = false; + + /** @private {boolean} */ this.hadPassword_ = false; /** @private {boolean} */ this.canSerializeDocument_ = false; - /** @private {!EventTracker} */ - this.tracker_ = new EventTracker(); - - PDFMetrics.record(PDFMetrics.UserAction.DOCUMENT_OPENED); - - // Parse open pdf parameters. - /** @private {!OpenPdfParamsParser} */ - this.paramsParser_ = new OpenPdfParamsParser( - destination => this.pluginController_.getNamedDestination(destination)); - const toolbarEnabled = - this.paramsParser_.getUiUrlParams(this.originalUrl_).toolbar && - !this.isPrintPreview_; - - // The sizer element is placed behind the plugin element to cause scrollbars - // to be displayed in the window. It is sized according to the document size - // of the pdf and zoom level. - this.sizer_ = /** @type {!HTMLDivElement} */ ($('sizer')); - - /** @private {?ViewerPageIndicatorElement} */ - this.pageIndicator_ = this.isPrintPreview_ ? - /** @type {!ViewerPageIndicatorElement} */ ($('page-indicator')) : - null; - - /** @private {?ViewerPasswordScreenElement} */ - this.passwordScreen_ = - /** @type {?ViewerPasswordScreenElement} */ ($('password-screen')); - if (this.passwordScreen_) { - this.passwordScreen_.addEventListener('password-submitted', e => { - this.onPasswordSubmitted_( - /** @type {!CustomEvent<{password: string}>} */ (e)); - }); - } + /** @private {string} */ + this.title_ = ''; - /** @private {?ViewerErrorScreenElement} */ - this.errorScreen_ = - /** @type {!ViewerErrorScreenElement} */ ($('error-screen')); - // Can only reload if we are in a normal tab. - if (chrome.tabs && this.browserApi_.getStreamInfo().tabId !== -1) { - this.errorScreen_.reloadFn = () => { - chrome.tabs.reload(this.browserApi_.getStreamInfo().tabId); - }; - } + /** @private {boolean} */ + this.isFormFieldFocused_ = false; - // Create the viewport. - const shortWindow = - window.innerHeight < PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT; - const topToolbarHeight = - (toolbarEnabled) ? PDFViewer.MATERIAL_TOOLBAR_HEIGHT : 0; - const defaultZoom = - this.browserApi_.getZoomBehavior() === BrowserApi.ZoomBehavior.MANAGE ? - this.browserApi_.getDefaultZoom() : - 1.0; - - /** @private {!Viewport} */ - this.viewport_ = new Viewport( - window, this.sizer_, getScrollbarWidth(), defaultZoom, - topToolbarHeight); - this.viewport_.setViewportChangedCallback(() => this.viewportChanged_()); - this.viewport_.setBeforeZoomCallback( - () => this.currentController_.beforeZoom()); - this.viewport_.setAfterZoomCallback( - () => this.currentController_.afterZoom()); - this.viewport_.setUserInitiatedCallback( - userInitiated => this.setUserInitiated_(userInitiated)); - window.addEventListener('beforeunload', () => this.resetTrackers_()); - - // Create the plugin object dynamically so we can set its src. The plugin - // element is sized to fill the entire window and is set to be fixed - // positioning, acting as a viewport. The plugin renders into this viewport - // according to the scroll position of the window. - /** @private {!HTMLEmbedElement} */ - this.plugin_ = - /** @type {!HTMLEmbedElement} */ (document.createElement('embed')); - - // NOTE: The plugin's 'id' field must be set to 'plugin' since - // chrome/renderer/printing/print_render_frame_helper.cc actually - // references it. - this.plugin_.id = 'plugin'; - this.plugin_.type = 'application/x-google-chrome-pdf'; - - // Handle scripting messages from outside the extension that wish to - // interact with it. We also send a message indicating that extension has - // loaded and is ready to receive messages. - window.addEventListener('message', message => { - this.handleScriptingMessage(/** @type {!MessageObject} */ (message)); - }, false); - - this.plugin_.setAttribute('src', this.originalUrl_); - this.plugin_.setAttribute( - 'stream-url', this.browserApi_.getStreamInfo().streamUrl); - let headers = ''; - for (const header in this.browserApi_.getStreamInfo().responseHeaders) { - headers += header + ': ' + - this.browserApi_.getStreamInfo().responseHeaders[header] + '\n'; - } - this.plugin_.setAttribute('headers', headers); + // Non-Polymer properties - this.plugin_.setAttribute('background-color', PDFViewer.BACKGROUND_COLOR); - this.plugin_.setAttribute('top-toolbar-height', topToolbarHeight); - this.plugin_.setAttribute('javascript', this.javascript_); + /** @private {number} */ + this.beepCount_ = 0; - if (this.browserApi_.getStreamInfo().embedded) { - this.plugin_.setAttribute( - 'top-level-url', this.browserApi_.getStreamInfo().tabUrl); - } else { - this.plugin_.setAttribute('full-frame', ''); - } + /** @private {boolean} */ + this.hadPassword_ = false; + + /** @private {boolean} */ + this.toolbarEnabled_ = false; + + // <if expr="chromeos"> + /** @private {?InkController} */ + this.inkController_ = null; + // </if> + + /** @private {?ToolbarManager} */ + this.toolbarManager_ = null; + + /** @private {?PdfNavigator} */ + this.navigator_ = null; + + /** @private {string} */ + this.title_ = ''; + + /** @private {boolean} */ + this.pdfViewerUpdateEnabled_; + } - $('content').appendChild(this.plugin_); + /** @override */ + getToolbarHeight() { + assert(this.paramsParser); + this.toolbarEnabled_ = + this.paramsParser.shouldShowToolbar(this.originalUrl); + return this.toolbarEnabled_ ? MATERIAL_TOOLBAR_HEIGHT : 0; + } + + /** @override */ + getContent() { + return /** @type {!HTMLDivElement} */ (this.$$('#content')); + } - /** @private {!PluginController} */ - this.pluginController_ = new PluginController( - this.plugin_, this.viewport_, () => this.isUserInitiatedEvent_, - () => this.loaded); - this.tracker_.add( - this.pluginController_.getEventTarget(), 'plugin-message', - e => this.handlePluginMessage_(e)); + /** @override */ + getSizer() { + return /** @type {!HTMLDivElement} */ (this.$$('#sizer')); + } + + /** @override */ + getZoomToolbar() { + return /** @type {!ViewerZoomToolbarElement} */ (this.$$('#zoom-toolbar')); + } - /** @private {!InkController} */ - this.inkController_ = new InkController(this.viewport_); - this.tracker_.add( + /** @override */ + getErrorScreen() { + return /** @type {!ViewerErrorScreenElement} */ (this.$$('#error-screen')); + } + + /** + * @return {!ViewerPdfToolbarElement} + * @private + */ + getToolbar_() { + return /** @type {!ViewerPdfToolbarElement} */ (this.$$('#toolbar')); + } + + /** @override */ + getBackgroundColor() { + return BACKGROUND_COLOR; + } + + /** @param {!BrowserApi} browserApi */ + init(browserApi) { + super.init(browserApi); + + // <if expr="chromeos"> + this.inkController_ = new InkController( + this.viewport, /** @type {!HTMLDivElement} */ (this.getContent())); + this.tracker.add( this.inkController_.getEventTarget(), 'stroke-added', () => chrome.mimeHandlerPrivate.setShowBeforeUnloadDialog(true)); - this.tracker_.add( + this.tracker.add( this.inkController_.getEventTarget(), 'set-annotation-undo-state', e => this.setAnnotationUndoState_(e)); + // </if> - /** @private {!ContentController} */ - this.currentController_ = this.pluginController_; - - // Setup the button event listeners. - /** @private {!ViewerZoomToolbarElement} */ - this.zoomToolbar_ = - /** @type {!ViewerZoomToolbarElement} */ ($('zoom-toolbar')); - this.zoomToolbar_.isPrintPreview = this.isPrintPreview_; - this.zoomToolbar_.addEventListener( - 'fit-to-changed', - e => this.fitToChanged_( - /** @type {!CustomEvent<FitToChangedEvent>} */ (e))); - this.zoomToolbar_.addEventListener( - 'two-up-view-changed', - e => this.twoUpViewChanged_( - /** @type {!CustomEvent<!TwoUpViewAction>} */ (e))); - this.zoomToolbar_.addEventListener( - 'zoom-in', () => this.viewport_.zoomIn()); - this.zoomToolbar_.addEventListener( - 'zoom-out', () => this.viewport_.zoomOut()); - - /** @private {!GestureDetector} */ - this.gestureDetector_ = new GestureDetector(assert($('content'))); - this.gestureDetector_.addEventListener( - 'pinchstart', e => this.onPinchStart_(e)); - this.sentPinchEvent_ = false; - this.gestureDetector_.addEventListener( - 'pinchupdate', e => this.onPinchUpdate_(e)); - this.gestureDetector_.addEventListener( - 'pinchend', e => this.onPinchEnd_(e)); - - /** @private {?ViewerPdfToolbarElement} */ - this.toolbar_ = null; - if (toolbarEnabled) { - this.toolbar_ = /** @type {!ViewerPdfToolbarElement} */ ($('toolbar')); - this.toolbar_.hidden = false; - this.toolbar_.addEventListener('save', () => this.save_()); - this.toolbar_.addEventListener('print', () => this.print_()); - this.toolbar_.addEventListener( - 'undo', () => this.currentController_.undo()); - this.toolbar_.addEventListener( - 'redo', () => this.currentController_.redo()); - this.toolbar_.addEventListener( - 'rotate-right', () => this.rotateClockwise_()); - this.toolbar_.addEventListener('annotation-mode-toggled', e => { - this.annotationModeToggled_( - /** @type {!CustomEvent<{value: boolean}>} */ (e)); - }); - this.toolbar_.addEventListener( - 'annotation-tool-changed', - e => this.inkController_.setAnnotationTool(e.detail.value)); - - this.toolbar_.docTitle = getFilenameFromURL(this.originalUrl_); + this.title_ = getFilenameFromURL(this.originalUrl); + if (this.toolbarEnabled_) { + this.getToolbar_().hidden = false; } document.body.addEventListener('change-page', e => { - this.viewport_.goToPage(e.detail.page); + this.viewport.goToPage(e.detail.page); if (e.detail.origin === 'bookmark') { PDFMetrics.record(PDFMetrics.UserAction.FOLLOW_BOOKMARK); } else if (e.detail.origin === 'pageselector') { @@ -387,12 +272,8 @@ export class PDFViewer { } }); - document.body.addEventListener('change-zoom', e => { - this.viewport_.setZoom(e.detail.zoom); - }); - document.body.addEventListener('change-page-and-xy', e => { - const point = this.viewport_.convertPageToScreen(e.detail.page, e.detail); + const point = this.viewport.convertPageToScreen(e.detail.page, e.detail); this.goToPageAndXY_(e.detail.origin, e.detail.page, point); }); @@ -409,59 +290,21 @@ export class PDFViewer { } }); - /** @private {!ToolbarManager} */ - this.toolbarManager_ = - new ToolbarManager(window, this.toolbar_, this.zoomToolbar_); - - // Set up the ZoomManager. - /** @private {!ZoomManager} */ - this.zoomManager_ = ZoomManager.create( - this.browserApi_.getZoomBehavior(), () => this.viewport_.getZoom(), - zoom => this.browserApi_.setZoom(zoom), - this.browserApi_.getInitialZoom()); - this.viewport_.setZoomManager(this.zoomManager_); - this.browserApi_.addZoomEventListener( - zoom => this.zoomManager_.onBrowserZoomChange(zoom)); + this.toolbarManager_ = new ToolbarManager( + window, this.pdfViewerUpdateEnabled_ ? null : this.getToolbar_(), + this.getZoomToolbar()); // Setup the keyboard event listener. document.addEventListener( 'keydown', e => this.handleKeyEvent_(/** @type {!KeyboardEvent} */ (e))); - document.addEventListener('mousemove', e => this.handleMouseEvent_(e)); - document.addEventListener('mouseout', e => this.handleMouseEvent_(e)); - document.addEventListener( - 'contextmenu', e => this.handleContextMenuEvent_(e)); - const tabId = this.browserApi_.getStreamInfo().tabId; - /** @private {!PdfNavigator} */ + const tabId = this.browserApi.getStreamInfo().tabId; this.navigator_ = new PdfNavigator( - this.originalUrl_, this.viewport_, this.paramsParser_, + this.originalUrl, this.viewport, + /** @type {!OpenPdfParamsParser} */ (this.paramsParser), new NavigatorDelegate(tabId)); - /** @private {!ViewportScroller} */ - this.viewportScroller_ = - new ViewportScroller(this.viewport_, this.plugin_, window); - - /** @private {!Array<!Bookmark>} */ - this.bookmarks_; - - /** @private {!Point} */ - this.lastViewportPosition_; - - /** @private {boolean} */ - this.inPrintPreviewMode_; - - /** @private {boolean} */ - this.dark_; - - /** @private {!DocumentDimensionsMessageData} */ - this.documentDimensions_; - - // Request translated strings. - chrome.resourcesPrivate.getStrings( - chrome.resourcesPrivate.Component.PDF, - strings => this.handleStrings_(strings)); - // Listen for save commands from the browser. if (chrome.mimeHandlerPrivate && chrome.mimeHandlerPrivate.onSave) { chrome.mimeHandlerPrivate.onSave.addListener(url => this.onSave_(url)); @@ -475,168 +318,57 @@ export class PDFViewer { * @private */ handleKeyEvent_(e) { - const position = this.viewport_.position; - // Certain scroll events may be sent from outside of the extension. - const fromScriptingAPI = e.fromScriptingAPI; - if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented) { return; } this.toolbarManager_.hideToolbarsAfterTimeout(); - const pageUpHandler = () => { - // Go to the previous page if we are fit-to-page or fit-to-height. - if (this.viewport_.isPagedMode()) { - this.viewport_.goToPreviousPage(); - // Since we do the movement of the page. - e.preventDefault(); - } else if (fromScriptingAPI) { - position.y -= this.viewport_.size.height; - this.viewport_.position = position; - } - }; - const pageDownHandler = () => { - // Go to the next page if we are fit-to-page or fit-to-height. - if (this.viewport_.isPagedMode()) { - this.viewport_.goToNextPage(); - // Since we do the movement of the page. - e.preventDefault(); - } else if (fromScriptingAPI) { - position.y += this.viewport_.size.height; - this.viewport_.position = position; - } - }; + // Let the viewport handle directional key events. + if (this.viewport.handleDirectionalKeyEvent(e, this.isFormFieldFocused_)) { + return; + } - switch (e.keyCode) { - case 9: // Tab key. + switch (e.key) { + case 'Tab': this.toolbarManager_.showToolbarsForKeyboardNavigation(); return; - case 27: // Escape key. - if (!this.isPrintPreview_) { - this.toolbarManager_.hideSingleToolbarLayer(); - return; - } - break; // Ensure escape falls through to the print-preview handler. - case 32: // Space key. - if (e.shiftKey) { - pageUpHandler(); - } else { - pageDownHandler(); - } - return; - case 33: // Page up key. - pageUpHandler(); - return; - case 34: // Page down key. - pageDownHandler(); - return; - case 37: // Left arrow key. - if (!hasKeyModifiers(e)) { - // Go to the previous page if there are no horizontal scrollbars and - // no form field is focused. - if (!(this.viewport_.documentHasScrollbars().horizontal || - this.isFormFieldFocused_)) { - this.viewport_.goToPreviousPage(); - // Since we do the movement of the page. - e.preventDefault(); - } else if (fromScriptingAPI) { - position.x -= Viewport.SCROLL_INCREMENT; - this.viewport_.position = position; - } - } + case 'Escape': + this.toolbarManager_.hideSingleToolbarLayer(); return; - case 38: // Up arrow key. - if (fromScriptingAPI) { - position.y -= Viewport.SCROLL_INCREMENT; - this.viewport_.position = position; - } - return; - case 39: // Right arrow key. - if (!hasKeyModifiers(e)) { - // Go to the next page if there are no horizontal scrollbars and no - // form field is focused. - if (!(this.viewport_.documentHasScrollbars().horizontal || - this.isFormFieldFocused_)) { - this.viewport_.goToNextPage(); - // Since we do the movement of the page. - e.preventDefault(); - } else if (fromScriptingAPI) { - position.x += Viewport.SCROLL_INCREMENT; - this.viewport_.position = position; - } - } - return; - case 40: // Down arrow key. - if (fromScriptingAPI) { - position.y += Viewport.SCROLL_INCREMENT; - this.viewport_.position = position; - } - return; - case 65: // 'a' key. + case 'a': if (e.ctrlKey || e.metaKey) { - this.pluginController_.selectAll(); + this.pluginController.selectAll(); // Since we do selection ourselves. e.preventDefault(); } return; - case 71: // 'g' key. - if (this.toolbar_ && (e.ctrlKey || e.metaKey) && e.altKey) { + case 'g': + if (this.toolbarEnabled_ && (e.ctrlKey || e.metaKey) && e.altKey) { this.toolbarManager_.showToolbars(); - this.toolbar_.selectPageNumber(); + this.getToolbar_().selectPageNumber(); } return; - case 219: // Left bracket key. + case '[': if (e.ctrlKey) { - this.rotateCounterclockwise_(); + this.rotateCounterclockwise(); } return; - case 220: // Backslash key. + case '\\': if (e.ctrlKey) { - this.zoomToolbar_.fitToggleFromHotKey(); + this.getZoomToolbar().fitToggleFromHotKey(); } return; - case 221: // Right bracket key. + case ']': if (e.ctrlKey) { - this.rotateClockwise_(); + this.rotateClockwise(); } return; } - // Give print preview a chance to handle the key event. - if (!fromScriptingAPI && this.isPrintPreview_) { - this.sendScriptingMessage_( - {type: 'sendKeyEvent', keyEvent: SerializeKeyEvent(e)}); - } else { - // Show toolbars as a fallback. - if (!(e.shiftKey || e.ctrlKey || e.altKey)) { - this.toolbarManager_.showToolbars(); - } - } - } - - handleMouseEvent_(e) { - if (e.type === 'mousemove') { - this.toolbarManager_.handleMouseMove(e); - } else if (e.type === 'mouseout') { - this.toolbarManager_.hideToolbarsForMouseOut(); - } - } - - /** - * @param {!Event} e The context menu event - * @private - */ - handleContextMenuEvent_(e) { - // Stop Chrome from popping up the context menu on long press. We need to - // make sure the start event did not have 2 touches because we don't want - // to block two finger tap opening the context menu. We check for - // firesTouchEvents in order to not block the context menu on right click. - const capabilities = - /** @type {{ sourceCapabilities: Object }} */ (e).sourceCapabilities; - if (capabilities.firesTouchEvents && - !this.gestureDetector_.wasTwoFingerTouch()) { - e.preventDefault(); + // Show toolbars as a fallback. + if (!(e.shiftKey || e.ctrlKey || e.altKey)) { + this.toolbarManager_.showToolbars(); } } @@ -645,59 +377,61 @@ export class PDFViewer { * @param {!CustomEvent<{value: boolean}>} e * @private */ - async annotationModeToggled_(e) { + async onAnnotationModeToggled_(e) { const annotationMode = e.detail.value; - this.zoomToolbar_.annotationMode = annotationMode; + this.annotationMode_ = annotationMode; if (annotationMode) { // Enter annotation mode. - assert(this.currentController_ === this.pluginController_); + assert(this.currentController === this.pluginController); // TODO(dstockwell): set plugin read-only, begin transition - this.updateProgress_(0); + this.updateProgress(0); // TODO(dstockwell): handle save failure - const saveResult = await this.pluginController_.save(true); - // Data always exists when save is called with requireResult = true. + const saveResult = + await this.pluginController.save(SaveRequestType.ANNOTATION); + // Data always exists when save is called with requestType = ANNOTATION. const result = /** @type {!RequiredSaveResult} */ (saveResult); if (result.hasUnsavedChanges) { assert(!loadTimeData.getBoolean('pdfFormSaveEnabled')); try { - await $('form-warning').show(); + await this.$$('#form-warning').show(); } catch (e) { // The user aborted entering annotation mode. Revert to the plugin. - this.toolbar_.annotationMode = false; - this.zoomToolbar_.annotationMode = false; - this.updateProgress_(100); + this.getToolbar_().annotationMode = false; + this.annotationMode_ = false; + this.updateProgress(100); return; } } PDFMetrics.record(PDFMetrics.UserAction.ENTER_ANNOTATION_MODE); this.hasEnteredAnnotationMode_ = true; // TODO(dstockwell): feed real progress data from the Ink component - this.updateProgress_(50); + this.updateProgress(50); await this.inkController_.load(result.fileName, result.dataToSave); this.inkController_.setAnnotationTool( - assert(this.toolbar_.annotationTool)); - this.currentController_ = this.inkController_; - this.pluginController_.unload(); - this.updateProgress_(100); + assert(this.getToolbar_().annotationTool)); + this.currentController = this.inkController_; + this.pluginController.unload(); + this.updateProgress(100); } else { // Exit annotation mode. PDFMetrics.record(PDFMetrics.UserAction.EXIT_ANNOTATION_MODE); - assert(this.currentController_ === this.inkController_); + assert(this.currentController === this.inkController_); // TODO(dstockwell): set ink read-only, begin transition - this.updateProgress_(0); + this.updateProgress(0); // This runs separately to allow other consumers of `loaded` to queue // up after this task. this.loaded.then(() => { - this.currentController_ = this.pluginController_; + this.currentController = this.pluginController; this.inkController_.unload(); }); // TODO(dstockwell): handle save failure - const saveResult = await this.inkController_.save(true); - // Data always exists when save is called with requireResult = true. + const saveResult = + await this.inkController_.save(SaveRequestType.ANNOTATION); + // Data always exists when save is called with requestType = ANNOTATION. const result = /** @type {!RequiredSaveResult} */ (saveResult); - await this.pluginController_.load(result.fileName, result.dataToSave); + await this.pluginController.load(result.fileName, result.dataToSave); // Ensure the plugin gets the initial viewport. - this.pluginController_.afterZoom(); + this.pluginController.afterZoom(); } } @@ -706,32 +440,21 @@ export class PDFViewer { * @return {Promise<void>} */ async exitAnnotationMode_() { - if (!this.toolbar_.annotationMode) { + if (!this.getToolbar_().annotationMode) { return; } - this.toolbar_.toggleAnnotation(); - this.zoomToolbar_.annotationMode = false; + this.getToolbar_().toggleAnnotation(); + this.annotationMode_ = false; await this.loaded; } - /** - * Request to change the viewport fitting type. - * @param {!CustomEvent<FitToChangedEvent>} e - * @private - */ - fitToChanged_(e) { - if (e.detail.fittingType === FittingType.FIT_TO_PAGE) { - this.viewport_.fitToPage(); - this.toolbarManager_.forceHideTopToolbar(); - } else if (e.detail.fittingType === FittingType.FIT_TO_WIDTH) { - this.viewport_.fitToWidth(); - } else if (e.detail.fittingType === FittingType.FIT_TO_HEIGHT) { - this.viewport_.fitToHeight(); - this.toolbarManager_.forceHideTopToolbar(); - } + /** @override */ + onFitToChanged(e) { + super.onFitToChanged(e); - if (e.detail.userInitiated) { - PDFMetrics.recordFitTo(e.detail.fittingType); + if (e.detail.fittingType === FittingType.FIT_TO_PAGE || + e.detail.fittingType === FittingType.FIT_TO_HEIGHT) { + this.toolbarManager_.forceHideTopToolbar(); } } @@ -741,70 +464,17 @@ export class PDFViewer { * @param {!CustomEvent<!TwoUpViewAction>} e * @private */ - twoUpViewChanged_(e) { - this.currentController_.setTwoUpView( + onTwoUpViewChanged_(e) { + this.currentController.setTwoUpView( e.detail === TwoUpViewAction.TWO_UP_VIEW_ENABLE); this.toolbarManager_.forceHideTopToolbar(); - this.toolbar_.annotationAvailable = + this.getToolbar_().annotationAvailable = (e.detail !== TwoUpViewAction.TWO_UP_VIEW_ENABLE); PDFMetrics.recordTwoUpView(e.detail); } /** - * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has - * finished loading. - * @private - */ - sendDocumentLoadedMessage_() { - if (this.loadState_ === LoadState.LOADING) { - return; - } - if (this.isPrintPreview_ && !this.isPrintPreviewLoadingFinished_) { - return; - } - this.sendScriptingMessage_( - {type: 'documentLoaded', load_state: this.loadState_}); - } - - /** - * Handle open pdf parameters. This function updates the viewport as per - * the parameters mentioned in the url while opening pdf. The order is - * important as later actions can override the effects of previous actions. - * @param {Object} params The open params passed in the URL. - * @private - */ - handleURLParams_(params) { - if (params.zoom) { - this.viewport_.setZoom(params.zoom); - } - - if (params.position) { - this.viewport_.goToPageAndXY( - params.page ? params.page : 0, params.position.x, params.position.y); - } else if (params.page) { - this.viewport_.goToPage(params.page); - } - - if (params.view) { - this.isUserInitiatedEvent_ = false; - this.zoomToolbar_.forceFit(params.view); - if (params.viewPosition) { - const zoomedPositionShift = - params.viewPosition * this.viewport_.getZoom(); - const currentViewportPosition = this.viewport_.position; - if (params.view === FittingType.FIT_TO_WIDTH) { - currentViewportPosition.y += zoomedPositionShift; - } else if (params.view === FittingType.FIT_TO_HEIGHT) { - currentViewportPosition.x += zoomedPositionShift; - } - this.viewport_.position = currentViewportPosition; - } - this.isUserInitiatedEvent_ = true; - } - } - - /** * Moves the viewport to a point in a page. Called back after a * 'transformPagePointReply' is returned from the plugin. * @param {string} origin Identifier for the caller for logging purposes. @@ -814,146 +484,38 @@ export class PDFViewer { * @private */ goToPageAndXY_(origin, page, message) { - this.viewport_.goToPageAndXY(page, message.x, message.y); + this.viewport.goToPageAndXY(page, message.x, message.y); if (origin === 'bookmark') { PDFMetrics.record(PDFMetrics.UserAction.FOLLOW_BOOKMARK); } } - /** - * @return {?Promise} Resolved when the load state reaches LOADED, - * rejects on FAILED. Returns null if no promise has been created, which - * is the case for initial load of the PDF. - */ - get loaded() { - return this.loaded_ ? this.loaded_.promise : null; - } - /** @return {!Viewport} The viewport. Used for testing. */ - get viewport() { - return this.viewport_; - } - /** @return {!Array<!Bookmark>} The bookmarks. Used for testing. */ get bookmarks() { return this.bookmarks_; } - /** - * Updates the load state and triggers completion of the `loaded` - * promise if necessary. - * @param {!LoadState} loadState - * @private - */ - setLoadState_(loadState) { - if (this.loadState_ === loadState) { - return; - } - assert( - loadState === LoadState.LOADING || - this.loadState_ === LoadState.LOADING); - this.loadState_ = loadState; - if (!this.initialLoadComplete_) { - this.initialLoadComplete_ = true; - return; - } - if (loadState === LoadState.SUCCESS) { - this.loaded_.resolve(); - } else if (loadState === LoadState.FAILED) { - this.loaded_.reject(); - } else { - this.loaded_ = new PromiseResolver(); - } - } - - /** - * Update the loading progress of the document in response to a progress - * message being received from the content controller. - * @param {number} progress the progress as a percentage. - * @private - */ - updateProgress_(progress) { - if (this.toolbar_) { - this.toolbar_.loadProgress = progress; - } - - if (progress === -1) { - // Document load failed. - this.errorScreen_.show(); - this.sizer_.style.display = 'none'; - if (this.passwordScreen_ && this.passwordScreen_.active) { - this.passwordScreen_.deny(); - this.passwordScreen_.close(); - } - this.setLoadState_(LoadState.FAILED); - this.isPrintPreviewLoadingFinished_ = true; - this.sendDocumentLoadedMessage_(); - } else if (progress === 100) { - // Document load complete. - if (this.lastViewportPosition_) { - this.viewport_.position = this.lastViewportPosition_; + /** @override */ + setLoadState(loadState) { + super.setLoadState(loadState); + if (loadState === LoadState.FAILED) { + const passwordScreen = this.$$('#password-screen'); + if (passwordScreen && passwordScreen.active) { + passwordScreen.deny(); + passwordScreen.close(); } - this.paramsParser_.getViewportFromUrlParams( - this.originalUrl_, params => this.handleURLParams_(params)); - this.setLoadState_(LoadState.SUCCESS); - this.sendDocumentLoadedMessage_(); - while (this.delayedScriptingMessages_.length > 0) { - this.handleScriptingMessage(this.delayedScriptingMessages_.shift()); - } - - this.toolbarManager_.hideToolbarsAfterTimeout(); - } else { - this.setLoadState_(LoadState.LOADING); } } - /** @private */ - sendBackgroundColorForPrintPreview_() { - this.pluginController_.backgroundColorChanged( - this.dark_ ? PDFViewer.PRINT_PREVIEW_DARK_BACKGROUND_COLOR : - PDFViewer.PRINT_PREVIEW_BACKGROUND_COLOR); - } - - /** - * Load a dictionary of translated strings into the UI. Used as a callback for - * chrome.resourcesPrivate. - * @param {Object} strings Dictionary of translated strings - * @private - */ - handleStrings_(strings) { - const stringsDictionary = - /** @type {{ textdirection: string, language: string }} */ (strings); - document.documentElement.dir = stringsDictionary.textdirection; - document.documentElement.lang = stringsDictionary.language; - - loadTimeData.data = strings; - - // Predefined zoom factors to be used when zooming in/out. These are in - // ascending order. - const presetZoomFactors = /** @type {!Array<number>} */ ( - JSON.parse(loadTimeData.getString('presetZoomFactors'))); - this.viewport_.setZoomFactorRange(presetZoomFactors); - - if (this.isPrintPreview_) { - this.sendBackgroundColorForPrintPreview_(); - } else { - $('toolbar').strings = strings; - $('toolbar').pdfAnnotationsEnabled = - loadTimeData.getBoolean('pdfAnnotationsEnabled'); - $('toolbar').printingEnabled = loadTimeData.getBoolean('printingEnabled'); - } - $('zoom-toolbar').setStrings(strings); - $('zoom-toolbar').twoUpViewEnabled = - loadTimeData.getBoolean('pdfTwoUpViewEnabled') && !this.isPrintPreview_; - // Display the zoom toolbar after the UI text direction is set, to ensure it - // appears on the correct side of the PDF viewer. - $('zoom-toolbar').hidden = false; - if (this.passwordScreen_) { - $('password-screen').strings = strings; + /** @override */ + updateProgress(progress) { + if (this.toolbarEnabled_) { + this.getToolbar_().loadProgress = progress; } - $('error-screen').strings = strings; - if ($('form-warning')) { - $('form-warning').strings = strings; + super.updateProgress(progress); + if (progress === 100) { + this.toolbarManager_.hideToolbarsAfterTimeout(); } } @@ -964,73 +526,14 @@ export class PDFViewer { * @private */ onPasswordSubmitted_(event) { - this.pluginController_.getPasswordComplete(event.detail.password); - } - - /** - * A callback that sets |isUserInitiatedEvent_| to |userInitiated|. - * @param {boolean} userInitiated The value to set |isUserInitiatedEvent_| to. - * @private - */ - setUserInitiated_(userInitiated) { - assert(this.isUserInitiatedEvent_ !== userInitiated); - this.isUserInitiatedEvent_ = userInitiated; - } - - /** - * A callback that's called when an update to a pinch zoom is detected. - * @param {!Object} e the pinch event. - * @private - */ - onPinchUpdate_(e) { - // Throttle number of pinch events to one per frame. - if (!this.sentPinchEvent_) { - this.sentPinchEvent_ = true; - window.requestAnimationFrame(() => { - this.sentPinchEvent_ = false; - this.viewport_.pinchZoom(e); - }); - } - } - - /** - * A callback that's called when the end of a pinch zoom is detected. - * @param {!Object} e the pinch event. - * @private - */ - onPinchEnd_(e) { - // Using rAF for pinch end prevents pinch updates scheduled by rAF getting - // sent after the pinch end. - window.requestAnimationFrame(() => { - this.viewport_.pinchZoomEnd(e); - }); + this.pluginController.getPasswordComplete(event.detail.password); } - /** - * A callback that's called when the start of a pinch zoom is detected. - * @param {!Object} e the pinch event. - * @private - */ - onPinchStart_(e) { - // We also use rAF for pinch start, so that if there is a pinch end event - // scheduled by rAF, this pinch start will be sent after. - window.requestAnimationFrame(() => { - this.viewport_.pinchZoomStart(e); - }); - } - - /** - * A callback that's called after the viewport changes. - * @private - */ - viewportChanged_() { - if (!this.documentDimensions_) { - return; - } - + /** @override */ + updateUIForViewportChange() { // Offset the toolbar position so that it doesn't move if scrollbars appear. - const hasScrollbars = this.viewport_.documentHasScrollbars(); - const scrollbarWidth = this.viewport_.scrollbarWidth; + const hasScrollbars = this.viewport.documentHasScrollbars(); + const scrollbarWidth = this.viewport.scrollbarWidth; const verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0; const horizontalScrollbarWidth = hasScrollbars.horizontal ? scrollbarWidth : 0; @@ -1039,217 +542,70 @@ export class PDFViewer { // gives a compromise: if there is no scrollbar visible then the toolbar // will be half a scrollbar width further left than the spec but if there // is a scrollbar visible it will be half a scrollbar width further right - // than the spec. In RTL layout normally, and in LTR layout in Print Preview - // when the NewPrintPreview flag is enabled, the zoom toolbar is on the left + // than the spec. In RTL layout normally, the zoom toolbar is on the left // left side, but the scrollbar is still on the right, so this is not // necessary. - if (isRTL() === this.isPrintPreview_) { - this.zoomToolbar_.style.right = + const zoomToolbar = this.getZoomToolbar(); + if (!isRTL()) { + zoomToolbar.style.right = -verticalScrollbarWidth + (scrollbarWidth / 2) + 'px'; } // Having a horizontal scrollbar is much rarer so we don't offset the // toolbar from the bottom any more than what the spec says. This means // that when there is a scrollbar visible, it will be a full scrollbar // width closer to the bottom of the screen than usual, but this is ok. - this.zoomToolbar_.style.bottom = -horizontalScrollbarWidth + 'px'; + zoomToolbar.style.bottom = -horizontalScrollbarWidth + 'px'; // Update the page indicator. - const visiblePage = this.viewport_.getMostVisiblePage(); - - if (this.toolbar_) { - this.toolbar_.pageNo = visiblePage + 1; - } - - // TODO(raymes): Give pageIndicator_ the same API as toolbar_. - if (this.pageIndicator_) { - const lastIndex = this.pageIndicator_.index; - this.pageIndicator_.index = visiblePage; - if (this.documentDimensions_.pageDimensions.length > 1 && - hasScrollbars.vertical && lastIndex !== undefined) { - this.pageIndicator_.style.visibility = 'visible'; - } else { - this.pageIndicator_.style.visibility = 'hidden'; - } + const visiblePage = this.viewport.getMostVisiblePage(); + if (this.toolbarEnabled_) { + this.getToolbar_().pageNo = visiblePage + 1; } - this.currentController_.viewportChanged(); - - const visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage); - const size = this.viewport_.size; - this.sendScriptingMessage_({ - type: 'viewport', - pageX: visiblePageDimensions.x, - pageY: visiblePageDimensions.y, - pageWidth: visiblePageDimensions.width, - viewportWidth: size.width, - viewportHeight: size.height - }); + this.currentController.viewportChanged(); } - /** - * Handle a scripting message from outside the extension (typically sent by - * PDFScriptingAPI in a page containing the extension) to interact with the - * plugin. - * @param {!MessageObject} message The message to handle. - */ + /** @override */ handleScriptingMessage(message) { - if (this.parentWindow_ !== message.source) { - this.parentWindow_ = message.source; - this.parentOrigin_ = message.origin; - // Ensure that we notify the embedder if the document is loaded. - if (this.loadState_ !== LoadState.LOADING) { - this.sendDocumentLoadedMessage_(); - } - } + super.handleScriptingMessage(message); - if (this.handlePrintPreviewScriptingMessage_(message)) { - return; - } - - // Delay scripting messages from users of the scripting API until the - // document is loaded. This simplifies use of the APIs. - if (this.loadState_ !== LoadState.SUCCESS) { - this.delayedScriptingMessages_.push(message); + if (this.delayScriptingMessage(message)) { return; } switch (message.data.type.toString()) { case 'getSelectedText': - this.pluginController_.getSelectedText(); + this.pluginController.getSelectedText(); break; case 'print': - this.pluginController_.print(); + this.pluginController.print(); break; case 'selectAll': - this.pluginController_.selectAll(); + this.pluginController.selectAll(); break; } } - /** - * Handle scripting messages specific to print preview. - * @param {!MessageObject} message the message to handle. - * @return {boolean} true if the message was handled, false otherwise. - * @private - */ - handlePrintPreviewScriptingMessage_(message) { - if (!this.isPrintPreview_) { - return false; - } - - let messageData = message.data; - switch (messageData.type.toString()) { - case 'loadPreviewPage': - messageData = - /** @type {{ url: string, index: number }} */ (messageData); - this.pluginController_.loadPreviewPage( - messageData.url, messageData.index); - return true; - case 'resetPrintPreviewMode': - messageData = /** @type {!PrintPreviewParams} */ (messageData); - this.setLoadState_(LoadState.LOADING); - if (!this.inPrintPreviewMode_) { - this.inPrintPreviewMode_ = true; - this.isUserInitiatedEvent_ = false; - this.zoomToolbar_.forceFit(FittingType.FIT_TO_PAGE); - this.isUserInitiatedEvent_ = true; - } - - // Stash the scroll location so that it can be restored when the new - // document is loaded. - this.lastViewportPosition_ = this.viewport_.position; - - // TODO(raymes): Disable these properly in the plugin. - const printButton = $('print-button'); - if (printButton) { - printButton.parentNode.removeChild(printButton); - } - const saveButton = $('save-button'); - if (saveButton) { - saveButton.parentNode.removeChild(saveButton); - } - - this.pageIndicator_.pageLabels = messageData.pageNumbers; - - this.pluginController_.resetPrintPreviewMode(messageData); - return true; - case 'sendKeyEvent': - this.handleKeyEvent_(/** @type {!KeyboardEvent} */ (DeserializeKeyEvent( - /** @type {{ keyEvent: Object }} */ (message.data).keyEvent))); - return true; - case 'hideToolbars': - this.toolbarManager_.resetKeyboardNavigationAndHideToolbars(); - return true; - case 'darkModeChanged': - this.dark_ = /** @type {{darkMode: boolean}} */ (message.data).darkMode; - if (this.isPrintPreview_) { - this.sendBackgroundColorForPrintPreview_(); - } - return true; - case 'scrollPosition': - const position = this.viewport_.position; - messageData = /** @type {{ x: number, y: number }} */ (message.data); - position.y += messageData.y; - position.x += messageData.x; - this.viewport_.position = position; - return true; - } - - return false; - } - - /** - * Send a scripting message outside the extension (typically to - * PDFScriptingAPI in a page containing the extension). - * @param {Object} message the message to send. - * @private - */ - sendScriptingMessage_(message) { - if (this.parentWindow_ && this.parentOrigin_) { - let targetOrigin; - // Only send data back to the embedder if it is from the same origin, - // unless we're sending it to ourselves (which could happen in the case - // of tests). We also allow documentLoaded messages through as this won't - // leak important information. - if (this.parentOrigin_ === window.location.origin) { - targetOrigin = this.parentOrigin_; - } else if (message.type === 'documentLoaded') { - targetOrigin = '*'; - } else { - targetOrigin = this.originalUrl_; - } - try { - this.parentWindow_.postMessage(message, targetOrigin); - } catch (ok) { - // TODO(crbug.com/1004425): targetOrigin probably was rejected, such as - // a "data:" URL. This shouldn't cause this method to throw, though. - } - } - } - - /** - * @param {!CustomEvent<MessageData>} e - * @private - */ - handlePluginMessage_(e) { + /** @override */ + handlePluginMessage(e) { const data = e.detail; switch (data.type.toString()) { case 'beep': this.handleBeep_(); return; case 'documentDimensions': - this.setDocumentDimensions_( + this.setDocumentDimensions( /** @type {!DocumentDimensionsMessageData} */ (data)); return; case 'getPassword': this.handlePasswordRequest_(); return; case 'getSelectedTextReply': - this.handleSelectedTextReply_( + this.handleSelectedTextReply( /** @type {{ selectedText: string }} */ (data).selectedText); return; case 'loadProgress': - this.updateProgress_( + this.updateProgress( /** @type {{ progress: number }} */ (data).progress); return; case 'navigate': @@ -1258,52 +614,56 @@ export class PDFViewer { return; case 'navigateToDestination': const destinationData = /** @type {!DestinationMessageData} */ (data); - this.handleNavigateToDestination_( + this.viewport.handleNavigateToDestination( destinationData.page, destinationData.x, destinationData.y, destinationData.zoom); return; - case 'printPreviewLoaded': - this.handlePrintPreviewLoaded_(); - return; case 'metadata': const metadata = /** @type {!MetadataMessageData} */ (data); this.setDocumentMetadata_( metadata.title, metadata.bookmarks, metadata.canSerializeDocument); return; + case 'setIsEditing': + // Editing mode can only be entered once, and cannot be exited. + this.hasEdits_ = true; + return; case 'setIsSelecting': - this.setIsSelecting_( + this.viewportScroller.setEnableScrolling( /** @type {{ isSelecting: boolean }} */ (data).isSelecting); return; case 'getNamedDestinationReply': - this.paramsParser_.onNamedDestinationReceived( + this.paramsParser.onNamedDestinationReceived( /** @type {{ pageNumber: number }} */ (data).pageNumber); return; case 'formFocusChange': this.isFormFieldFocused_ = /** @type {{ focused: boolean }} */ (data).focused; return; + case 'touchSelectionOccurred': + this.sendScriptingMessage({ + type: 'touchSelectionOccurred', + }); + return; + case 'documentFocusChanged': + // TODO(crbug.com/1069370): Draw a focus rect around plugin. + return; } assertNotReached('Unknown message type received: ' + data.type); } - /** - * Sets document dimensions from the current controller. - * @param {!DocumentDimensionsMessageData} documentDimensions - * @private - */ - setDocumentDimensions_(documentDimensions) { - this.documentDimensions_ = documentDimensions; - this.isUserInitiatedEvent_ = false; - this.viewport_.setDocumentDimensions(this.documentDimensions_); - this.isUserInitiatedEvent_ = true; + /** @override */ + setDocumentDimensions(documentDimensions) { + super.setDocumentDimensions(documentDimensions); // If we received the document dimensions, the password was good so we // can dismiss the password screen. - if (this.passwordScreen_ && this.passwordScreen_.active) { - this.passwordScreen_.close(); + const passwordScreen = this.$$('#password-screen'); + if (passwordScreen && passwordScreen.active) { + passwordScreen.close(); } - if (this.toolbar_) { - this.toolbar_.docLength = this.documentDimensions_.pageDimensions.length; + if (this.toolbarEnabled_) { + this.getToolbar_().docLength = + this.documentDimensions.pageDimensions.length; } } @@ -1323,74 +683,24 @@ export class PDFViewer { handlePasswordRequest_() { // If the password screen isn't up, put it up. Otherwise we're // responding to an incorrect password so deny it. - assert(!!this.passwordScreen_); - if (!this.passwordScreen_.active) { + const passwordScreen = this.$$('#password-screen'); + assert(passwordScreen); + if (!passwordScreen.active) { this.hadPassword_ = true; - this.updateAnnotationAvailable_(); - this.passwordScreen_.show(); + passwordScreen.show(); } else { - this.passwordScreen_.deny(); + passwordScreen.deny(); } } /** - * Handles a selected text reply from the current controller. - * @param {string} selectedText - * @private - */ - handleSelectedTextReply_(selectedText) { - this.sendScriptingMessage_({ - type: 'getSelectedTextReply', - selectedText: selectedText, - }); - } - - /** * Handles a navigation request from the current controller. * @param {string} url * @param {!PdfNavigator.WindowOpenDisposition} disposition * @private */ handleNavigate_(url, disposition) { - // If in print preview, always open a new tab. - if (this.isPrintPreview_) { - this.navigator_.navigate( - url, PdfNavigator.WindowOpenDisposition.NEW_BACKGROUND_TAB); - } else { - this.navigator_.navigate(url, disposition); - } - } - - /** - * Handles an internal navigation request to a destination from the current - * controller. - * - * @param {number} page - * @param {number} x - * @param {number} y - * @param {number} zoom - * @private - */ - handleNavigateToDestination_(page, x, y, zoom) { - if (zoom) { - this.viewport_.setZoom(zoom); - } - - if (x || y) { - this.viewport_.goToPageAndXY(page, x ? x : 0, y ? y : 0); - } else { - this.viewport_.goToPage(page); - } - } - - /** - * Handles a notification that print preview has loaded from the - * current controller. - * @private - */ - handlePrintPreviewLoaded_() { - this.isPrintPreviewLoadingFinished_ = true; - this.sendDocumentLoadedMessage_(); + this.navigator_.navigate(url, disposition); } /** @@ -1401,27 +711,10 @@ export class PDFViewer { * @private */ setDocumentMetadata_(title, bookmarks, canSerializeDocument) { - if (title) { - document.title = title; - } else { - document.title = getFilenameFromURL(this.originalUrl_); - } + this.title_ = title ? title : getFilenameFromURL(this.originalUrl); + document.title = this.title_; this.bookmarks_ = bookmarks; - if (this.toolbar_) { - this.toolbar_.docTitle = document.title; - this.toolbar_.bookmarks = this.bookmarks_; - } this.canSerializeDocument_ = canSerializeDocument; - this.updateAnnotationAvailable_(); - } - - /** - * Sets the is selecting flag from the current controller. - * @param {boolean} isSelecting - * @private - */ - setIsSelecting_(isSelecting) { - this.viewportScroller_.setEnableScrolling(isSelecting); } /** @@ -1431,28 +724,58 @@ export class PDFViewer { * @private */ async onSave_(streamUrl) { - if (streamUrl !== this.browserApi_.getStreamInfo().streamUrl) { + if (streamUrl !== this.browserApi.getStreamInfo().streamUrl) { return; } - this.save_(); + let saveMode; + if (this.hasEnteredAnnotationMode_) { + saveMode = SaveRequestType.ANNOTATION; + } else if ( + loadTimeData.getBoolean('pdfFormSaveEnabled') && this.hasEdits_) { + saveMode = SaveRequestType.EDITED; + } else { + saveMode = SaveRequestType.ORIGINAL; + } + + this.save_(saveMode); + } + + /** + * @param {!CustomEvent<!SaveRequestType>} e + * @private + */ + onToolbarSave_(e) { + this.save_(e.detail); } /** * Saves the current PDF document to disk. + * @param {SaveRequestType} requestType The type of save request. * @private */ - async save_() { + async save_(requestType) { PDFMetrics.record(PDFMetrics.UserAction.SAVE); - if (this.hasEnteredAnnotationMode_) { + // If we have entered annotation mode we must require the local + // contents to ensure annotations are saved, unless the user specifically + // requested the original document. Otherwise we would save the cached + // remote copy without annotations. + if (requestType === SaveRequestType.ANNOTATION) { PDFMetrics.record(PDFMetrics.UserAction.SAVE_WITH_ANNOTATION); } - // If we have entered annotation mode we must require the local - // contents to ensure annotations are saved. Otherwise we would - // save the cached or remote copy without annotatios. - const requireResult = this.hasEnteredAnnotationMode_; + // Always send requests of type ORIGINAL to the plugin controller, not the + // ink controller. The ink controller always saves the edited document. // TODO(dstockwell): Report an error to user if this fails. - const result = await this.currentController_.save(requireResult); + let result; + if (requestType !== SaveRequestType.ORIGINAL || !this.annotationMode_) { + result = await this.currentController.save(requestType); + } else { + // Request type original in annotation mode --> need to exit annotation + // mode before saving. See https://crbug.com/919364. + await this.exitAnnotationMode_(); + assert(!this.annotationMode_); + result = await this.currentController.save(SaveRequestType.ORIGINAL); + } if (result == null) { // The content controller handled the save internally. return; @@ -1465,7 +788,12 @@ export class PDFViewer { } chrome.fileSystem.chooseEntry( - {type: 'saveFile', suggestedName: fileName}, entry => { + { + type: 'saveFile', + accepts: [{description: '*.pdf', extensions: ['pdf']}], + suggestedName: fileName + }, + entry => { if (chrome.runtime.lastError) { if (chrome.runtime.lastError.message !== 'User cancelled') { console.log( @@ -1488,97 +816,83 @@ export class PDFViewer { } /** @private */ - async print_() { + async onPrint_() { PDFMetrics.record(PDFMetrics.UserAction.PRINT); await this.exitAnnotationMode_(); - this.currentController_.print(); + this.currentController.print(); } /** * Updates the toolbar's annotation available flag depending on current * conditions. + * @return {boolean} Whether annotations are available. * @private */ - updateAnnotationAvailable_() { - if (!this.toolbar_) { - return; - } - let annotationAvailable = true; - if (this.viewport_.getClockwiseRotations() !== 0) { - annotationAvailable = false; - } - if (this.hadPassword_) { - annotationAvailable = false; - } - if (!this.canSerializeDocument_) { - annotationAvailable = false; - } - this.toolbar_.annotationAvailable = annotationAvailable; + computeAnnotationAvailable_() { + return this.canSerializeDocument_ && !this.rotated_ && !this.hadPassword_; } /** @private */ - rotateClockwise_() { - PDFMetrics.record(PDFMetrics.UserAction.ROTATE); - this.viewport_.rotateClockwise(); - this.currentController_.rotateClockwise(); - this.updateAnnotationAvailable_(); + onUndo_() { + this.currentController.undo(); } /** @private */ - rotateCounterclockwise_() { - PDFMetrics.record(PDFMetrics.UserAction.ROTATE); - this.viewport_.rotateCounterclockwise(); - this.currentController_.rotateCounterclockwise(); - this.updateAnnotationAvailable_(); + onRedo_() { + this.currentController.redo(); + } + + /** + * @param {!CustomEvent<{value: AnnotationTool}>} e + * @private + */ + onAnnotationToolChanged_(e) { + this.inkController_.setAnnotationTool(e.detail.value); } + // <if expr="chromeos"> /** * @param {!CustomEvent<{canUndo: boolean, canRedo: boolean}>} e * @private */ setAnnotationUndoState_(e) { - this.toolbar_.canUndoAnnotation = e.detail.canUndo; - this.toolbar_.canRedoAnnotation = e.detail.canRedo; + this.getToolbar_().canUndoAnnotation = e.detail.canUndo; + this.getToolbar_().canRedoAnnotation = e.detail.canRedo; } + // </if> - /** @private */ - resetTrackers_() { - this.viewport_.resetTracker(); - if (this.tracker_) { - this.tracker_.removeAll(); - } + /** @override */ + rotateClockwise() { + super.rotateClockwise(); + this.rotated_ = this.viewport.getClockwiseRotations() !== 0; } -} -// Export on |window| such that scripts injected from pdf_extension_test.cc can -// access it. -window.PDFViewer = PDFViewer; + /** @override */ + rotateCounterclockwise() { + super.rotateCounterclockwise(); + this.rotated_ = this.viewport.getClockwiseRotations() !== 0; + } +} /** * The height of the toolbar along the top of the page. The document will be * shifted down by this much in the viewport. + * @type {number} */ -PDFViewer.MATERIAL_TOOLBAR_HEIGHT = 56; +const MATERIAL_TOOLBAR_HEIGHT = 56; /** * Minimum height for the material toolbar to show (px). Should match the media * query in index-material.css. If the window is smaller than this at load, * leave no space for the toolbar. + * @type {number} */ -PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT = 250; - -/** - * The background color used for print preview (--google-grey-refresh-300). - */ -PDFViewer.PRINT_PREVIEW_BACKGROUND_COLOR = '0xFFDADCE0'; - -/** - * The background color used for print preview when dark mode is enabled - * (--google-grey-refresh-700). - */ -PDFViewer.PRINT_PREVIEW_DARK_BACKGROUND_COLOR = '0xFF5F6368'; +const TOOLBAR_WINDOW_MIN_HEIGHT = 250; /** * The background color used for the regular viewer. + * @type {string} */ -PDFViewer.BACKGROUND_COLOR = '0xFF525659'; +const BACKGROUND_COLOR = '0xFF525659'; + +customElements.define(PDFViewerElement.is, PDFViewerElement); diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js new file mode 100644 index 00000000000..bb0c9a91d12 --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_base.js @@ -0,0 +1,640 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js'; +import {EventTracker} from 'chrome://resources/js/event_tracker.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; +import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js'; +import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +import {BrowserApi} from './browser_api.js'; +import {FittingType} from './constants.js'; +import {ContentController, MessageData, PluginController} from './controller.js'; +import {FitToChangedEvent} from './elements/viewer-zoom-toolbar.js'; +import {PDFMetrics} from './metrics.js'; +import {OpenPdfParamsParser} from './open_pdf_params_parser.js'; +import {LoadState} from './pdf_scripting_api.js'; +import {DocumentDimensionsMessageData, MessageObject} from './pdf_viewer_utils.js'; +import {Point, Viewport} from './viewport.js'; +import {ViewportScroller} from './viewport_scroller.js'; +import {ZoomManager} from './zoom_manager.js'; + +/** @return {number} Width of a scrollbar in pixels */ +function getScrollbarWidth() { + const 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); + const result = div.offsetWidth - div.clientWidth; + div.parentNode.removeChild(div); + return result; +} + +export class PDFViewerBaseElement extends PolymerElement { + static get is() { + return 'pdf-viewer-base'; + } + + static get template() { + return null; + } + + static get properties() { + return { + strings: Object, + }; + } + + constructor() { + super(); + + /** @protected {Object|undefined} */ + this.strings = undefined; + + /** @protected {?BrowserApi} */ + this.browserApi = null; + + /** @protected {?ContentController} */ + this.currentController = null; + + /** @protected {string} */ + this.originalUrl = ''; + + /** @protected {!EventTracker} */ + this.tracker = new EventTracker(); + + /** @protected {boolean} */ + this.isUserInitiatedEvent = true; + + /** @protected {?Point} */ + this.lastViewportPosition = null; + + /** @protected {?OpenPdfParamsParser} */ + this.paramsParser = null; + + /** @protected {?ViewportScroller} */ + this.viewportScroller = null; + + /** @protected {?DocumentDimensionsMessageData} */ + this.documentDimensions = null; + + /** @private {boolean} */ + this.overrideSendScriptingMessageForTest_ = false; + + /** @private {!LoadState} */ + this.loadState_ = LoadState.LOADING; + + /** @private {?Object} */ + this.parentWindow_ = null; + + /** @private {?string} */ + this.parentOrigin_ = null; + + /** @private {!Array} */ + this.delayedScriptingMessages_ = []; + + /** @private {?PromiseResolver} */ + this.loaded_ = null; + + /** @private {boolean} */ + this.initialLoadComplete_ = false; + + /** @private {?Viewport} */ + this.viewport_ = null; + + /** @private {?PluginController} */ + this.pluginController_ = null; + + /** @private {?HTMLEmbedElement} */ + this.plugin_ = null; + + /** @private {?ZoomManager} */ + this.zoomManager_ = null; + } + + /** @return {number} The height of the top toolbar */ + getToolbarHeight() { + return 0; + } + + /** + * @return {!HTMLDivElement} + * @protected + */ + getContent() {} + + /** + * @return {!HTMLDivElement} + * @protected + */ + getSizer() {} + + /** + * @return {!ViewerZoomToolbarElement} + * @protected + */ + getZoomToolbar() {} + + /** + * @return {!ViewerErrorScreenElement} + * @protected + */ + getErrorScreen() {} + + /** + * @param {string} query + * @return {?Element} + * @protected + */ + $$(query) { + return this.shadowRoot.querySelector(query); + } + + /** @return {string} */ + getBackgroundColor() { + return ''; + } + + /** + * @return {!HTMLEmbedElement} The plugin + * @private + */ + createPlugin_() { + // Create the plugin object dynamically so we can set its src. The plugin + // element is sized to fill the entire window and is set to be fixed + // positioning, acting as a viewport. The plugin renders into this viewport + // according to the scroll position of the window. + const plugin = + /** @type {!HTMLEmbedElement} */ (document.createElement('embed')); + + // NOTE: The plugin's 'id' field must be set to 'plugin' since + // ChromePrintRenderFrameHelperDeleage::GetPdfElement() in + // chrome/renderer/printing/chrome_print_render_frame_helper_delegate.cc + // actually references it. + plugin.id = 'plugin'; + plugin.type = 'application/x-google-chrome-pdf'; + + plugin.setAttribute('src', this.originalUrl); + plugin.setAttribute( + 'stream-url', this.browserApi.getStreamInfo().streamUrl); + let headers = ''; + for (const header in this.browserApi.getStreamInfo().responseHeaders) { + headers += header + ': ' + + this.browserApi.getStreamInfo().responseHeaders[header] + '\n'; + } + plugin.setAttribute('headers', headers); + + plugin.setAttribute('background-color', this.getBackgroundColor()); + plugin.setAttribute('top-toolbar-height', this.getToolbarHeight()); + + const javascript = this.browserApi.getStreamInfo().javascript || 'block'; + plugin.setAttribute('javascript', javascript); + + if (this.browserApi.getStreamInfo().embedded) { + plugin.setAttribute( + 'top-level-url', this.browserApi.getStreamInfo().tabUrl); + } else { + plugin.toggleAttribute('full-frame', true); + } + + return plugin; + } + + /** @param {!BrowserApi} browserApi */ + init(browserApi) { + this.browserApi = browserApi; + this.originalUrl = this.browserApi.getStreamInfo().originalUrl; + + PDFMetrics.record(PDFMetrics.UserAction.DOCUMENT_OPENED); + + // Parse open pdf parameters. + this.paramsParser = new OpenPdfParamsParser( + destination => this.pluginController_.getNamedDestination(destination)); + + // Can only reload if we are in a normal tab. + if (chrome.tabs && this.browserApi.getStreamInfo().tabId !== -1) { + this.getErrorScreen().reloadFn = () => { + chrome.tabs.reload(this.browserApi.getStreamInfo().tabId); + }; + } + + // Create the viewport. + const defaultZoom = + this.browserApi.getZoomBehavior() === BrowserApi.ZoomBehavior.MANAGE ? + this.browserApi.getDefaultZoom() : + 1.0; + this.viewport_ = new Viewport( + window, this.getSizer(), this.getContent(), getScrollbarWidth(), + defaultZoom, this.getToolbarHeight()); + this.viewport_.setViewportChangedCallback(() => this.viewportChanged_()); + this.viewport_.setBeforeZoomCallback( + () => this.currentController.beforeZoom()); + this.viewport_.setAfterZoomCallback( + () => this.currentController.afterZoom()); + this.viewport_.setUserInitiatedCallback( + userInitiated => this.setUserInitiated_(userInitiated)); + window.addEventListener('beforeunload', () => this.resetTrackers_()); + + // Handle scripting messages from outside the extension that wish to + // interact with it. We also send a message indicating that extension has + // loaded and is ready to receive messages. + window.addEventListener('message', message => { + this.handleScriptingMessage(/** @type {!MessageObject} */ (message)); + }, false); + + // Create the plugin. + this.plugin_ = this.createPlugin_(); + this.getContent().appendChild(this.plugin_); + this.pluginController_ = new PluginController( + this.plugin_, this.viewport_, () => this.isUserInitiatedEvent, + () => this.loaded); + this.currentController = this.pluginController_; + this.tracker.add( + this.pluginController_.getEventTarget(), 'plugin-message', + e => this.handlePluginMessage(e)); + + document.body.addEventListener('change-page-and-xy', e => { + const point = this.viewport_.convertPageToScreen(e.detail.page, e.detail); + this.viewport_.goToPageAndXY(e.detail.page, point.x, point.y); + }); + + // Set up the ZoomManager. + this.zoomManager_ = ZoomManager.create( + this.browserApi.getZoomBehavior(), () => this.viewport_.getZoom(), + zoom => this.browserApi.setZoom(zoom), + this.browserApi.getInitialZoom()); + this.viewport_.setZoomManager(assert(this.zoomManager_)); + this.browserApi.addZoomEventListener( + zoom => this.zoomManager_.onBrowserZoomChange(zoom)); + + this.viewportScroller = + new ViewportScroller(this.viewport_, this.plugin_, window); + + // Request translated strings. + chrome.resourcesPrivate.getStrings( + chrome.resourcesPrivate.Component.PDF, + strings => this.handleStrings(strings)); + } + + /** + * Update the loading progress of the document in response to a progress + * message being received from the content controller. + * @param {number} progress the progress as a percentage. + */ + updateProgress(progress) { + if (progress === -1) { + // Document load failed. + this.getErrorScreen().show(); + this.getSizer().style.display = 'none'; + this.setLoadState(LoadState.FAILED); + this.sendDocumentLoadedMessage(); + } else if (progress === 100) { + // Document load complete. + if (this.lastViewportPosition) { + this.viewport_.position = this.lastViewportPosition; + } + this.paramsParser.getViewportFromUrlParams( + this.originalUrl, params => this.handleURLParams_(params)); + this.setLoadState(LoadState.SUCCESS); + this.sendDocumentLoadedMessage(); + while (this.delayedScriptingMessages_.length > 0) { + this.handleScriptingMessage(this.delayedScriptingMessages_.shift()); + } + } else { + this.setLoadState(LoadState.LOADING); + } + } + + /** @return {boolean} Whether the documentLoaded message can be sent. */ + readyToSendLoadMessage() { + return true; + } + + /** + * Sends a 'documentLoaded' message to the PDFScriptingAPI if the document has + * finished loading. + */ + sendDocumentLoadedMessage() { + if (this.loadState_ === LoadState.LOADING || + !this.readyToSendLoadMessage()) { + return; + } + this.sendScriptingMessage( + {type: 'documentLoaded', load_state: this.loadState_}); + } + + /** + * Called to update the UI before sending the viewport scripting message. + * Should be overridden by subclasses. + * @protected + */ + updateUIForViewportChange() {} + + /** + * A callback that's called after the viewport changes. + * @private + */ + viewportChanged_() { + if (!this.documentDimensions) { + return; + } + + this.updateUIForViewportChange(); + + const visiblePage = this.viewport_.getMostVisiblePage(); + const visiblePageDimensions = this.viewport_.getPageScreenRect(visiblePage); + const size = this.viewport_.size; + this.sendScriptingMessage({ + type: 'viewport', + pageX: visiblePageDimensions.x, + pageY: visiblePageDimensions.y, + pageWidth: visiblePageDimensions.width, + viewportWidth: size.width, + viewportHeight: size.height + }); + } + + /** + * Handle a scripting message from outside the extension (typically sent by + * PDFScriptingAPI in a page containing the extension) to interact with the + * plugin. + * @param {!MessageObject} message The message to handle. + */ + handleScriptingMessage(message) { + if (this.parentWindow_ !== message.source) { + this.parentWindow_ = message.source; + this.parentOrigin_ = message.origin; + // Ensure that we notify the embedder if the document is loaded. + if (this.loadState_ !== LoadState.LOADING) { + this.sendDocumentLoadedMessage(); + } + } + } + + /** + * @param {!MessageObject} message The message to handle. + * @return {boolean} Whether the message was delayed and added to the queue. + */ + delayScriptingMessage(message) { + // Delay scripting messages from users of the scripting API until the + // document is loaded. This simplifies use of the APIs. + if (this.loadState_ !== LoadState.SUCCESS) { + this.delayedScriptingMessages_.push(message); + return true; + } + return false; + } + + /** + * @param {!CustomEvent<MessageData>} e + * @protected + */ + handlePluginMessage(e) {} + + /** + * Sets document dimensions from the current controller. + * @param {!DocumentDimensionsMessageData} documentDimensions + * @protected + */ + setDocumentDimensions(documentDimensions) { + this.documentDimensions = documentDimensions; + this.isUserInitiatedEvent = false; + this.viewport_.setDocumentDimensions(this.documentDimensions); + this.isUserInitiatedEvent = true; + } + + /** + * @return {?Promise} Resolved when the load state reaches LOADED, + * rejects on FAILED. Returns null if no promise has been created, which + * is the case for initial load of the PDF. + */ + get loaded() { + return this.loaded_ ? this.loaded_.promise : null; + } + + /** @return {!Viewport} */ + get viewport() { + return assert(this.viewport_); + } + + /** + * @return {!PluginController} + * @protected + */ + get pluginController() { + return assert(this.pluginController_); + } + + /** + * Updates the load state and triggers completion of the `loaded` + * promise if necessary. + * @param {!LoadState} loadState + * @protected + */ + setLoadState(loadState) { + if (this.loadState_ === loadState) { + return; + } + assert( + loadState === LoadState.LOADING || + this.loadState_ === LoadState.LOADING); + this.loadState_ = loadState; + if (!this.initialLoadComplete_) { + this.initialLoadComplete_ = true; + return; + } + if (loadState === LoadState.SUCCESS) { + this.loaded_.resolve(); + } else if (loadState === LoadState.FAILED) { + this.loaded_.reject(); + } else { + this.loaded_ = new PromiseResolver(); + } + } + + /** + * Load a dictionary of translated strings into the UI. Used as a callback for + * chrome.resourcesPrivate. + * @param {?Object} strings Dictionary of translated strings + * @protected + */ + handleStrings(strings) { + if (!strings) { + return; + } + loadTimeData.data = strings; + + // Predefined zoom factors to be used when zooming in/out. These are in + // ascending order. + const presetZoomFactors = /** @type {!Array<number>} */ ( + JSON.parse(loadTimeData.getString('presetZoomFactors'))); + this.viewport_.setZoomFactorRange(presetZoomFactors); + + this.strings = strings; + + // Display the zoom toolbar after the UI text direction is set, to ensure it + // appears on the correct side of the PDF viewer. + this.getZoomToolbar().hidden = false; + } + + /** + * Handle open pdf parameters. This function updates the viewport as per + * the parameters mentioned in the url while opening pdf. The order is + * important as later actions can override the effects of previous actions. + * @param {Object} params The open params passed in the URL. + * @private + */ + handleURLParams_(params) { + if (params.zoom) { + this.viewport_.setZoom(params.zoom); + } + + if (params.position) { + this.viewport_.goToPageAndXY( + params.page ? params.page : 0, params.position.x, params.position.y); + } else if (params.page) { + this.viewport_.goToPage(params.page); + } + + if (params.view) { + this.isUserInitiatedEvent = false; + this.getZoomToolbar().forceFit(params.view); + if (params.viewPosition) { + const zoomedPositionShift = + params.viewPosition * this.viewport_.getZoom(); + const currentViewportPosition = this.viewport_.position; + if (params.view === FittingType.FIT_TO_WIDTH) { + currentViewportPosition.y += zoomedPositionShift; + } else if (params.view === FittingType.FIT_TO_HEIGHT) { + currentViewportPosition.x += zoomedPositionShift; + } + this.viewport_.position = currentViewportPosition; + } + this.isUserInitiatedEvent = true; + } + } + + /** + * A callback that sets |isUserInitiatedEvent| to |userInitiated|. + * @param {boolean} userInitiated The value to set |isUserInitiatedEvent| to. + * @private + */ + setUserInitiated_(userInitiated) { + assert(this.isUserInitiatedEvent !== userInitiated); + this.isUserInitiatedEvent = userInitiated; + } + + overrideSendScriptingMessageForTest() { + this.overrideSendScriptingMessageForTest_ = true; + } + + /** + * Send a scripting message outside the extension (typically to + * PDFScriptingAPI in a page containing the extension). + * @param {Object} message the message to send. + * @protected + */ + sendScriptingMessage(message) { + if (this.parentWindow_ && this.parentOrigin_) { + let targetOrigin; + // Only send data back to the embedder if it is from the same origin, + // unless we're sending it to ourselves (which could happen in the case + // of tests). We also allow documentLoaded messages through as this won't + // leak important information. + if (this.parentOrigin_ === window.location.origin) { + targetOrigin = this.parentOrigin_; + } else if (message.type === 'documentLoaded') { + targetOrigin = '*'; + } else { + targetOrigin = this.originalUrl; + } + try { + this.parentWindow_.postMessage(message, targetOrigin); + } catch (ok) { + // TODO(crbug.com/1004425): targetOrigin probably was rejected, such as + // a "data:" URL. This shouldn't cause this method to throw, though. + } + } + } + + /** + * Request to change the viewport fitting type. + * @param {!CustomEvent<FitToChangedEvent>} e + * @protected + */ + onFitToChanged(e) { + if (e.detail.fittingType === FittingType.FIT_TO_PAGE) { + this.viewport_.fitToPage(); + } else if (e.detail.fittingType === FittingType.FIT_TO_WIDTH) { + this.viewport_.fitToWidth(); + } else if (e.detail.fittingType === FittingType.FIT_TO_HEIGHT) { + this.viewport_.fitToHeight(); + } + + if (e.detail.userInitiated) { + PDFMetrics.recordFitTo(e.detail.fittingType); + } + } + + /** @protected */ + onZoomIn() { + this.viewport_.zoomIn(); + PDFMetrics.recordZoomAction(/*isZoomIn=*/ true); + } + + /** @protected */ + onZoomOut() { + this.viewport_.zoomOut(); + PDFMetrics.recordZoomAction(/*isZoomIn=*/ false); + } + + /** + * Handles a selected text reply from the current controller. + * @param {string} selectedText + * @protected + */ + handleSelectedTextReply(selectedText) { + const message = { + type: 'getSelectedTextReply', + selectedText: selectedText, + }; + if (this.overrideSendScriptingMessageForTest_) { + this.overrideSendScriptingMessageForTest_ = false; + try { + this.sendScriptingMessage(message); + } finally { + this.parentWindow_.postMessage('flush', '*'); + } + return; + } + this.sendScriptingMessage(message); + } + + /** @protected */ + rotateClockwise() { + PDFMetrics.record(PDFMetrics.UserAction.ROTATE); + this.viewport_.rotateClockwise(); + this.currentController.rotateClockwise(); + } + + /** @protected */ + rotateCounterclockwise() { + PDFMetrics.record(PDFMetrics.UserAction.ROTATE); + this.viewport_.rotateCounterclockwise(); + this.currentController.rotateCounterclockwise(); + } + + /** @private */ + resetTrackers_() { + this.viewport_.resetTracker(); + if (this.tracker) { + this.tracker.removeAll(); + } + } +} + +customElements.define(PDFViewerBaseElement.is, PDFViewerBaseElement); diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.html b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.html new file mode 100644 index 00000000000..dac20fda069 --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.html @@ -0,0 +1,21 @@ +<style include="pdf-viewer-shared-style"> + viewer-page-indicator { + opacity: 0; + visibility: hidden; + z-index: 2; + } +</style> + +<div id="sizer"></div> + +<viewer-zoom-toolbar id="zoom-toolbar" strings="[[strings]]" + on-fit-to-changed="onFitToChanged" is-print-preview + on-zoom-in="onZoomIn" on-zoom-out="onZoomOut" + hidden> +</viewer-zoom-toolbar> + +<viewer-error-screen id="error-screen"></viewer-error-screen> + +<viewer-page-indicator id="page-indicator"></viewer-page-indicator> + +<div id="content"></div> diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js new file mode 100644 index 00000000000..6fa9a6f3e3c --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_pp.js @@ -0,0 +1,379 @@ +// Copyright 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import './elements/viewer-error-screen.js'; +import './elements/viewer-page-indicator.js'; +import './elements/shared-vars.js'; +import './pdf_viewer_shared_style.js'; + +import {assertNotReached} from 'chrome://resources/js/assert.m.js'; +import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js'; +import {isRTL} from 'chrome://resources/js/util.m.js'; +import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +import {BrowserApi} from './browser_api.js'; +import {FittingType} from './constants.js'; +import {MessageData, PrintPreviewParams} from './controller.js'; +import {DeserializeKeyEvent, LoadState, SerializeKeyEvent} from './pdf_scripting_api.js'; +import {PDFViewerBaseElement} from './pdf_viewer_base.js'; +import {DestinationMessageData, DocumentDimensionsMessageData, MessageObject, shouldIgnoreKeyEvents} from './pdf_viewer_utils.js'; +import {ToolbarManager} from './toolbar_manager.js'; + +class PDFViewerPPElement extends PDFViewerBaseElement { + static get is() { + return 'pdf-viewer-pp'; + } + + static get template() { + return html`{__html_template__}`; + } + + constructor() { + super(); + + /** @private {boolean} */ + this.isPrintPreviewLoadingFinished_ = false; + + /** @private {boolean} */ + this.inPrintPreviewMode_ = false; + + /** @private {boolean} */ + this.dark_ = false; + + /** @private {?ToolbarManager} */ + this.toolbarManager_ = null; + } + + /** @override */ + getContent() { + return /** @type {!HTMLDivElement} */ (this.$$('#content')); + } + + /** @override */ + getSizer() { + return /** @type {!HTMLDivElement} */ (this.$$('#sizer')); + } + + /** @override */ + getZoomToolbar() { + return /** @type {!ViewerZoomToolbarElement} */ (this.$$('#zoom-toolbar')); + } + + /** @override */ + getErrorScreen() { + return /** @type {!ViewerErrorScreenElement} */ (this.$$('#error-screen')); + } + + /** @override */ + getBackgroundColor() { + return PRINT_PREVIEW_BACKGROUND_COLOR; + } + + /** @param {!BrowserApi} browserApi */ + init(browserApi) { + super.init(browserApi); + + this.toolbarManager_ = + new ToolbarManager(window, null, this.getZoomToolbar()); + + // Setup the keyboard event listener. + document.addEventListener( + 'keydown', + e => this.handleKeyEvent_(/** @type {!KeyboardEvent} */ (e))); + } + + /** + * Handle key events. These may come from the user directly or via the + * scripting API. + * @param {!KeyboardEvent} e the event to handle. + * @private + */ + handleKeyEvent_(e) { + if (shouldIgnoreKeyEvents(document.activeElement) || e.defaultPrevented) { + return; + } + + this.toolbarManager_.hideToolbarsAfterTimeout(); + // Let the viewport handle directional key events. + if (this.viewport.handleDirectionalKeyEvent(e, false)) { + return; + } + + switch (e.key) { + case 'Tab': + this.toolbarManager_.showToolbarsForKeyboardNavigation(); + return; + case 'Escape': + break; // Ensure escape falls through to the print-preview handler. + case 'a': + if (e.ctrlKey || e.metaKey) { + this.pluginController.selectAll(); + // Since we do selection ourselves. + e.preventDefault(); + } + return; + case '[': + if (e.ctrlKey) { + this.rotateCounterclockwise(); + } + return; + case '\\': + if (e.ctrlKey) { + this.getZoomToolbar().fitToggleFromHotKey(); + } + return; + case ']': + if (e.ctrlKey) { + this.rotateClockwise(); + } + return; + } + + // Give print preview a chance to handle the key event. + if (!e.fromScriptingAPI) { + this.sendScriptingMessage( + {type: 'sendKeyEvent', keyEvent: SerializeKeyEvent(e)}); + } else { + // Show toolbars as a fallback. + if (!(e.shiftKey || e.ctrlKey || e.altKey)) { + this.toolbarManager_.showToolbars(); + } + } + } + + /** @private */ + sendBackgroundColorForPrintPreview_() { + this.pluginController.backgroundColorChanged( + this.dark_ ? PRINT_PREVIEW_DARK_BACKGROUND_COLOR : + PRINT_PREVIEW_BACKGROUND_COLOR); + } + + /** @override */ + updateUIForViewportChange() { + // Offset the toolbar position so that it doesn't move if scrollbars appear. + const hasScrollbars = this.viewport.documentHasScrollbars(); + const scrollbarWidth = this.viewport.scrollbarWidth; + const verticalScrollbarWidth = hasScrollbars.vertical ? scrollbarWidth : 0; + const horizontalScrollbarWidth = + hasScrollbars.horizontal ? scrollbarWidth : 0; + + // Shift the zoom toolbar to the left by half a scrollbar width. This + // gives a compromise: if there is no scrollbar visible then the toolbar + // will be half a scrollbar width further left than the spec but if there + // is a scrollbar visible it will be half a scrollbar width further right + // than the spec. In LTR layout, the zoom toolbar is on the left + // left side, but the scrollbar is still on the right, so this is not + // necessary. + const zoomToolbar = this.getZoomToolbar(); + if (isRTL()) { + zoomToolbar.style.right = + -verticalScrollbarWidth + (scrollbarWidth / 2) + 'px'; + } + // Having a horizontal scrollbar is much rarer so we don't offset the + // toolbar from the bottom any more than what the spec says. This means + // that when there is a scrollbar visible, it will be a full scrollbar + // width closer to the bottom of the screen than usual, but this is ok. + zoomToolbar.style.bottom = -horizontalScrollbarWidth + 'px'; + + // Update the page indicator. + const visiblePage = this.viewport.getMostVisiblePage(); + const pageIndicator = this.$$('#page-indicator'); + const lastIndex = pageIndicator.index; + pageIndicator.index = visiblePage; + if (this.documentDimensions.pageDimensions.length > 1 && + hasScrollbars.vertical && lastIndex !== undefined) { + pageIndicator.style.visibility = 'visible'; + } else { + pageIndicator.style.visibility = 'hidden'; + } + + this.pluginController.viewportChanged(); + } + + /** @override */ + handleScriptingMessage(message) { + super.handleScriptingMessage(message); + + if (this.handlePrintPreviewScriptingMessage_(message)) { + return; + } + + if (this.delayScriptingMessage(message)) { + return; + } + + switch (message.data.type.toString()) { + case 'getSelectedText': + this.pluginController.getSelectedText(); + break; + case 'selectAll': + this.pluginController.selectAll(); + break; + } + } + + /** + * Handle scripting messages specific to print preview. + * @param {!MessageObject} message the message to handle. + * @return {boolean} true if the message was handled, false otherwise. + * @private + */ + handlePrintPreviewScriptingMessage_(message) { + let messageData = message.data; + switch (messageData.type.toString()) { + case 'loadPreviewPage': + messageData = + /** @type {{ url: string, index: number }} */ (messageData); + this.pluginController.loadPreviewPage( + messageData.url, messageData.index); + return true; + case 'resetPrintPreviewMode': + messageData = /** @type {!PrintPreviewParams} */ (messageData); + this.setLoadState(LoadState.LOADING); + if (!this.inPrintPreviewMode_) { + this.inPrintPreviewMode_ = true; + this.isUserInitiatedEvent = false; + this.getZoomToolbar().forceFit(FittingType.FIT_TO_PAGE); + this.isUserInitiatedEvent = true; + } + + // Stash the scroll location so that it can be restored when the new + // document is loaded. + this.lastViewportPosition = this.viewport.position; + this.$$('#page-indicator').pageLabels = messageData.pageNumbers; + + this.pluginController.resetPrintPreviewMode(messageData); + return true; + case 'sendKeyEvent': + this.handleKeyEvent_(/** @type {!KeyboardEvent} */ (DeserializeKeyEvent( + /** @type {{ keyEvent: Object }} */ (message.data).keyEvent))); + return true; + case 'hideToolbars': + this.toolbarManager_.resetKeyboardNavigationAndHideToolbars(); + return true; + case 'darkModeChanged': + this.dark_ = /** @type {{darkMode: boolean}} */ (message.data).darkMode; + this.sendBackgroundColorForPrintPreview_(); + return true; + case 'scrollPosition': + const position = this.viewport.position; + messageData = /** @type {{ x: number, y: number }} */ (message.data); + position.y += messageData.y; + position.x += messageData.x; + this.viewport.position = position; + return true; + } + + return false; + } + + /** @override */ + setLoadState(loadState) { + super.setLoadState(loadState); + if (loadState === LoadState.FAILED) { + this.isPrintPreviewLoadingFinished_ = true; + } + } + + /** @override */ + handlePluginMessage(e) { + const data = e.detail; + switch (data.type.toString()) { + case 'documentDimensions': + this.setDocumentDimensions( + /** @type {!DocumentDimensionsMessageData} */ (data)); + return; + case 'getSelectedTextReply': + this.handleSelectedTextReply( + /** @type {{ selectedText: string }} */ (data).selectedText); + return; + case 'loadProgress': + this.updateProgress( + /** @type {{ progress: number }} */ (data).progress); + return; + case 'navigateToDestination': + const destinationData = /** @type {!DestinationMessageData} */ (data); + this.viewport.handleNavigateToDestination( + destinationData.page, destinationData.x, destinationData.y, + destinationData.zoom); + return; + case 'printPreviewLoaded': + this.handlePrintPreviewLoaded_(); + return; + case 'setIsSelecting': + this.viewportScroller.setEnableScrolling( + /** @type {{ isSelecting: boolean }} */ (data).isSelecting); + return; + case 'getNamedDestinationReply': + this.paramsParser.onNamedDestinationReceived( + /** @type {{ pageNumber: number }} */ (data).pageNumber); + return; + case 'touchSelectionOccurred': + this.sendScriptingMessage({ + type: 'touchSelectionOccurred', + }); + return; + case 'documentFocusChanged': + // TODO(crbug.com/1069370): Draw a focus rect around plugin. + return; + case 'beep': + case 'formFocusChange': + case 'getPassword': + case 'metadata': + case 'navigate': + case 'setIsEditing': + // These messages are not relevant in Print Preview. + return; + } + assertNotReached('Unknown message type received: ' + data.type); + } + + /** + * Handles a notification that print preview has loaded from the + * current controller. + * @private + */ + handlePrintPreviewLoaded_() { + this.isPrintPreviewLoadingFinished_ = true; + this.sendDocumentLoadedMessage(); + } + + /** @override */ + readyToSendLoadMessage() { + return this.isPrintPreviewLoadingFinished_; + } + + /** @override */ + handleStrings(strings) { + super.handleStrings(strings); + + if (!strings) { + return; + } + this.sendBackgroundColorForPrintPreview_(); + } + + /** @override */ + updateProgress(progress) { + super.updateProgress(progress); + if (progress === 100) { + this.toolbarManager_.hideToolbarsAfterTimeout(); + } + } +} + +/** + * The background color used for print preview (--google-grey-refresh-300). + * @type {string} + */ +const PRINT_PREVIEW_BACKGROUND_COLOR = '0xFFDADCE0'; + +/** + * The background color used for print preview when dark mode is enabled + * (--google-grey-refresh-700). + * @type {string} + */ +const PRINT_PREVIEW_DARK_BACKGROUND_COLOR = '0xFF5F6368'; + +customElements.define(PDFViewerPPElement.is, PDFViewerPPElement); diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.html b/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.html new file mode 100644 index 00000000000..ba2a5e4cf9b --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.html @@ -0,0 +1,33 @@ +<template> + <style> + #content { + height: 100%; + position: fixed; + width: 100%; + z-index: 1; + } + + #plugin { + height: 100%; + position: absolute; + width: 100%; + } + + #sizer { + position: absolute; + z-index: 0; + } + + @media(max-height: 200px) { + viewer-zoom-toolbar { + display: none; + } + } + + @media(max-width: 300px) { + viewer-zoom-toolbar { + display: none; + } + } + </style> +</template> diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.js new file mode 100644 index 00000000000..d7411e11a3a --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_shared_style.js @@ -0,0 +1,11 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; + +const template = document.createElement('template'); +template.innerHTML = ` +<dom-module id="pdf-viewer-shared-style">{__html_template__}</dom-module> +`; +document.body.appendChild(template.content.cloneNode(true)); diff --git a/chromium/chrome/browser/resources/pdf/pdf_viewer_utils.js b/chromium/chrome/browser/resources/pdf/pdf_viewer_utils.js new file mode 100644 index 00000000000..2250aa80637 --- /dev/null +++ b/chromium/chrome/browser/resources/pdf/pdf_viewer_utils.js @@ -0,0 +1,64 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {MessageData} from './controller.js'; +import {LayoutOptions} from './viewport.js'; + +/** + * @typedef {{ + * source: Object, + * origin: string, + * data: !MessageData, + * }} + */ +export let MessageObject; + +/** + * @typedef {{ + * type: string, + * height: number, + * width: number, + * layoutOptions: (!LayoutOptions|undefined), + * pageDimensions: Array + * }} + */ +export let DocumentDimensionsMessageData; + +/** + * @typedef {{ + * type: string, + * page: number, + * x: number, + * y: number, + * zoom: number + * }} + */ +export let DestinationMessageData; + +/** + * @typedef {{ + * hasUnsavedChanges: (boolean|undefined), + * fileName: string, + * dataToSave: !ArrayBuffer + * }} + */ +export let RequiredSaveResult; + +/** + * Whether keydown events should currently be ignored. Events are ignored when + * an editable element has focus, to allow for proper editing controls. + * @param {Element} activeElement The currently selected DOM node. + * @return {boolean} True if keydown events should be ignored. + */ +export function shouldIgnoreKeyEvents(activeElement) { + while (activeElement.shadowRoot != null && + activeElement.shadowRoot.activeElement != null) { + activeElement = activeElement.shadowRoot.activeElement; + } + + return ( + activeElement.isContentEditable || + (activeElement.tagName === 'INPUT' && activeElement.type !== 'radio') || + activeElement.tagName === 'TEXTAREA'); +} diff --git a/chromium/chrome/browser/resources/pdf/toolbar_manager.js b/chromium/chrome/browser/resources/pdf/toolbar_manager.js index 847b21172fc..f4b7aade0b4 100644 --- a/chromium/chrome/browser/resources/pdf/toolbar_manager.js +++ b/chromium/chrome/browser/resources/pdf/toolbar_manager.js @@ -4,20 +4,42 @@ import {isRTL} from 'chrome://resources/js/util.m.js'; -/** Idle time in ms before the UI is hidden. */ +/** + * Idle time in ms before the UI is hidden. + * @type {number} + */ const HIDE_TIMEOUT = 2000; -/** Time in ms after force hide before toolbar is shown again. */ + +/** + * Time in ms after force hide before toolbar is shown again. + * @type {number} + */ const FORCE_HIDE_TIMEOUT = 1000; + /** * Velocity required in a mousemove to reveal the UI (pixels/ms). This is * intended to be high enough that a fast flick of the mouse is required to * reach it. + * @type {number} */ const SHOW_VELOCITY = 10; -/** Distance from the top of the screen required to reveal the toolbars. */ + +/** + * Distance from the top of the screen required to reveal the toolbars. + * @type {number} + */ const TOP_TOOLBAR_REVEAL_DISTANCE = 100; -/** Distance from the bottom-right of the screen required to reveal toolbars. */ + +/** + * Distance from right of the screen required to reveal toolbars. + * @type {number} + */ const SIDE_TOOLBAR_REVEAL_DISTANCE_RIGHT = 150; + +/** + * Distance from bottom of the screen required to reveal toolbars. + * @type {number} + */ const SIDE_TOOLBAR_REVEAL_DISTANCE_BOTTOM = 250; /** @@ -45,7 +67,7 @@ function isMouseNearSideToolbar(e, window, reverse) { return atSide && atBottom; } -/** Responsible for co-ordinating between multiple toolbar elements. */ +// Responsible for co-ordinating between multiple toolbar elements. export class ToolbarManager { /** * @param {!Window} window The window containing the UI. @@ -89,6 +111,12 @@ export class ToolbarManager { this.window_.addEventListener('resize', this.resizeDropdowns_.bind(this)); this.resizeDropdowns_(); + document.addEventListener( + 'mousemove', + e => this.handleMouseMove_(/** @type {!MouseEvent} */ (e))); + document.addEventListener( + 'mouseout', () => this.hideToolbarsForMouseOut_()); + if (this.isPrintPreview_) { this.zoomToolbar_.addEventListener('keyboard-navigation-active', e => { this.keyboardNavigationActive = e.detail; @@ -96,8 +124,11 @@ export class ToolbarManager { } } - /** @param {!MouseEvent} e */ - handleMouseMove(e) { + /** + * @param {!MouseEvent} e + * @private + */ + handleMouseMove_(e) { this.isMouseNearTopToolbar_ = !!this.toolbar_ && isMouseNearTopToolbar(e); this.isMouseNearSideToolbar_ = isMouseNearSideToolbar(e, this.window_, this.isPrintPreview_); @@ -192,8 +223,9 @@ export class ToolbarManager { /** * Hide toolbars after a delay, regardless of the position of the mouse. * Intended to be called when the mouse has moved out of the parent window. + * @private */ - hideToolbarsForMouseOut() { + hideToolbarsForMouseOut_() { this.isMouseNearTopToolbar_ = false; this.isMouseNearSideToolbar_ = false; this.hideToolbarsAfterTimeout(); diff --git a/chromium/chrome/browser/resources/pdf/viewport.js b/chromium/chrome/browser/resources/pdf/viewport.js index 37967176729..3e68d39f673 100644 --- a/chromium/chrome/browser/resources/pdf/viewport.js +++ b/chromium/chrome/browser/resources/pdf/viewport.js @@ -4,9 +4,10 @@ import {assert} from 'chrome://resources/js/assert.m.js'; import {EventTracker} from 'chrome://resources/js/event_tracker.m.js'; -import {$} from 'chrome://resources/js/util.m.js'; +import {$, hasKeyModifiers} from 'chrome://resources/js/util.m.js'; import {FittingType} from './constants.js'; +import {GestureDetector, PinchEventDetail} from './gesture_detector.js'; import {InactiveZoomManager, ZoomManager} from './zoom_manager.js'; /** @@ -66,35 +67,29 @@ function vectorDelta(p1, p2) { return {x: p2.x - p1.x, y: p2.y - p1.y}; } -/** - * @param {!Point} coordinateInFrame - * @return {!Point} Coordinate converted to plugin coordinates. - */ -function frameToPluginCoordinate(coordinateInFrame) { - const container = $('plugin'); - return { - x: coordinateInFrame.x - container.getBoundingClientRect().left, - y: coordinateInFrame.y - container.getBoundingClientRect().top - }; -} - export class Viewport { /** * @param {!Window} window * @param {!HTMLDivElement} sizer The element which represents the size of the * document in the viewport + * @param {!HTMLDivElement} content The element which is the parent of the + * plugin in the viewer. * @param {number} scrollbarWidth The width of scrollbars on the page * @param {number} defaultZoom The default zoom level. * @param {number} topToolbarHeight The number of pixels that should initially * be left blank above the document for the toolbar. */ - constructor(window, sizer, scrollbarWidth, defaultZoom, topToolbarHeight) { + constructor( + window, sizer, content, scrollbarWidth, defaultZoom, topToolbarHeight) { /** @private {!Window} */ this.window_ = window; /** @private {!HTMLDivElement} */ this.sizer_ = sizer; + /** @private {!HTMLDivElement} */ + this.content_ = content; + /** @private {number} */ this.scrollbarWidth_ = scrollbarWidth; @@ -168,11 +163,33 @@ export class Viewport { /** @private {!EventTracker} */ this.tracker_ = new EventTracker(); + /** @private {!GestureDetector} */ + this.gestureDetector_ = new GestureDetector(this.content_); + + /** @private {boolean} */ + this.sentPinchEvent_ = false; + + this.gestureDetector_.getEventTarget().addEventListener( + 'pinchstart', + e => this.onPinchStart_( + /** @type {!CustomEvent<!PinchEventDetail>} */ (e))); + this.gestureDetector_.getEventTarget().addEventListener( + 'pinchupdate', + e => this.onPinchUpdate_( + /** @type {!CustomEvent<!PinchEventDetail>} */ (e))); + this.gestureDetector_.getEventTarget().addEventListener( + 'pinchend', + e => this.onPinchEnd_( + /** @type {!CustomEvent<!PinchEventDetail>} */ (e))); + // Set to a default zoom manager - used in tests. this.setZoomManager(new InactiveZoomManager(this.getZoom.bind(this), 1)); window.addEventListener('scroll', this.updateViewport_.bind(this)); window.addEventListener('resize', this.resizeWrapper_.bind(this)); + + document.body.addEventListener( + 'change-zoom', e => this.setZoom(e.detail.zoom)); } /** @param {function():void} viewportChangedCallback */ @@ -288,8 +305,8 @@ export class Viewport { y: point.y * pointsToPixels, }); return { - x: result.x + Viewport.PAGE_SHADOW.left, - y: result.y + Viewport.PAGE_SHADOW.top, + x: result.x + PAGE_SHADOW.left, + y: result.y + PAGE_SHADOW.top, }; } @@ -348,9 +365,9 @@ export class Viewport { * @param {number} zoom Zoom to compute scrollbars for * @return {{horizontal: boolean, vertical: boolean}} Whether horizontal or * vertical scrollbars are needed. - * @private + * Public so tests can call it directly. */ - documentNeedsScrollbars_(zoom) { + documentNeedsScrollbars(zoom) { const zoomedDimensions = this.getZoomedDocumentDimensions_(zoom); if (!zoomedDimensions) { return {horizontal: false, vertical: false}; @@ -376,7 +393,7 @@ export class Viewport { * vertical scrollbars are needed. */ documentHasScrollbars() { - return this.documentNeedsScrollbars_(this.getZoom()); + return this.documentNeedsScrollbars(this.getZoom()); } /** @@ -394,6 +411,19 @@ export class Viewport { } /** + * @param {!Point} coordinateInFrame + * @return {!Point} Coordinate converted to plugin coordinates. + * @private + */ + frameToPluginCoordinate_(coordinateInFrame) { + const container = this.content_.querySelector('#plugin'); + return { + x: coordinateInFrame.x - container.getBoundingClientRect().left, + y: coordinateInFrame.y - container.getBoundingClientRect().top + }; + } + + /** * Called when the viewport should be updated. * @private */ @@ -447,7 +477,7 @@ export class Viewport { /** @return {!Size} the size of the viewport excluding scrollbars. */ get size() { - const needsScrollbars = this.documentNeedsScrollbars_(this.getZoom()); + const needsScrollbars = this.documentNeedsScrollbars(this.getZoom()); const scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0; const scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0; @@ -589,9 +619,7 @@ export class Viewport { }; } - /** - * @param {number} newZoom The zoom level to zoom to. - */ + /** @param {number} newZoom The zoom level to zoom to. */ setZoom(newZoom) { this.fittingType_ = FittingType.NONE; this.mightZoom_(() => { @@ -775,7 +803,7 @@ export class Viewport { pageDimensions.width, pageDimensions.height); // Check if there needs to be any scrollbars. - const needsScrollbars = this.documentNeedsScrollbars_(zoom); + const needsScrollbars = this.documentNeedsScrollbars(zoom); // If the document fits, just return the zoom. if (!needsScrollbars.horizontal && !needsScrollbars.vertical) { @@ -992,84 +1020,152 @@ export class Viewport { } /** - * Pinch zoom event handler. - * @param {!Object} e The pinch event. + * @param {!KeyboardEvent} e + * @private */ - pinchZoom(e) { - this.mightZoom_(() => { - this.pinchPhase_ = e.direction === 'out' ? - Viewport.PinchPhase.PINCH_UPDATE_ZOOM_OUT : - Viewport.PinchPhase.PINCH_UPDATE_ZOOM_IN; - - const scaleDelta = e.startScaleRatio / this.prevScale_; - if (this.firstPinchCenterInFrame_ != null) { - this.pinchPanVector_ = - vectorDelta(e.center, this.firstPinchCenterInFrame_); - } + pageUpHandler_(e) { + // Go to the previous page if we are fit-to-page or fit-to-height. + if (this.isPagedMode_()) { + this.goToPreviousPage(); + // Since we do the movement of the page. + e.preventDefault(); + } else if ( + /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e) + .fromScriptingAPI) { + this.position.y -= this.size.height; + } + } - const needsScrollbars = - this.documentNeedsScrollbars_(this.zoomManager_.applyBrowserZoom( - this.clampZoom_(this.internalZoom_ * scaleDelta))); - - this.pinchCenter_ = e.center; - - // If there's no horizontal scrolling, keep the content centered so the - // user can't zoom in on the non-content area. - // TODO(mcnee) Investigate other ways of scaling when we don't have - // horizontal scrolling. We want to keep the document centered, - // but this causes a potentially awkward transition when we start - // using the gesture center. - if (!needsScrollbars.horizontal) { - this.pinchCenter_ = { - x: this.window_.innerWidth / 2, - y: this.window_.innerHeight / 2 - }; - } else if (this.keepContentCentered_) { - this.oldCenterInContent_ = - this.frameToContent_(frameToPluginCoordinate(e.center)); - this.keepContentCentered_ = false; - } + /** + * @param {!KeyboardEvent} e + * @private + */ + pageDownHandler_(e) { + // Go to the next page if we are fit-to-page or fit-to-height. + if (this.isPagedMode_()) { + this.goToNextPage(); + // Since we do the movement of the page. + e.preventDefault(); + } else if ( + /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e) + .fromScriptingAPI) { + this.position.y += this.size.height; + } + } - this.setPinchZoomInternal_(scaleDelta, frameToPluginCoordinate(e.center)); - this.updateViewport_(); - this.prevScale_ = e.startScaleRatio; - }); + /** + * @param {!KeyboardEvent} e + * @param {boolean} formFieldFocused + * @private + */ + arrowLeftHandler_(e, formFieldFocused) { + if (hasKeyModifiers(e)) { + return; + } + + // Go to the previous page if there are no horizontal scrollbars and + // no form field is focused. + if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) { + this.goToPreviousPage(); + // Since we do the movement of the page. + e.preventDefault(); + } else if ( + /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e) + .fromScriptingAPI) { + this.position.x -= SCROLL_INCREMENT; + } } - /** @param {!Object} e The pinch event. */ - pinchZoomStart(e) { - this.pinchPhase_ = Viewport.PinchPhase.PINCH_START; - this.prevScale_ = 1; - this.oldCenterInContent_ = - this.frameToContent_(frameToPluginCoordinate(e.center)); + /** + * @param {!KeyboardEvent} e + * @param {boolean} formFieldFocused + * @private + */ + arrowRightHandler_(e, formFieldFocused) { + if (hasKeyModifiers(e)) { + return; + } - const needsScrollbars = this.documentNeedsScrollbars_(this.getZoom()); - this.keepContentCentered_ = !needsScrollbars.horizontal; - // We keep track of begining of the pinch. - // By doing so we will be able to compute the pan distance. - this.firstPinchCenterInFrame_ = e.center; + // Go to the next page if there are no horizontal scrollbars and no + // form field is focused. + if (!(this.documentHasScrollbars().horizontal || formFieldFocused)) { + this.goToNextPage(); + // Since we do the movement of the page. + e.preventDefault(); + } else if ( + /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e) + .fromScriptingAPI) { + this.position.x += SCROLL_INCREMENT; + } } - /** @param {!Object} e The pinch event. */ - pinchZoomEnd(e) { - this.mightZoom_(() => { - this.pinchPhase_ = Viewport.PinchPhase.PINCH_END; - const scaleDelta = e.startScaleRatio / this.prevScale_; - this.pinchCenter_ = /** @type {!Point} */ (e.center); + /** + * @param {boolean} fromScriptingAPI + * @private + */ + arrowDownHandler_(fromScriptingAPI) { + if (fromScriptingAPI) { + this.position.y += SCROLL_INCREMENT; + } + } - this.setPinchZoomInternal_(scaleDelta, frameToPluginCoordinate(e.center)); - this.updateViewport_(); - }); + /** + * @param {boolean} fromScriptingAPI + * @private + */ + arrowUpHandler_(fromScriptingAPI) { + if (fromScriptingAPI) { + this.position.y -= SCROLL_INCREMENT; + } + } - this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; - this.pinchPanVector_ = null; - this.pinchCenter_ = null; - this.firstPinchCenterInFrame_ = null; + /** + * Handle certain directional key events. + * @param {!KeyboardEvent} e the event to handle. + * @param {boolean} formFieldFocused Whether a form field is currently + * focused. + * @return {boolean} Whether the event was handled. + */ + handleDirectionalKeyEvent(e, formFieldFocused) { + // Certain scroll events may be sent from outside of the extension. + const fromScriptingAPI = + /** @type {!{fromScriptingAPI: (boolean|undefined)}} */ (e) + .fromScriptingAPI; + + switch (e.key) { + case '': + if (e.shiftKey) { + this.pageUpHandler_(e); + } else { + this.pageDownHandler_(e); + } + return true; + case 'PageUp': + this.pageUpHandler_(e); + return true; + case 'PageDown': + this.pageDownHandler_(e); + return true; + case 'ArrowLeft': + this.arrowLeftHandler_(e, formFieldFocused); + return true; + case 'ArrowUp': + this.arrowUpHandler_(!!fromScriptingAPI); + return true; + case 'ArrowRight': + this.arrowRightHandler_(e, formFieldFocused); + return true; + case 'ArrowDown': + this.arrowDownHandler_(!!fromScriptingAPI); + return true; + default: + return false; + } } /** * Go to the next page. If the document is in two-up view, go to the left page - * of the next row. + * of the next row. Public for tests. */ goToNextPage() { const currentPage = this.getMostVisiblePage(); @@ -1080,7 +1176,7 @@ export class Viewport { /** * Go to the previous page. If the document is in two-up view, go to the left - * page of the previous row. + * page of the previous row. Public for tests. */ goToPreviousPage() { const currentPage = this.getMostVisiblePage(); @@ -1123,7 +1219,7 @@ export class Viewport { // Unless we're in fit to page or fit to height mode, scroll above the // page by |this.topToolbarHeight_| so that the toolbar isn't covering it // initially. - if (!this.isPagedMode()) { + if (!this.isPagedMode_()) { toolbarOffset = this.topToolbarHeight_; } this.position = { @@ -1160,7 +1256,7 @@ export class Viewport { */ getPageInsetDimensions(page) { const pageDimensions = this.pageDimensions_[page]; - const shadow = Viewport.PAGE_SHADOW; + const shadow = PAGE_SHADOW; return { x: pageDimensions.x + shadow.left, y: pageDimensions.y + shadow.top, @@ -1192,7 +1288,7 @@ export class Viewport { // TODO(raymes): This should really be set when the PDF plugin passes the // page coordinates, but it isn't yet. const x = (this.documentDimensions_.width - pageDimensions.width) / 2 + - Viewport.PAGE_SHADOW.left; + PAGE_SHADOW.left; // Compute the space on the left of the document if the document fits // completely in the screen. const zoom = this.getZoom(); @@ -1213,14 +1309,34 @@ export class Viewport { * In a paged mode, page up and page down scroll to the top of the * previous/next page and part of the page is under the toolbar. * @return {boolean} Whether the current fitting type is a paged mode. + * @private */ - isPagedMode() { + isPagedMode_() { return ( this.fittingType_ === FittingType.FIT_TO_PAGE || this.fittingType_ === FittingType.FIT_TO_HEIGHT); } /** + * Handles a navigation request to a destination from the current controller. + * @param {number} page + * @param {number} x + * @param {number} y + * @param {number} zoom + */ + handleNavigateToDestination(page, x, y, zoom) { + if (zoom) { + this.setZoom(zoom); + } + + if (x || y) { + this.goToPageAndXY(page, x ? x : 0, y ? y : 0); + } else { + this.goToPage(page); + } + } + + /** * @param {!PartialPoint} point The position to which to scroll the viewport. */ scrollTo(point) { @@ -1254,6 +1370,112 @@ export class Viewport { this.tracker_.removeAll(); } } + + /** + * A callback that's called when an update to a pinch zoom is detected. + * @param {!CustomEvent<!PinchEventDetail>} e the pinch event. + * @private + */ + onPinchUpdate_(e) { + // Throttle number of pinch events to one per frame. + if (this.sentPinchEvent_) { + return; + } + + this.sentPinchEvent_ = true; + this.window_.requestAnimationFrame(() => { + this.sentPinchEvent_ = false; + this.mightZoom_(() => { + const {direction, center, startScaleRatio} = e.detail; + this.pinchPhase_ = direction === 'out' ? + Viewport.PinchPhase.PINCH_UPDATE_ZOOM_OUT : + Viewport.PinchPhase.PINCH_UPDATE_ZOOM_IN; + + const scaleDelta = startScaleRatio / this.prevScale_; + if (this.firstPinchCenterInFrame_ != null) { + this.pinchPanVector_ = + vectorDelta(center, this.firstPinchCenterInFrame_); + } + + const needsScrollbars = + this.documentNeedsScrollbars(this.zoomManager_.applyBrowserZoom( + this.clampZoom_(this.internalZoom_ * scaleDelta))); + + this.pinchCenter_ = center; + + // If there's no horizontal scrolling, keep the content centered so + // the user can't zoom in on the non-content area. + // TODO(mcnee) Investigate other ways of scaling when we don't have + // horizontal scrolling. We want to keep the document centered, + // but this causes a potentially awkward transition when we start + // using the gesture center. + if (!needsScrollbars.horizontal) { + this.pinchCenter_ = { + x: this.window_.innerWidth / 2, + y: this.window_.innerHeight / 2 + }; + } else if (this.keepContentCentered_) { + this.oldCenterInContent_ = + this.frameToContent_(this.frameToPluginCoordinate_(center)); + this.keepContentCentered_ = false; + } + + this.setPinchZoomInternal_( + scaleDelta, this.frameToPluginCoordinate_(center)); + this.updateViewport_(); + this.prevScale_ = /** @type {number} */ (startScaleRatio); + }); + }); + } + + /** + * A callback that's called when the end of a pinch zoom is detected. + * @param {!CustomEvent<!PinchEventDetail>} e the pinch event. + * @private + */ + onPinchEnd_(e) { + // Using rAF for pinch end prevents pinch updates scheduled by rAF getting + // sent after the pinch end. + this.window_.requestAnimationFrame(() => { + this.mightZoom_(() => { + const {center, startScaleRatio} = e.detail; + this.pinchPhase_ = Viewport.PinchPhase.PINCH_END; + const scaleDelta = startScaleRatio / this.prevScale_; + this.pinchCenter_ = /** @type {!Point} */ (center); + + this.setPinchZoomInternal_( + scaleDelta, this.frameToPluginCoordinate_(center)); + this.updateViewport_(); + }); + + this.pinchPhase_ = Viewport.PinchPhase.PINCH_NONE; + this.pinchPanVector_ = null; + this.pinchCenter_ = null; + this.firstPinchCenterInFrame_ = null; + }); + } + + /** + * A callback that's called when the start of a pinch zoom is detected. + * @param {!CustomEvent<!PinchEventDetail>} e the pinch event. + * @private + */ + onPinchStart_(e) { + // We also use rAF for pinch start, so that if there is a pinch end event + // scheduled by rAF, this pinch start will be sent after. + this.window_.requestAnimationFrame(() => { + this.pinchPhase_ = Viewport.PinchPhase.PINCH_START; + this.prevScale_ = 1; + this.oldCenterInContent_ = + this.frameToContent_(this.frameToPluginCoordinate_(e.detail.center)); + + const needsScrollbars = this.documentNeedsScrollbars(this.getZoom()); + this.keepContentCentered_ = !needsScrollbars.horizontal; + // We keep track of beginning of the pinch. + // By doing so we will be able to compute the pan distance. + this.firstPinchCenterInFrame_ = e.detail.center; + }); + } } /** @@ -1274,11 +1496,15 @@ Viewport.PinchPhase = { * keys are pressed. Usually we just let the browser handle scrolling on the * window when these keys are pressed but in certain cases we need to simulate * these events. + * @type {number} */ -Viewport.SCROLL_INCREMENT = 40; +const SCROLL_INCREMENT = 40; -/** The width of the page shadow around pages in pixels. */ -Viewport.PAGE_SHADOW = { +/** + * The width of the page shadow around pages in pixels. + * @type {!{top: number, bottom: number, left: number, right: number}} + */ +export const PAGE_SHADOW = { top: 3, bottom: 7, left: 5, diff --git a/chromium/chrome/browser/resources/pdf/viewport_scroller.js b/chromium/chrome/browser/resources/pdf/viewport_scroller.js index c0d8fe4f45f..8d0186ce3d9 100644 --- a/chromium/chrome/browser/resources/pdf/viewport_scroller.js +++ b/chromium/chrome/browser/resources/pdf/viewport_scroller.js @@ -2,12 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -/** - * Creates a new ViewportScroller. - * A ViewportScroller scrolls the page in response to drag selection with the - * mouse. - * - */ +// Scrolls the page in response to drag selection with the mouse. export class ViewportScroller { /** * @param {Object} viewport The viewport info of the page. @@ -27,7 +22,6 @@ export class ViewportScroller { /** * Start scrolling the page by |scrollVelocity_| every * |DRAG_TIMER_INTERVAL_MS_|. - * * @private */ startDragScrollTimer_() { @@ -41,7 +35,6 @@ export class ViewportScroller { /** * Stops the drag scroll timer if it is active. - * * @private */ stopDragScrollTimer_() { @@ -54,7 +47,6 @@ export class ViewportScroller { /** * Scrolls the viewport by the current scroll velocity. - * * @private */ dragScrollPage_() { @@ -71,7 +63,6 @@ export class ViewportScroller { /** * Calculate the velocity to scroll while dragging using the distance of the * cursor outside the viewport. - * * @param {Object} event The mousemove event. * @return {Object} Object with x and y direction scroll velocity. * @private @@ -95,7 +86,6 @@ export class ViewportScroller { /** * Handles mousemove events. It updates the scroll velocity and starts and * stops timer based on scroll velocity. - * * @param {Object} event The mousemove event. * @private */ @@ -111,7 +101,6 @@ export class ViewportScroller { /** * Sets whether to scroll the viewport when the mouse is outside the * viewport. - * * @param {boolean} isSelecting Represents selection status. */ setEnableScrolling(isSelecting) { @@ -134,14 +123,12 @@ export class ViewportScroller { /** * The period of time in milliseconds to wait between updating the viewport * position by the scroll velocity. - * * @private */ ViewportScroller.DRAG_TIMER_INTERVAL_MS_ = 100; /** * The maximum drag scroll distance per DRAG_TIMER_INTERVAL in pixels. - * * @private */ ViewportScroller.MAX_DRAG_SCROLL_DISTANCE_ = 100; diff --git a/chromium/chrome/browser/resources/pdf/zoom_manager.js b/chromium/chrome/browser/resources/pdf/zoom_manager.js index bb4e687e02c..bab9f2ec738 100644 --- a/chromium/chrome/browser/resources/pdf/zoom_manager.js +++ b/chromium/chrome/browser/resources/pdf/zoom_manager.js @@ -6,11 +6,8 @@ import {NativeEventTarget as EventTarget} from 'chrome://resources/js/cr/event_t import {BrowserApi} from './browser_api.js'; -/** - * Abstract parent of classes that manage updating the browser - * with zoom changes and/or updating the viewer's zoom when - * the browser zoom changes. - */ +// Abstract parent of classes that manage updating the browser with zoom changes +// and/or updating the viewer's zoom when the browser zoom changes. export class ZoomManager { /** * @param {function():number} getViewportZoom Callback to get the viewport's @@ -39,7 +36,6 @@ export class ZoomManager { /** * Creates the appropriate kind of zoom manager given the zoom behavior. - * * @param {BrowserApi.ZoomBehavior} zoomBehavior How to manage zoom. * @param {function():number} getViewportZoom A function that gets the current * viewport zoom. @@ -62,20 +58,16 @@ export class ZoomManager { /** * Invoked when a browser-initiated zoom-level change occurs. - * * @param {number} newZoom the zoom level to zoom to. */ onBrowserZoomChange(newZoom) {} - /** - * Invoked when an extension-initiated zoom-level change occurs. - */ + /** Invoked when an extension-initiated zoom-level change occurs. */ onPdfZoomChange() {} /** * Combines the internal pdf zoom and the browser zoom to * produce the total zoom level for the viewer. - * * @param {number} internalZoom the zoom level internal to the viewer. * @return {number} the total zoom level. */ @@ -86,7 +78,6 @@ export class ZoomManager { /** * Given a zoom level, return the internal zoom level needed to * produce that zoom level. - * * @param {number} totalZoom the total zoom level. * @return {number} the zoom level internal to the viewer. */ @@ -96,7 +87,6 @@ export class ZoomManager { /** * Returns whether two numbers are approximately equal. - * * @param {number} a The first number. * @param {number} b The second number. */ @@ -109,15 +99,11 @@ export class ZoomManager { } } -/** - * InactiveZoomManager has no control over the browser's zoom - * and does not respond to browser zoom changes. - */ +// Has no control over the browser's zoom and does not respond to browser zoom +// changes. export class InactiveZoomManager extends ZoomManager {} -/** - * ActiveZoomManager controls the browser's zoom. - */ +// ActiveZoomManager controls the browser's zoom. class ActiveZoomManager extends ZoomManager { /** * Constructs a ActiveZoomManager. @@ -139,7 +125,6 @@ class ActiveZoomManager extends ZoomManager { /** * Invoked when a browser-initiated zoom-level change occurs. - * * @param {number} newZoom the zoom level to zoom to. */ onBrowserZoomChange(newZoom) { @@ -193,7 +178,6 @@ class ActiveZoomManager extends ZoomManager { /** * Combines the internal pdf zoom and the browser zoom to * produce the total zoom level for the viewer. - * * @param {number} internalZoom the zoom level internal to the viewer. * @return {number} the total zoom level. */ @@ -206,7 +190,6 @@ class ActiveZoomManager extends ZoomManager { /** * Given a zoom level, return the internal zoom level needed to * produce that zoom level. - * * @param {number} totalZoom the total zoom level. * @return {number} the zoom level internal to the viewer. */ @@ -217,14 +200,11 @@ class ActiveZoomManager extends ZoomManager { } } -/** - * This EmbeddedZoomManager responds to changes in the browser zoom, - * but does not control the browser zoom. - */ +// Responds to changes in the browser zoom, but does not control the browser +// zoom. class EmbeddedZoomManager extends ZoomManager { /** * Invoked when a browser-initiated zoom-level change occurs. - * * @param {number} newZoom the new browser zoom level. */ onBrowserZoomChange(newZoom) { |