From 3914768085027376eb76d065c7ca40c2935b8f3c Mon Sep 17 00:00:00 2001 From: Rob Wu Date: Wed, 2 Oct 2013 22:09:43 +0200 Subject: [PATCH] Implement hand tool The logic for the hand tool is implemented in a separate project, maintained at https://github.com/Rob--W/grab-to-pan.js Integration notes - Added toggle as an entry under the Secondary toolbar - Added shortcut "h" to toggle hand tool (to-do: document this in wiki after merge). This shortcut is also used in Adobe's Acrobat Reader. To-do: localizations for: hand_tool_enable.title= hand_tool_enable_label= hand_tool_disable.title= hand_tool_disable_label= To-do (wish): persistence of hand tool preference, preferably a global setting. secondaryToolbarButton-handTool.png created by Stephen Horlander --- l10n/en-US/viewer.properties | 5 + web/grab_to_pan.js | 212 ++++++++++++++++++ web/hand_tool.js | 62 +++++ web/images/grab.cur | Bin 0 -> 326 bytes web/images/grabbing.cur | Bin 0 -> 326 bytes .../secondaryToolbarButton-handTool.png | Bin 0 -> 250 bytes web/presentation_mode.js | 4 +- web/viewer.css | 28 +++ web/viewer.html | 8 + web/viewer.js | 13 +- 10 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 web/grab_to_pan.js create mode 100644 web/hand_tool.js create mode 100644 web/images/grab.cur create mode 100644 web/images/grabbing.cur create mode 100644 web/images/secondaryToolbarButton-handTool.png diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index 629bfb27c..2c893a4a3 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -57,6 +57,11 @@ page_rotate_ccw.title=Rotate Counterclockwise page_rotate_ccw.label=Rotate Counterclockwise page_rotate_ccw_label=Rotate Counterclockwise +hand_tool_enable.title=Enable hand tool +hand_tool_enable_label=Enable hand tool +hand_tool_disable.title=Disable hand tool +hand_tool_disable_label=Disable hand tool + # Tooltips and alt text for side panel toolbar buttons # (the _label strings are alt text for the buttons, the .title strings are # tooltips) diff --git a/web/grab_to_pan.js b/web/grab_to_pan.js new file mode 100644 index 000000000..8332e4604 --- /dev/null +++ b/web/grab_to_pan.js @@ -0,0 +1,212 @@ +/* Copyright 2013 Rob Wu + * https://github.com/Rob--W/grab-to-pan.js + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +var GrabToPan = (function GrabToPanClosure() { + /** + * Construct a GrabToPan instance for a given HTML element. + * @param options.element {Element} + * @param options.ignoreTarget {function} optional. See `ignoreTarget(node)` + * @param options.onActiveChanged {function(boolean)} optional. Called + * when grab-to-pan is (de)activated. The first argument is a boolean that + * shows whether grab-to-pan is activated. + */ + function GrabToPan(options) { + this.element = options.element; + this.document = options.element.ownerDocument; + if (typeof options.ignoreTarget === 'function') { + this.ignoreTarget = options.ignoreTarget; + } + this.onActiveChanged = options.onActiveChanged; + + // Bind the contexts to ensure that `this` always points to + // the GrabToPan instance. + this.activate = this.activate.bind(this); + this.deactivate = this.deactivate.bind(this); + this.toggle = this.toggle.bind(this); + this._onmousedown = this._onmousedown.bind(this); + this._onmousemove = this._onmousemove.bind(this); + this._endPan = this._endPan.bind(this); + } + GrabToPan.prototype = { + /** + * Class name of element which can be grabbed + */ + CSS_CLASS_GRAB: 'grab-to-pan-grab', + /** + * Class name of element which is being dragged & panned + */ + CSS_CLASS_GRABBING: 'grab-to-pan-grabbing', + + /** + * Bind a mousedown event to the element to enable grab-detection. + */ + activate: function GrabToPan_activate() { + if (!this.active) { + this.active = true; + this.element.addEventListener('mousedown', this._onmousedown, true); + this.element.classList.add(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(true); + } + } + }, + + /** + * Removes all events. Any pending pan session is immediately stopped. + */ + deactivate: function GrabToPan_deactivate() { + if (this.active) { + this.active = false; + this.element.removeEventListener('mousedown', this._onmousedown, true); + this._endPan(); + this.element.classList.remove(this.CSS_CLASS_GRAB); + if (this.onActiveChanged) { + this.onActiveChanged(false); + } + } + }, + + toggle: function GrabToPan_toggle() { + if (this.active) { + this.deactivate(); + } else { + this.activate(); + } + }, + + /** + * Whether to not pan if the target element is clicked. + * Override this method to change the default behaviour. + * + * @param node {Element} The target of the event + * @return {boolean} Whether to not react to the click event. + */ + ignoreTarget: function GrabToPan_ignoreTarget(node) { + // Use matchesSelector to check whether the clicked element + // is (a child of) an input element / link + return node[matchesSelector]( + 'a[href], a[href] *, input, textarea, button, button *, select, option' + ); + }, + + /** + * @private + */ + _onmousedown: function GrabToPan__onmousedown(event) { + if (event.button !== 0 || this.ignoreTarget(event.target)) { + return; + } + if (event.originalTarget) { + try { + /* jshint expr:true */ + event.originalTarget.tagName; + } catch (e) { + // Mozilla-specific: element is a scrollbar (XUL element) + return; + } + } + + this.scrollLeftStart = this.element.scrollLeft; + this.scrollTopStart = this.element.scrollTop; + this.clientXStart = event.clientX; + this.clientYStart = event.clientY; + this.document.addEventListener('mousemove', this._onmousemove, true); + this.document.addEventListener('mouseup', this._endPan, true); + // When a scroll event occurs before a mousemove, assume that the user + // dragged a scrollbar (necessary for Opera Presto, Safari and IE) + // (not needed for Chrome/Firefox) + this.element.addEventListener('scroll', this._endPan, true); + event.preventDefault(); + event.stopPropagation(); + this.element.classList.remove(this.CSS_CLASS_GRAB); + this.document.documentElement.classList.add(this.CSS_CLASS_GRABBING); + }, + + /** + * @private + */ + _onmousemove: function GrabToPan__onmousemove(event) { + this.element.removeEventListener('scroll', this._endPan, true); + if (isLeftMouseReleased(event)) { + this.document.removeEventListener('mousemove', this._onmousemove, true); + return; + } + var xDiff = event.clientX - this.clientXStart; + var yDiff = event.clientY - this.clientYStart; + this.element.scrollTop = this.scrollTopStart - yDiff; + this.element.scrollLeft = this.scrollLeftStart - xDiff; + }, + + /** + * @private + */ + _endPan: function GrabToPan__endPan() { + this.element.removeEventListener('scroll', this._endPan, true); + this.document.removeEventListener('mousemove', this._onmousemove, true); + this.document.removeEventListener('mouseup', this._endPan, true); + this.document.documentElement.classList.remove(this.CSS_CLASS_GRABBING); + this.element.classList.add(this.CSS_CLASS_GRAB); + } + }; + + // Get the correct (vendor-prefixed) name of the matches method. + var matchesSelector; + ['webkitM', 'mozM', 'msM', 'oM', 'm'].some(function(prefix) { + var name = prefix + 'atches'; + if (name in document.documentElement) { + matchesSelector = name; + } + name += 'Selector'; + if (name in document.documentElement) { + matchesSelector = name; + } + return matchesSelector; // If found, then truthy, and [].some() ends. + }); + + // Browser sniffing because it's impossible to feature-detect + // whether event.which for onmousemove is reliable + var isNotIEorIsIE10plus = !document.documentMode || document.documentMode > 9; + var chrome = window.chrome; + var isChrome15OrOpera15plus = chrome && (chrome.webstore || chrome.app); + // ^ Chrome 15+ ^ Opera 15+ + var isSafari6plus = /Apple/.test(navigator.vendor) && + /Version\/([6-9]\d*|[1-5]\d+)/.test(navigator.userAgent); + + /** + * Whether the left mouse is not pressed. + * @param event {MouseEvent} + * @return {boolean} True if the left mouse button is not pressed. + * False if unsure or if the left mouse button is pressed. + */ + function isLeftMouseReleased(event) { + if ('buttons' in event && isNotIEorIsIE10plus) { + // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-buttons + // Firefox 15+ + // Internet Explorer 10+ + return !(event.buttons | 1); + } + if (isChrome15OrOpera15plus || isSafari6plus) { + // Chrome 14+ + // Opera 15+ + // Safari 6.0+ + return event.which === 0; + } + } + + return GrabToPan; +})(); diff --git a/web/hand_tool.js b/web/hand_tool.js new file mode 100644 index 000000000..987e11d1f --- /dev/null +++ b/web/hand_tool.js @@ -0,0 +1,62 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* Copyright 2013 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* globals mozL10n, GrabToPan, PDFView */ + +'use strict'; + +//#include grab_to_pan.js +var HandTool = { + initialize: function handToolInitialize(options) { + var toggleHandTool = options.toggleHandTool; + this.handTool = new GrabToPan({ + element: options.container, + onActiveChanged: function(isActive) { + if (isActive) { + toggleHandTool.title = + mozL10n.get('hand_tool_disable.title', null, 'Disable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_disable_label', null, 'Disable hand tool'); + } else { + toggleHandTool.title = + mozL10n.get('hand_tool_enable.title', null, 'Enable hand tool'); + toggleHandTool.firstElementChild.textContent = + mozL10n.get('hand_tool_enable_label', null, 'Enable hand tool'); + } + } + }); + toggleHandTool.addEventListener('click', this.handTool.toggle, false); + // TODO: Read global prefs and call this.handTool.activate() if needed. + }, + + toggle: function handToolToggle() { + this.handTool.toggle(); + }, + + enterPresentationMode: function handToolEnterPresentationMode() { + if (this.handTool.active) { + this.wasActive = true; + this.handTool.deactivate(); + } + }, + + exitPresentationMode: function handToolExitPresentationMode() { + if (this.wasActive) { + this.wasActive = null; + this.handTool.activate(); + } + } +}; diff --git a/web/images/grab.cur b/web/images/grab.cur new file mode 100644 index 0000000000000000000000000000000000000000..db7ad5aed3ef958aa13903afa769386382a87ad3 GIT binary patch literal 326 zcmajZAr8Vo5QX7?si;7#!yvj6Nx}i(CG{p)>hLyxD99 z76ej)=-vic0UL9~!GgO~9_MsOnufoUx`QHG)5E@a!o?nT8@#*2`fMlI9X~2&%Qwtd fAElzRww7{H`T9z)QIL|gUwP@h-(DMO`2YPI1=f8x literal 0 HcmV?d00001 diff --git a/web/images/grabbing.cur b/web/images/grabbing.cur new file mode 100644 index 0000000000000000000000000000000000000000..e0dfd04e4d3fcbaa6588c8cbb9e9065609bcb862 GIT binary patch literal 326 zcmZQzU}9ioP*7lC;0HnjMg|5k1_lNVAO;FCH~=vt5Q0Dhn8YOh|NoCEh)sn30RsaF z^8>N`2L=Xv5dHz=L$Uk|1_tQ_266z<4TQl5{{R0$_yG_fVE_NW0fd=>Y#@FBr9t9P K<`XsxO$7j30D*Y` literal 0 HcmV?d00001 diff --git a/web/images/secondaryToolbarButton-handTool.png b/web/images/secondaryToolbarButton-handTool.png new file mode 100644 index 0000000000000000000000000000000000000000..10845c695d7b832450d530702ffab9f674427618 GIT binary patch literal 250 zcmV1bpiJDg3v)>MCqV1XU?xAVe_VeFh zM>Qjym>Qv^sjPch6IsHcv58G3jb(kw+EvU5cNR7rdsi%!6^qrwgEvb@wv^7CFt|~z z3$?Q(@aRKPdtM0)uGF4Sw*+|cq!=3Co}HTW2R&t##L7-;C;$Ke07*qoM6N<$g1!u6 AC;$Ke literal 0 HcmV?d00001 diff --git a/web/presentation_mode.js b/web/presentation_mode.js index c2aa7ac12..212887b22 100644 --- a/web/presentation_mode.js +++ b/web/presentation_mode.js @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* globals PDFView, scrollIntoView */ +/* globals PDFView, scrollIntoView, HandTool */ 'use strict'; @@ -103,6 +103,7 @@ var PresentationMode = { window.addEventListener('contextmenu', this.contextMenu, false); this.showControls(); + HandTool.enterPresentationMode(); this.contextMenuOpen = false; this.container.setAttribute('contextmenu', 'viewerContextMenu'); }, @@ -121,6 +122,7 @@ var PresentationMode = { this.hideControls(); this.args = null; PDFView.clearMouseScrollState(); + HandTool.exitPresentationMode(); this.container.removeAttribute('contextmenu'); this.contextMenuOpen = false; diff --git a/web/viewer.css b/web/viewer.css index c0948a788..41fdbbe10 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -983,6 +983,10 @@ html[dir="rtl"] .secondaryToolbarButton > span { content: url(images/secondaryToolbarButton-rotateCw.png); } +.secondaryToolbarButton.handTool::before { + content: url(images/secondaryToolbarButton-handTool.png); +} + .verticalToolbarSeparator { display: block; padding: 8px 0; @@ -1494,6 +1498,30 @@ canvas { color: black; } +.grab-to-pan-grab * { + cursor: url("images/grab.cur"), move !important; + cursor: -webkit-grab !important; + cursor: -moz-grab !important; +} +.grab-to-pan-grabbing, +.grab-to-pan-grabbing * { + cursor: url("images/grabbing.cur"), move !important; + cursor: -webkit-grabbing !important; + cursor: -moz-grabbing !important; +} +.grab-to-pan-grab input, +.grab-to-pan-grab textarea, +.grab-to-pan-grab button, +.grab-to-pan-grab button *, +.grab-to-pan-grab select, +.grab-to-pan-grab option { + cursor: auto !important; +} +.grab-to-pan-grab a[href], +.grab-to-pan-grab a[href] * { + cursor: pointer !important; +} + @page { margin: 0; } diff --git a/web/viewer.html b/web/viewer.html index 9d2c475a4..5d73eca52 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -75,6 +75,8 @@ limitations under the License. + + @@ -163,6 +165,12 @@ limitations under the License. + +
+ + diff --git a/web/viewer.js b/web/viewer.js index a6821f3ca..9ea0388d3 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -18,7 +18,7 @@ PDFFindController, ProgressBar, TextLayerBuilder, DownloadManager, getFileName, scrollIntoView, getPDFFileNameFromURL, PDFHistory, Preferences, Settings, PageView, ThumbnailView, noContextMenuHandler, - SecondaryToolbar, PasswordPrompt, PresentationMode */ + SecondaryToolbar, PasswordPrompt, PresentationMode, HandTool */ 'use strict'; @@ -87,6 +87,7 @@ var currentPageNumber = 1; //#include secondary_toolbar.js //#include password_prompt.js //#include presentation_mode.js +//#include hand_tool.js var PDFView = { pages: [], @@ -140,6 +141,11 @@ var PDFView = { integratedFind: this.supportsIntegratedFind }); + HandTool.initialize({ + container: container, + toggleHandTool: document.getElementById('toggleHandTool') + }); + SecondaryToolbar.initialize({ toolbar: document.getElementById('secondaryToolbar'), presentationMode: PresentationMode, @@ -2138,6 +2144,11 @@ window.addEventListener('keydown', function keydown(evt) { } break; + case 72: // 'h' + if (!PresentationMode.active) { + HandTool.toggle(); + } + break; case 82: // 'r' PDFView.rotatePages(90); break;