Add ability for NetLogLogger to gather data from more than just NetLog
[chromium-blink-merge.git] / ui / file_manager / gallery / js / slide_mode.js
blob8bbb554594b28ed2e42615e4a26ae9fe5ae81256
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6 * Slide mode displays a single image and has a set of controls to navigate
7 * between the images and to edit an image.
9 * @param {!HTMLElement} container Main container element.
10 * @param {!HTMLElement} content Content container element.
11 * @param {!HTMLElement} toolbar Toolbar element.
12 * @param {!ImageEditor.Prompt} prompt Prompt.
13 * @param {!ErrorBanner} errorBanner Error banner.
14 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
15 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
16 * @param {!MetadataModel} metadataModel
17 * @param {!ThumbnailModel} thumbnailModel
18 * @param {!Object} context Context.
19 * @param {!VolumeManager} volumeManager Volume manager.
20 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
21 * @param {function(string):string} displayStringFunction String formatting
22 * function.
24 * @constructor
25 * @struct
26 * @suppress {checkStructDictInheritance}
27 * @extends {cr.EventTarget}
29 function SlideMode(container, content, toolbar, prompt, errorBanner, dataModel,
30 selectionModel, metadataModel, thumbnailModel, context, volumeManager,
31 toggleMode, displayStringFunction) {
32 /**
33 * @type {!HTMLElement}
34 * @private
35 * @const
37 this.container_ = container;
39 /**
40 * @type {!Document}
41 * @private
42 * @const
44 this.document_ = assert(container.ownerDocument);
46 /**
47 * @type {!HTMLElement}
48 * @const
50 this.content = content;
52 /**
53 * @type {!HTMLElement}
54 * @private
55 * @const
57 this.toolbar_ = toolbar;
59 /**
60 * @type {!ImageEditor.Prompt}
61 * @private
62 * @const
64 this.prompt_ = prompt;
66 /**
67 * @type {!ErrorBanner}
68 * @private
69 * @const
71 this.errorBanner_ = errorBanner;
73 /**
74 * @type {!cr.ui.ArrayDataModel}
75 * @private
76 * @const
78 this.dataModel_ = dataModel;
80 /**
81 * @type {!cr.ui.ListSelectionModel}
82 * @private
83 * @const
85 this.selectionModel_ = selectionModel;
87 /**
88 * @type {!Object}
89 * @private
90 * @const
92 this.context_ = context;
94 /**
95 * @type {!VolumeManager}
96 * @private
97 * @const
99 this.volumeManager_ = volumeManager;
102 * @type {function(function())}
103 * @private
104 * @const
106 this.toggleMode_ = toggleMode;
109 * @type {function(string):string}
110 * @private
111 * @const
113 this.displayStringFunction_ = displayStringFunction;
116 * @type {function(this:SlideMode)}
117 * @private
118 * @const
120 this.onSelectionBound_ = this.onSelection_.bind(this);
123 * @type {function(this:SlideMode,!Event)}
124 * @private
125 * @const
127 this.onSpliceBound_ = this.onSplice_.bind(this);
130 * Unique numeric key, incremented per each load attempt used to discard
131 * old attempts. This can happen especially when changing selection fast or
132 * Internet connection is slow.
134 * @type {number}
135 * @private
137 this.currentUniqueKey_ = 0;
140 * @type {number}
141 * @private
143 this.sequenceDirection_ = 0;
146 * @type {number}
147 * @private
149 this.sequenceLength_ = 0;
152 * @type {Array.<number>}
153 * @private
155 this.savedSelection_ = null;
158 * @type {Gallery.Item}
159 * @private
161 this.displayedItem_ = null;
164 * @type {?number}
165 * @private
167 this.slideHint_ = null;
170 * @type {boolean}
171 * @private
173 this.active_ = false;
176 * @type {boolean}
177 * @private
179 this.leaveAfterSlideshow_ = false;
182 * @type {boolean}
183 * @private
185 this.fullscreenBeforeSlideshow_ = false;
188 * @type {?number}
189 * @private
191 this.slideShowTimeout_ = null;
194 * @type {?number}
195 * @private
197 this.spinnerTimer_ = null;
199 window.addEventListener('resize', this.onResize_.bind(this));
201 // ----------------------------------------------------------------
202 // Initializes the UI.
205 * Container for displayed image.
206 * @type {!HTMLElement}
207 * @private
208 * @const
210 this.imageContainer_ = util.createChild(queryRequiredElement(
211 this.document_, '.content'), 'image-container');
212 this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
214 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
217 * Overwrite options and info bubble.
218 * @type {!HTMLElement}
219 * @private
220 * @const
222 this.options_ = util.createChild(queryRequiredElement(
223 this.toolbar_, '.filename-spacer'), 'options');
226 * @type {!HTMLElement}
227 * @private
228 * @const
230 this.savedLabel_ = util.createChild(this.options_, 'saved');
231 this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
234 * @type {!HTMLElement}
235 * @const
237 var overwriteOriginalBox = util.createChild(
238 this.options_, 'overwrite-original');
241 * @type {!HTMLElement}
242 * @private
243 * @const
245 this.overwriteOriginal_ = util.createChild(
246 overwriteOriginalBox, '', 'input');
247 this.overwriteOriginal_.type = 'checkbox';
248 this.overwriteOriginal_.id = 'overwrite-checkbox';
249 chrome.storage.local.get(SlideMode.OVERWRITE_KEY, function(values) {
250 var value = values[SlideMode.OVERWRITE_KEY];
251 // Out-of-the box default is 'true'
252 this.overwriteOriginal_.checked =
253 (value === 'false' || value === false) ? false : true;
254 }.bind(this));
255 this.overwriteOriginal_.addEventListener('click',
256 this.onOverwriteOriginalClick_.bind(this));
259 * @type {!HTMLElement}
260 * @const
262 var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
263 overwriteLabel.textContent =
264 this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
265 overwriteLabel.setAttribute('for', 'overwrite-checkbox');
268 * @type {!HTMLElement}
269 * @private
270 * @const
272 this.bubble_ = util.createChild(this.toolbar_, 'bubble');
273 this.bubble_.hidden = true;
276 * @type {!HTMLElement}
277 * @const
279 var bubbleContent = util.createChild(this.bubble_);
280 bubbleContent.innerHTML = this.displayStringFunction_(
281 'GALLERY_OVERWRITE_BUBBLE');
283 util.createChild(this.bubble_, 'pointer bottom', 'span');
286 * @type {!HTMLElement}
287 * @const
289 var bubbleClose = util.createChild(this.bubble_, 'close-x');
290 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
293 * Ribbon and related controls.
294 * @type {!HTMLElement}
295 * @private
296 * @const
298 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
301 * @type {!HTMLElement}
302 * @private
303 * @const
305 this.arrowLeft_ = util.createChild(
306 this.arrowBox_, 'arrow left tool dimmable');
307 this.arrowLeft_.addEventListener('click',
308 this.advanceManually.bind(this, -1));
309 util.createChild(this.arrowLeft_);
311 util.createChild(this.arrowBox_, 'arrow-spacer');
314 * @type {!HTMLElement}
315 * @private
316 * @const
318 this.arrowRight_ = util.createChild(
319 this.arrowBox_, 'arrow right tool dimmable');
320 this.arrowRight_.addEventListener('click',
321 this.advanceManually.bind(this, 1));
322 util.createChild(this.arrowRight_);
325 * @type {!HTMLElement}
326 * @private
327 * @const
329 this.ribbonSpacer_ = queryRequiredElement(this.toolbar_, '.ribbon-spacer');
332 * @type {!Ribbon}
333 * @private
334 * @const
336 this.ribbon_ = new Ribbon(
337 this.document_, this.dataModel_, this.selectionModel_, thumbnailModel);
338 this.ribbonSpacer_.appendChild(this.ribbon_);
340 util.createChild(this.container_, 'spinner');
343 * @type {!HTMLElement}
344 * @const
346 var slideShowButton = queryRequiredElement(this.toolbar_, 'button.slideshow');
347 slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
348 slideShowButton.addEventListener('click',
349 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
352 * @type {!HTMLElement}
353 * @const
355 var slideShowToolbar = util.createChild(
356 this.container_, 'tool slideshow-toolbar');
357 util.createChild(slideShowToolbar, 'slideshow-play').
358 addEventListener('click', this.toggleSlideshowPause_.bind(this));
359 util.createChild(slideShowToolbar, 'slideshow-end').
360 addEventListener('click', this.stopSlideshow_.bind(this));
362 // Editor.
364 * @type {!HTMLElement}
365 * @private
366 * @const
368 this.editButton_ = queryRequiredElement(this.toolbar_, 'button.edit');
369 this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
370 this.editButton_.disabled = true; // Disabled by default.
371 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
374 * @type {!HTMLElement}
375 * @private
376 * @const
378 this.printButton_ = queryRequiredElement(this.toolbar_, 'button.print');
379 this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
380 this.printButton_.disabled = true; // Disabled by default.
381 this.printButton_.addEventListener('click', this.print_.bind(this));
384 * @type {!HTMLElement}
385 * @private
386 * @const
388 this.editBarSpacer_ = queryRequiredElement(this.toolbar_, '.edit-bar-spacer');
391 * @type {!HTMLElement}
392 * @private
393 * @const
395 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
398 * @type {!HTMLElement}
399 * @private
400 * @const
402 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
405 * @type {!HTMLElement}
406 * @private
407 * @const
409 this.editBarModeWrapper_ = util.createChild(
410 this.editBarMode_, 'edit-modal-wrapper dimmable');
411 this.editBarModeWrapper_.hidden = true;
414 * Objects supporting image display and editing.
415 * @type {!Viewport}
416 * @private
417 * @const
419 this.viewport_ = new Viewport();
422 * @type {!ImageView}
423 * @private
424 * @const
426 this.imageView_ = new ImageView(
427 this.imageContainer_,
428 this.viewport_,
429 metadataModel);
432 * @type {!ImageEditor}
433 * @private
434 * @const
436 this.editor_ = new ImageEditor(
437 this.viewport_,
438 this.imageView_,
439 this.prompt_,
441 root: this.container_,
442 image: this.imageContainer_,
443 toolbar: this.editBarMain_,
444 mode: this.editBarModeWrapper_
446 SlideMode.EDITOR_MODES,
447 this.displayStringFunction_,
448 this.onToolsVisibilityChanged_.bind(this));
451 * @type {!TouchHandler}
452 * @private
453 * @const
455 this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
459 * List of available editor modes.
460 * @type {!Array.<ImageEditor.Mode>}
461 * @const
463 SlideMode.EDITOR_MODES = [
464 new ImageEditor.Mode.InstantAutofix(),
465 new ImageEditor.Mode.Crop(),
466 new ImageEditor.Mode.Exposure(),
467 new ImageEditor.Mode.OneClick(
468 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
469 new ImageEditor.Mode.OneClick(
470 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
474 * Map of the key identifier and offset delta.
475 * @enum {!Array.<number>})
476 * @const
478 SlideMode.KEY_OFFSET_MAP = {
479 'Up': [0, 20],
480 'Down': [0, -20],
481 'Left': [20, 0],
482 'Right': [-20, 0]
486 * SlideMode extends cr.EventTarget.
488 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
491 * @return {string} Mode name.
493 SlideMode.prototype.getName = function() { return 'slide'; };
496 * @return {string} Mode title.
498 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
501 * @return {!Viewport} Viewport.
503 SlideMode.prototype.getViewport = function() { return this.viewport_; };
506 * Load items, display the selected item.
507 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
508 * @param {function()} displayCallback Called when the image is displayed.
509 * @param {function()} loadCallback Called when the image is displayed.
511 SlideMode.prototype.enter = function(
512 zoomFromRect, displayCallback, loadCallback) {
513 this.sequenceDirection_ = 0;
514 this.sequenceLength_ = 0;
516 var loadDone = function(loadType, delay) {
517 this.active_ = true;
519 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
520 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
522 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
523 this.ribbon_.enable();
525 // Wait 1000ms after the animation is done, then prefetch the next image.
526 this.requestPrefetch(1, delay + 1000);
528 if (loadCallback) loadCallback();
529 }.bind(this);
531 // The latest |leave| call might have left the image animating. Remove it.
532 this.unloadImage_();
534 new Promise(function(fulfill) {
535 // If the items are empty, just show the error message.
536 if (this.getItemCount_() === 0) {
537 this.displayedItem_ = null;
538 //TODO(hirono) Show this message in the grid mode too.
539 this.errorBanner_.show('GALLERY_NO_IMAGES');
540 fulfill();
541 return;
544 // Remember the selection if it is empty or multiple. It will be restored
545 // in |leave| if the user did not changing the selection manually.
546 var currentSelection = this.selectionModel_.selectedIndexes;
547 if (currentSelection.length === 1)
548 this.savedSelection_ = null;
549 else
550 this.savedSelection_ = currentSelection;
552 // Ensure valid single selection.
553 // Note that the SlideMode object is not listening to selection change yet.
554 this.select(Math.max(0, this.getSelectedIndex()));
556 // Show the selected item ASAP, then complete the initialization
557 // (loading the ribbon thumbnails can take some time).
558 var selectedItem = this.getSelectedItem();
559 this.displayedItem_ = selectedItem;
561 // Load the image of the item.
562 this.loadItem_(
563 selectedItem,
564 zoomFromRect ?
565 this.imageView_.createZoomEffect(zoomFromRect) :
566 new ImageView.Effect.None(),
567 displayCallback,
568 function(loadType, delay) {
569 fulfill(delay);
571 }.bind(this)).then(function(delay) {
572 // Turn the mode active.
573 this.active_ = true;
574 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
575 this.ribbon_.enable();
577 // Register handlers.
578 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
579 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
580 this.touchHandlers_.enabled = true;
582 // Wait 1000ms after the animation is done, then prefetch the next image.
583 this.requestPrefetch(1, delay + 1000);
585 // Call load callback.
586 if (loadCallback)
587 loadCallback();
588 }.bind(this)).catch(function(error) {
589 console.error(error.stack, error);
594 * Leave the mode.
595 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
596 * @param {function()} callback Called when the image is committed and
597 * the zoom-out animation has started.
599 SlideMode.prototype.leave = function(zoomToRect, callback) {
600 var commitDone = function() {
601 this.stopEditing_();
602 this.stopSlideshow_();
603 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
604 this.selectionModel_.removeEventListener(
605 'change', this.onSelectionBound_);
606 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
607 this.ribbon_.disable();
608 this.active_ = false;
609 if (this.savedSelection_)
610 this.selectionModel_.selectedIndexes = this.savedSelection_;
611 this.unloadImage_(zoomToRect);
612 callback();
613 }.bind(this);
615 this.viewport_.resetView();
616 if (this.getItemCount_() === 0) {
617 this.errorBanner_.clear();
618 commitDone();
619 } else {
620 this.commitItem_(commitDone);
623 // Disable the slide-mode only buttons when leaving.
624 this.editButton_.disabled = true;
625 this.printButton_.disabled = true;
627 // Disable touch operation.
628 this.touchHandlers_.enabled = false;
633 * Execute an action when the editor is not busy.
635 * @param {function()} action Function to execute.
637 SlideMode.prototype.executeWhenReady = function(action) {
638 this.editor_.executeWhenReady(action);
642 * @return {boolean} True if the mode has active tools (that should not fade).
644 SlideMode.prototype.hasActiveTool = function() {
645 return this.isEditing();
649 * @return {number} Item count.
650 * @private
652 SlideMode.prototype.getItemCount_ = function() {
653 return this.dataModel_.length;
657 * @param {number} index Index.
658 * @return {Gallery.Item} Item.
660 SlideMode.prototype.getItem = function(index) {
661 var item =
662 /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
663 return item === undefined ? null : item;
667 * @return {number} Selected index.
669 SlideMode.prototype.getSelectedIndex = function() {
670 return this.selectionModel_.selectedIndex;
674 * @return {ImageRect} Screen rectangle of the selected image.
676 SlideMode.prototype.getSelectedImageRect = function() {
677 if (this.getSelectedIndex() < 0)
678 return null;
679 else
680 return this.viewport_.getImageBoundsOnScreen();
684 * @return {Gallery.Item} Selected item.
686 SlideMode.prototype.getSelectedItem = function() {
687 return this.getItem(this.getSelectedIndex());
691 * Toggles the full screen mode.
692 * @private
694 SlideMode.prototype.toggleFullScreen_ = function() {
695 util.toggleFullScreen(this.context_.appWindow,
696 !util.isFullScreen(this.context_.appWindow));
700 * Selection change handler.
702 * Commits the current image and displays the newly selected image.
703 * @private
705 SlideMode.prototype.onSelection_ = function() {
706 if (this.selectionModel_.selectedIndexes.length === 0)
707 return; // Ignore temporary empty selection.
709 // Forget the saved selection if the user changed the selection manually.
710 if (!this.isSlideshowOn_())
711 this.savedSelection_ = null;
713 if (this.getSelectedItem() === this.displayedItem_)
714 return; // Do not reselect.
716 this.commitItem_(this.loadSelectedItem_.bind(this));
720 * Handles changes in tools visibility, and if the header is dimmed, then
721 * requests disabling the draggable app region.
723 * @private
725 SlideMode.prototype.onToolsVisibilityChanged_ = function() {
726 var headerDimmed = queryRequiredElement(this.document_, '.header')
727 .hasAttribute('dimmed');
728 this.context_.onAppRegionChanged(!headerDimmed);
732 * Change the selection.
734 * @param {number} index New selected index.
735 * @param {number=} opt_slideHint Slide animation direction (-1|1).
737 SlideMode.prototype.select = function(index, opt_slideHint) {
738 this.slideHint_ = opt_slideHint || null;
739 this.selectionModel_.selectedIndex = index;
740 this.selectionModel_.leadIndex = index;
744 * Load the selected item.
746 * @private
748 SlideMode.prototype.loadSelectedItem_ = function() {
749 var slideHint = this.slideHint_;
750 this.slideHint_ = null;
752 if (this.getSelectedItem() === this.displayedItem_)
753 return; // Do not reselect.
755 var index = this.getSelectedIndex();
756 var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
757 var step =
758 slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);
760 if (Math.abs(step) != 1) {
761 // Long leap, the sequence is broken, we have no good prefetch candidate.
762 this.sequenceDirection_ = 0;
763 this.sequenceLength_ = 0;
764 } else if (this.sequenceDirection_ === step) {
765 // Keeping going in sequence.
766 this.sequenceLength_++;
767 } else {
768 // Reversed the direction. Reset the counter.
769 this.sequenceDirection_ = step;
770 this.sequenceLength_ = 1;
773 this.displayedItem_ = this.getSelectedItem();
774 var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);
776 if (this.sequenceLength_ <= 1) {
777 // We have just broke the sequence. Touch the current image so that it stays
778 // in the cache longer.
779 this.imageView_.prefetch(selectedItem);
782 function shouldPrefetch(loadType, step, sequenceLength) {
783 // Never prefetch when selecting out of sequence.
784 if (Math.abs(step) != 1)
785 return false;
787 // Always prefetch if the previous load was from cache.
788 if (loadType === ImageView.LoadType.CACHED_FULL)
789 return true;
791 // Prefetch if we have been going in the same direction for long enough.
792 return sequenceLength >= 3;
795 this.currentUniqueKey_++;
796 var selectedUniqueKey = this.currentUniqueKey_;
798 // Discard, since another load has been invoked after this one.
799 if (selectedUniqueKey != this.currentUniqueKey_)
800 return;
802 this.loadItem_(
803 selectedItem,
804 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
805 function() {} /* no displayCallback */,
806 function(loadType, delay) {
807 // Discard, since another load has been invoked after this one.
808 if (selectedUniqueKey != this.currentUniqueKey_)
809 return;
810 if (shouldPrefetch(loadType, step, this.sequenceLength_))
811 this.requestPrefetch(step, delay);
812 if (this.isSlideshowPlaying_())
813 this.scheduleNextSlide_();
814 }.bind(this));
818 * Unload the current image.
820 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
821 * @private
823 SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
824 this.imageView_.unload(opt_zoomToRect);
828 * Data model 'splice' event handler.
829 * @param {!Event} event Event.
830 * @this {SlideMode}
831 * @private
833 SlideMode.prototype.onSplice_ = function(event) {
834 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
836 // Splice invalidates saved indices, drop the saved selection.
837 this.savedSelection_ = null;
839 if (event.removed.length != 1)
840 return;
842 // Delay the selection to let the ribbon splice handler work first.
843 setTimeout(function() {
844 var displayedItemNotRemvoed = event.removed.every(function(item) {
845 return item !== this.displayedItem_;
846 }.bind(this));
847 if (displayedItemNotRemvoed)
848 return;
849 var nextIndex;
850 if (event.index < this.dataModel_.length) {
851 // There is the next item, select it.
852 // The next item is now at the same index as the removed one, so we need
853 // to correct displayIndex_ so that loadSelectedItem_ does not think
854 // we are re-selecting the same item (and does right-to-left slide-in
855 // animation).
856 nextIndex = event.index;
857 } else if (this.dataModel_.length) {
858 // Removed item is the rightmost, but there are more items.
859 nextIndex = event.index - 1; // Select the new last index.
860 } else {
861 // No items left. Unload the image, disable edit and print button, and
862 // show the banner.
863 this.commitItem_(function() {
864 this.unloadImage_();
865 this.printButton_.disabled = true;
866 this.editButton_.disabled = true;
867 this.errorBanner_.show('GALLERY_NO_IMAGES');
868 }.bind(this));
869 return;
871 // To force to dispatch a selection change event, clear selection before.
872 this.selectionModel_.clear();
873 this.select(nextIndex);
874 }.bind(this), 0);
878 * @param {number} direction -1 for left, 1 for right.
879 * @return {number} Next index in the given direction, with wrapping.
880 * @private
882 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
883 function advance(index, limit) {
884 index += (direction > 0 ? 1 : -1);
885 if (index < 0)
886 return limit - 1;
887 if (index === limit)
888 return 0;
889 return index;
892 // If the saved selection is multiple the Slideshow should cycle through
893 // the saved selection.
894 if (this.isSlideshowOn_() &&
895 this.savedSelection_ && this.savedSelection_.length > 1) {
896 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
897 this.savedSelection_.length);
898 return this.savedSelection_[pos];
899 } else {
900 return advance(this.getSelectedIndex(), this.getItemCount_());
905 * Advance the selection based on the pressed key ID.
906 * @param {string} keyID Key identifier.
908 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
909 var prev = (keyID === 'Up' ||
910 keyID === 'Left' ||
911 keyID === 'MediaPreviousTrack');
912 this.advanceManually(prev ? -1 : 1);
916 * Advance the selection as a result of a user action (as opposed to an
917 * automatic change in the slideshow mode).
918 * @param {number} direction -1 for left, 1 for right.
920 SlideMode.prototype.advanceManually = function(direction) {
921 if (this.isSlideshowPlaying_())
922 this.pauseSlideshow_();
923 cr.dispatchSimpleEvent(this, 'useraction');
924 this.selectNext(direction);
928 * Select the next item.
929 * @param {number} direction -1 for left, 1 for right.
931 SlideMode.prototype.selectNext = function(direction) {
932 this.select(this.getNextSelectedIndex_(direction), direction);
936 * Select the first item.
938 SlideMode.prototype.selectFirst = function() {
939 this.select(0);
943 * Select the last item.
945 SlideMode.prototype.selectLast = function() {
946 this.select(this.getItemCount_() - 1);
949 // Loading/unloading
952 * Load and display an item.
954 * @param {!Gallery.Item} item Item.
955 * @param {!ImageView.Effect} effect Transition effect object.
956 * @param {function()} displayCallback Called when the image is displayed
957 * (which can happen before the image load due to caching).
958 * @param {function(number, number)} loadCallback Called when the image is fully
959 * loaded.
960 * @private
962 SlideMode.prototype.loadItem_ = function(
963 item, effect, displayCallback, loadCallback) {
964 this.showSpinner_(true);
966 var loadDone = this.itemLoaded_.bind(this, item, loadCallback);
968 var displayDone = function() {
969 cr.dispatchSimpleEvent(this, 'image-displayed');
970 displayCallback();
971 }.bind(this);
973 this.editor_.openSession(
974 item,
975 effect,
976 this.saveCurrentImage_.bind(this, item),
977 displayDone,
978 loadDone);
982 * A callback function when the editor opens a editing session for an image.
983 * @param {!Gallery.Item} item Gallery item.
984 * @param {function(number, number)} loadCallback Called when the image is fully
985 * loaded.
986 * @param {number} loadType Load type.
987 * @param {number} delay Delay.
988 * @param {*=} opt_error Error.
989 * @private
991 SlideMode.prototype.itemLoaded_ = function(
992 item, loadCallback, loadType, delay, opt_error) {
993 var entry = item.getEntry();
995 this.showSpinner_(false);
996 if (loadType === ImageView.LoadType.ERROR) {
997 // if we have a specific error, then display it
998 if (opt_error) {
999 this.errorBanner_.show(/** @type {string} */ (opt_error));
1000 } else {
1001 // otherwise try to infer general error
1002 this.errorBanner_.show('GALLERY_IMAGE_ERROR');
1004 } else if (loadType === ImageView.LoadType.OFFLINE) {
1005 this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
1008 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
1010 var toMillions = function(number) {
1011 return Math.round(number / (1000 * 1000));
1014 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
1015 toMillions(item.getMetadataItem().size));
1017 var canvas = this.imageView_.getCanvas();
1018 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
1019 toMillions(canvas.width * canvas.height));
1021 var extIndex = entry.name.lastIndexOf('.');
1022 var ext = extIndex < 0 ? '' :
1023 entry.name.substr(extIndex + 1).toLowerCase();
1024 if (ext === 'jpeg') ext = 'jpg';
1025 ImageUtil.metrics.recordEnum(
1026 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
1028 // Enable or disable buttons for editing and printing.
1029 if (opt_error) {
1030 this.editButton_.disabled = true;
1031 this.printButton_.disabled = true;
1032 } else {
1033 this.editButton_.disabled = false;
1034 this.printButton_.disabled = false;
1037 // For once edited image, disallow the 'overwrite' setting change.
1038 ImageUtil.setAttribute(this.options_, 'saved',
1039 !this.getSelectedItem().isOriginal());
1041 chrome.storage.local.get(SlideMode.OVERWRITE_BUBBLE_KEY,
1042 function(values) {
1043 var times = values[SlideMode.OVERWRITE_BUBBLE_KEY] || 0;
1044 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
1045 this.bubble_.hidden = false;
1046 if (this.isEditing()) {
1047 var items = {};
1048 items[SlideMode.OVERWRITE_BUBBLE_KEY] = times + 1;
1049 chrome.storage.local.set(items);
1052 }.bind(this));
1054 loadCallback(loadType, delay);
1058 * Commit changes to the current item and reset all messages/indicators.
1060 * @param {function()} callback Callback.
1061 * @private
1063 SlideMode.prototype.commitItem_ = function(callback) {
1064 this.showSpinner_(false);
1065 this.errorBanner_.clear();
1066 this.editor_.getPrompt().hide();
1067 this.editor_.closeSession(callback);
1071 * Request a prefetch for the next image.
1073 * @param {number} direction -1 or 1.
1074 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1075 * loading from disrupting the animation that might be still in progress.
1077 SlideMode.prototype.requestPrefetch = function(direction, delay) {
1078 if (this.getItemCount_() <= 1) return;
1080 var index = this.getNextSelectedIndex_(direction);
1081 this.imageView_.prefetch(assert(this.getItem(index)), delay);
1084 // Event handlers.
1087 * Click handler for the image container.
1089 * @param {!Event} event Mouse click event.
1090 * @private
1092 SlideMode.prototype.onClick_ = function(event) {
1096 * Click handler for the entire document.
1097 * @param {!Event} event Mouse click event.
1098 * @private
1100 SlideMode.prototype.onDocumentClick_ = function(event) {
1101 // Events created in fakeMouseClick in test util don't pass this test.
1102 if (!window.IN_TEST)
1103 event = assertInstanceof(event, MouseEvent);
1105 var targetElement = assertInstanceof(event.target, HTMLElement);
1106 // Close the bubble if clicked outside of it and if it is visible.
1107 if (!this.bubble_.contains(targetElement) &&
1108 !this.editButton_.contains(targetElement) &&
1109 !this.arrowLeft_.contains(targetElement) &&
1110 !this.arrowRight_.contains(targetElement) &&
1111 !this.bubble_.hidden) {
1112 this.bubble_.hidden = true;
1117 * Keydown handler.
1119 * @param {!Event} event Event.
1120 * @return {boolean} True if handled.
1122 SlideMode.prototype.onKeyDown = function(event) {
1123 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
1125 if (this.isSlideshowOn_()) {
1126 switch (keyID) {
1127 case 'U+001B': // Escape exits the slideshow.
1128 case 'MediaStop':
1129 this.stopSlideshow_(event);
1130 break;
1132 case 'U+0020': // Space pauses/resumes the slideshow.
1133 case 'MediaPlayPause':
1134 this.toggleSlideshowPause_();
1135 break;
1137 case 'Up':
1138 case 'Down':
1139 case 'Left':
1140 case 'Right':
1141 case 'MediaNextTrack':
1142 case 'MediaPreviousTrack':
1143 this.advanceWithKeyboard(keyID);
1144 break;
1146 return true; // Consume all keystrokes in the slideshow mode.
1149 if (this.isEditing() && this.editor_.onKeyDown(event))
1150 return true;
1152 switch (keyID) {
1153 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
1154 if (!this.printButton_.disabled)
1155 this.print_();
1156 break;
1158 case 'U+0045': // 'e' toggles the editor.
1159 if (!this.editButton_.disabled)
1160 this.toggleEditor(event);
1161 break;
1163 case 'U+001B': // Escape
1164 if (this.isEditing()) {
1165 this.toggleEditor(event);
1166 } else if (this.viewport_.isZoomed()) {
1167 this.viewport_.resetView();
1168 this.touchHandlers_.stopOperation();
1169 this.imageView_.applyViewportChange();
1170 } else {
1171 return false; // Not handled.
1173 break;
1175 case 'Home':
1176 this.selectFirst();
1177 break;
1178 case 'End':
1179 this.selectLast();
1180 break;
1181 case 'Up':
1182 case 'Down':
1183 case 'Left':
1184 case 'Right':
1185 if (!this.isEditing() && this.viewport_.isZoomed()) {
1186 var delta = SlideMode.KEY_OFFSET_MAP[keyID];
1187 this.viewport_.setOffset(
1188 ~~(this.viewport_.getOffsetX() +
1189 delta[0] * this.viewport_.getZoom()),
1190 ~~(this.viewport_.getOffsetY() +
1191 delta[1] * this.viewport_.getZoom()));
1192 this.touchHandlers_.stopOperation();
1193 this.imageView_.applyViewportChange();
1194 } else {
1195 this.advanceWithKeyboard(keyID);
1197 break;
1198 case 'MediaNextTrack':
1199 case 'MediaPreviousTrack':
1200 this.advanceWithKeyboard(keyID);
1201 break;
1203 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
1204 if (!this.isEditing()) {
1205 this.viewport_.zoomIn();
1206 this.touchHandlers_.stopOperation();
1207 this.imageView_.applyViewportChange();
1209 break;
1211 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
1212 if (!this.isEditing()) {
1213 this.viewport_.zoomOut();
1214 this.touchHandlers_.stopOperation();
1215 this.imageView_.applyViewportChange();
1217 break;
1219 case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1220 if (!this.isEditing()) {
1221 this.viewport_.setZoom(1.0);
1222 this.touchHandlers_.stopOperation();
1223 this.imageView_.applyViewportChange();
1225 break;
1228 return true;
1232 * Resize handler.
1233 * @private
1235 SlideMode.prototype.onResize_ = function() {
1236 this.viewport_.setScreenSize(
1237 this.container_.clientWidth, this.container_.clientHeight);
1238 this.touchHandlers_.stopOperation();
1239 this.editor_.getBuffer().draw();
1243 * Update thumbnails.
1245 SlideMode.prototype.updateThumbnails = function() {
1246 this.ribbon_.reset();
1247 if (this.active_)
1248 this.ribbon_.redraw();
1251 // Saving
1254 * Save the current image to a file.
1256 * @param {!Gallery.Item} item Item to save the image.
1257 * @param {function()} callback Callback.
1258 * @private
1260 SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
1261 this.showSpinner_(true);
1263 var savedPromise = this.dataModel_.saveItem(
1264 this.volumeManager_,
1265 item,
1266 this.imageView_.getCanvas(),
1267 this.shouldOverwriteOriginal_());
1269 savedPromise.catch(function(error) {
1270 // TODO(hirono): Implement write error handling.
1271 // Until then pretend that the save succeeded.
1272 console.error(error.stack || error);
1273 }).then(function() {
1274 this.showSpinner_(false);
1275 this.flashSavedLabel_();
1277 // Allow changing the 'Overwrite original' setting only if the user
1278 // used Undo to restore the original image AND it is not a copy.
1279 // Otherwise lock the setting in its current state.
1280 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
1281 ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
1283 // Record UMA for the first edit.
1284 if (this.imageView_.getContentRevision() === 1)
1285 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
1287 callback();
1288 cr.dispatchSimpleEvent(this, 'image-saved');
1289 }.bind(this)).catch(function(error) {
1290 console.error(error.stack || error);
1295 * Flash 'Saved' label briefly to indicate that the image has been saved.
1296 * @private
1298 SlideMode.prototype.flashSavedLabel_ = function() {
1299 var setLabelHighlighted =
1300 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1301 setTimeout(setLabelHighlighted.bind(null, true), 0);
1302 setTimeout(setLabelHighlighted.bind(null, false), 300);
1306 * Local storage key for the 'Overwrite original' setting.
1307 * @type {string}
1309 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
1312 * Local storage key for the number of times that
1313 * the overwrite info bubble has been displayed.
1314 * @type {string}
1316 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1319 * Max number that the overwrite info bubble is shown.
1320 * @type {number}
1322 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1325 * @return {boolean} True if 'Overwrite original' is set.
1326 * @private
1328 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
1329 return this.overwriteOriginal_.checked;
1333 * 'Overwrite original' checkbox handler.
1334 * @param {!Event} event Event.
1335 * @private
1337 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
1338 var items = {};
1339 items[SlideMode.OVERWRITE_KEY] = event.target.checked;
1340 chrome.storage.local.set(items);
1344 * Overwrite info bubble close handler.
1345 * @private
1347 SlideMode.prototype.onCloseBubble_ = function() {
1348 this.bubble_.hidden = true;
1349 var items = {};
1350 items[SlideMode.OVERWRITE_BUBBLE_KEY] =
1351 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES;
1352 chrome.storage.local.set(items);
1355 // Slideshow
1358 * Slideshow interval in ms.
1360 SlideMode.SLIDESHOW_INTERVAL = 5000;
1363 * First slideshow interval in ms. It should be shorter so that the user
1364 * is not guessing whether the button worked.
1366 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1369 * Empirically determined duration of the fullscreen toggle animation.
1371 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1374 * @return {boolean} True if the slideshow is on.
1375 * @private
1377 SlideMode.prototype.isSlideshowOn_ = function() {
1378 return this.container_.hasAttribute('slideshow');
1382 * Starts the slideshow.
1383 * @param {number=} opt_interval First interval in ms.
1384 * @param {Event=} opt_event Event.
1386 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1387 // Reset zoom.
1388 this.viewport_.resetView();
1389 this.imageView_.applyViewportChange();
1391 // Disable touch operation.
1392 this.touchHandlers_.enabled = false;
1394 // Set the attribute early to prevent the toolbar from flashing when
1395 // the slideshow is being started from the mosaic view.
1396 this.container_.setAttribute('slideshow', 'playing');
1398 if (this.active_) {
1399 this.stopEditing_();
1400 } else {
1401 // We are in the Mosaic mode. Toggle the mode but remember to return.
1402 this.leaveAfterSlideshow_ = true;
1404 // Wait until the zoom animation from the mosaic mode is done.
1405 var startSlideshowAfterTransition = function() {
1406 setTimeout(function() {
1407 this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
1408 }.bind(this), ImageView.MODE_TRANSITION_DURATION);
1409 }.bind(this);
1410 this.toggleMode_(startSlideshowAfterTransition);
1411 return;
1414 if (opt_event) // Caused by user action, notify the Gallery.
1415 cr.dispatchSimpleEvent(this, 'useraction');
1417 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1418 if (!this.fullscreenBeforeSlideshow_) {
1419 this.toggleFullScreen_();
1420 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1421 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1424 this.resumeSlideshow_(opt_interval);
1428 * Stops the slideshow.
1429 * @param {Event=} opt_event Event.
1430 * @private
1432 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1433 if (!this.isSlideshowOn_())
1434 return;
1436 if (opt_event) // Caused by user action, notify the Gallery.
1437 cr.dispatchSimpleEvent(this, 'useraction');
1439 this.pauseSlideshow_();
1440 this.container_.removeAttribute('slideshow');
1442 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1443 var fullscreen = util.isFullScreen(this.context_.appWindow);
1444 var toggleModeDelay = 0;
1445 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1446 this.toggleFullScreen_();
1447 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1449 if (this.leaveAfterSlideshow_) {
1450 this.leaveAfterSlideshow_ = false;
1451 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1454 // Re-enable touch operation.
1455 this.touchHandlers_.enabled = true;
1459 * @return {boolean} True if the slideshow is playing (not paused).
1460 * @private
1462 SlideMode.prototype.isSlideshowPlaying_ = function() {
1463 return this.container_.getAttribute('slideshow') === 'playing';
1467 * Pauses/resumes the slideshow.
1468 * @private
1470 SlideMode.prototype.toggleSlideshowPause_ = function() {
1471 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1472 if (this.isSlideshowPlaying_()) {
1473 this.pauseSlideshow_();
1474 } else {
1475 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1480 * @param {number=} opt_interval Slideshow interval in ms.
1481 * @private
1483 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1484 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1486 if (this.slideShowTimeout_)
1487 clearTimeout(this.slideShowTimeout_);
1489 this.slideShowTimeout_ = setTimeout(function() {
1490 this.slideShowTimeout_ = null;
1491 this.selectNext(1);
1492 }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1496 * Resumes the slideshow.
1497 * @param {number=} opt_interval Slideshow interval in ms.
1498 * @private
1500 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1501 this.container_.setAttribute('slideshow', 'playing');
1502 this.scheduleNextSlide_(opt_interval);
1506 * Pauses the slideshow.
1507 * @private
1509 SlideMode.prototype.pauseSlideshow_ = function() {
1510 this.container_.setAttribute('slideshow', 'paused');
1511 if (this.slideShowTimeout_) {
1512 clearTimeout(this.slideShowTimeout_);
1513 this.slideShowTimeout_ = null;
1518 * @return {boolean} True if the editor is active.
1520 SlideMode.prototype.isEditing = function() {
1521 return this.container_.hasAttribute('editing');
1525 * Stops editing.
1526 * @private
1528 SlideMode.prototype.stopEditing_ = function() {
1529 if (this.isEditing())
1530 this.toggleEditor();
1534 * Activate/deactivate editor.
1535 * @param {Event=} opt_event Event.
1537 SlideMode.prototype.toggleEditor = function(opt_event) {
1538 if (opt_event) // Caused by user action, notify the Gallery.
1539 cr.dispatchSimpleEvent(this, 'useraction');
1541 if (!this.active_) {
1542 this.toggleMode_(this.toggleEditor.bind(this));
1543 return;
1546 this.stopSlideshow_();
1548 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1550 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1551 // Reset zoom.
1552 this.viewport_.resetView();
1553 this.imageView_.applyViewportChange();
1554 if (this.context_.readonlyDirName) {
1555 this.editor_.getPrompt().showAt(
1556 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1558 this.touchHandlers_.enabled = false;
1559 } else {
1560 this.editor_.getPrompt().hide();
1561 this.editor_.leaveModeGently();
1562 this.touchHandlers_.enabled = true;
1567 * Prints the current item.
1568 * @private
1570 SlideMode.prototype.print_ = function() {
1571 cr.dispatchSimpleEvent(this, 'useraction');
1572 window.print();
1576 * Shows/hides the busy spinner.
1578 * @param {boolean} on True if show, false if hide.
1579 * @private
1581 SlideMode.prototype.showSpinner_ = function(on) {
1582 if (this.spinnerTimer_) {
1583 clearTimeout(this.spinnerTimer_);
1584 this.spinnerTimer_ = null;
1587 if (on) {
1588 this.spinnerTimer_ = setTimeout(function() {
1589 this.spinnerTimer_ = null;
1590 ImageUtil.setAttribute(this.container_, 'spinner', true);
1591 }.bind(this), 1000);
1592 } else {
1593 ImageUtil.setAttribute(this.container_, 'spinner', false);
1598 * Apply the change of viewport.
1600 SlideMode.prototype.applyViewportChange = function() {
1601 this.imageView_.applyViewportChange();
1605 * Touch handlers of the slide mode.
1606 * @param {!Element} targetElement Event source.
1607 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1608 * @struct
1609 * @constructor
1611 function TouchHandler(targetElement, slideMode) {
1613 * Event source.
1614 * @type {!Element}
1615 * @private
1616 * @const
1618 this.targetElement_ = targetElement;
1621 * Target of touch operations.
1622 * @type {!SlideMode}
1623 * @private
1624 * @const
1626 this.slideMode_ = slideMode;
1629 * Flag to enable/disable touch operation.
1630 * @type {boolean}
1631 * @private
1633 this.enabled_ = true;
1636 * Whether it is in a touch operation that is started from targetElement or
1637 * not.
1638 * @type {boolean}
1639 * @private
1641 this.touchStarted_ = false;
1644 * The swipe action that should happen only once in an operation is already
1645 * done or not.
1646 * @type {boolean}
1647 * @private
1649 this.done_ = false;
1652 * Event on beginning of the current gesture.
1653 * The variable is updated when the number of touch finger changed.
1654 * @type {TouchEvent}
1655 * @private
1657 this.gestureStartEvent_ = null;
1660 * Rotation value on beginning of the current gesture.
1661 * @type {number}
1662 * @private
1664 this.gestureStartRotation_ = 0;
1667 * Last touch event.
1668 * @type {TouchEvent}
1669 * @private
1671 this.lastEvent_ = null;
1674 * Zoom value just after last touch event.
1675 * @type {number}
1676 * @private
1678 this.lastZoom_ = 1.0;
1680 targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
1681 var onTouchEventBound = this.onTouchEvent_.bind(this);
1682 targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
1683 targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
1685 targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
1689 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1690 * horizontally it's considered as a swipe gesture (change the current image).
1691 * @type {number}
1692 * @const
1694 TouchHandler.SWIPE_THRESHOLD = 100;
1697 * Rotation threshold in degrees.
1698 * @type {number}
1699 * @const
1701 TouchHandler.ROTATION_THRESHOLD = 25;
1704 * Obtains distance between fingers.
1705 * @param {!TouchEvent} event Touch event. It should include more than two
1706 * touches.
1707 * @return {number} Distance between touch[0] and touch[1].
1709 TouchHandler.getDistance = function(event) {
1710 var touch1 = event.touches[0];
1711 var touch2 = event.touches[1];
1712 var dx = touch1.clientX - touch2.clientX;
1713 var dy = touch1.clientY - touch2.clientY;
1714 return Math.sqrt(dx * dx + dy * dy);
1718 * Obtains the degrees of the pinch twist angle.
1719 * @param {!TouchEvent} event1 Start touch event. It should include more than
1720 * two touches.
1721 * @param {!TouchEvent} event2 Current touch event. It should include more than
1722 * two touches.
1723 * @return {number} Degrees of the pinch twist angle.
1725 TouchHandler.getTwistAngle = function(event1, event2) {
1726 var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
1727 var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
1728 var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
1729 var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
1730 var innerProduct = dx1 * dx2 + dy1 * dy2; // |v1| * |v2| * cos(t) = x / r
1731 var outerProduct = dx1 * dy2 - dy1 * dx2; // |v1| * |v2| * sin(t) = y / r
1732 return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI; // atan(y / x)
1735 TouchHandler.prototype = /** @struct */ {
1737 * @param {boolean} flag New value.
1739 set enabled(flag) {
1740 this.enabled_ = flag;
1741 if (!this.enabled_)
1742 this.stopOperation();
1747 * Stops the current touch operation.
1749 TouchHandler.prototype.stopOperation = function() {
1750 this.touchStarted_ = false;
1751 this.done_ = false;
1752 this.gestureStartEvent_ = null;
1753 this.lastEvent_ = null;
1754 this.lastZoom_ = 1.0;
1758 * Handles touch start events.
1759 * @param {!Event} event Touch event.
1760 * @private
1762 TouchHandler.prototype.onTouchStart_ = function(event) {
1763 event = assertInstanceof(event, TouchEvent);
1764 if (this.enabled_ && event.touches.length === 1)
1765 this.touchStarted_ = true;
1769 * Handles touch move and touch end events.
1770 * @param {!Event} event Touch event.
1771 * @private
1773 TouchHandler.prototype.onTouchEvent_ = function(event) {
1774 event = assertInstanceof(event, TouchEvent);
1775 // Check if the current touch operation started from the target element or
1776 // not.
1777 if (!this.touchStarted_)
1778 return;
1780 // Check if the current touch operation ends with the event.
1781 if (event.touches.length === 0) {
1782 this.stopOperation();
1783 return;
1786 // Check if a new gesture started or not.
1787 var viewport = this.slideMode_.getViewport();
1788 if (!this.lastEvent_ ||
1789 this.lastEvent_.touches.length !== event.touches.length) {
1790 if (event.touches.length === 2 ||
1791 event.touches.length === 1) {
1792 this.gestureStartEvent_ = event;
1793 this.gestureStartRotation_ = viewport.getRotation();
1794 this.lastEvent_ = event;
1795 this.lastZoom_ = viewport.getZoom();
1796 } else {
1797 this.gestureStartEvent_ = null;
1798 this.gestureStartRotation_ = 0;
1799 this.lastEvent_ = null;
1800 this.lastZoom_ = 1.0;
1802 return;
1805 // Handle the gesture movement.
1806 switch (event.touches.length) {
1807 case 1:
1808 if (viewport.isZoomed()) {
1809 // Scrolling an image by swipe.
1810 var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
1811 var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
1812 viewport.setOffset(
1813 viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
1814 this.slideMode_.applyViewportChange();
1815 } else {
1816 // Traversing images by swipe.
1817 if (this.done_)
1818 break;
1819 var dx =
1820 event.touches[0].clientX -
1821 this.gestureStartEvent_.touches[0].clientX;
1822 if (dx > TouchHandler.SWIPE_THRESHOLD) {
1823 this.slideMode_.advanceManually(-1);
1824 this.done_ = true;
1825 } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
1826 this.slideMode_.advanceManually(1);
1827 this.done_ = true;
1830 break;
1832 case 2:
1833 // Pinch zoom.
1834 var distance1 = TouchHandler.getDistance(this.lastEvent_);
1835 var distance2 = TouchHandler.getDistance(event);
1836 if (distance1 === 0)
1837 break;
1838 var zoom = distance2 / distance1 * this.lastZoom_;
1839 viewport.setZoom(zoom);
1841 // Pinch rotation.
1842 assert(this.gestureStartEvent_);
1843 var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
1844 if (angle > TouchHandler.ROTATION_THRESHOLD)
1845 viewport.setRotation(this.gestureStartRotation_ + 1);
1846 else if (angle < -TouchHandler.ROTATION_THRESHOLD)
1847 viewport.setRotation(this.gestureStartRotation_ - 1);
1848 else
1849 viewport.setRotation(this.gestureStartRotation_);
1850 this.slideMode_.applyViewportChange();
1851 break;
1854 // Update the last event.
1855 this.lastEvent_ = event;
1856 this.lastZoom_ = viewport.getZoom();
1860 * Handles mouse wheel events.
1861 * @param {!Event} event Wheel event.
1862 * @private
1864 TouchHandler.prototype.onMouseWheel_ = function(event) {
1865 var event = assertInstanceof(event, MouseEvent);
1866 var viewport = this.slideMode_.getViewport();
1867 if (!this.enabled_ || !viewport.isZoomed())
1868 return;
1869 this.stopOperation();
1870 viewport.setOffset(
1871 viewport.getOffsetX() + event.wheelDeltaX,
1872 viewport.getOffsetY() + event.wheelDeltaY);
1873 this.slideMode_.applyViewportChange();