Roll src/third_party/WebKit a3b4a2e:7441784 (svn 202551:202552)
[chromium-blink-merge.git] / ui / file_manager / gallery / js / slide_mode.js
blob025e128ab602ff02befe4d607a5c375b732525cb
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} topToolbar Top toolbar element.
12 * @param {!HTMLElement} bottomToolbar Toolbar element.
13 * @param {!ImageEditor.Prompt} prompt Prompt.
14 * @param {!ErrorBanner} errorBanner Error banner.
15 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
16 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
17 * @param {!MetadataModel} metadataModel
18 * @param {!ThumbnailModel} thumbnailModel
19 * @param {!Object} context Context.
20 * @param {!VolumeManagerWrapper} volumeManager Volume manager.
21 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
22 * @param {function(string):string} displayStringFunction String formatting
23 * function.
24 * @param {!DimmableUIController} dimmableUIController Dimmable UI controller.
25 * @constructor
26 * @struct
27 * @extends {cr.EventTarget}
29 function SlideMode(container, content, topToolbar, bottomToolbar, prompt,
30 errorBanner, dataModel, selectionModel, metadataModel, thumbnailModel,
31 context, volumeManager, toggleMode, displayStringFunction,
32 dimmableUIController) {
33 /**
34 * @type {!HTMLElement}
35 * @private
36 * @const
38 this.container_ = container;
40 /**
41 * @type {!Document}
42 * @private
43 * @const
45 this.document_ = assert(container.ownerDocument);
47 /**
48 * @type {!HTMLElement}
49 * @const
51 this.content = content;
53 /**
54 * @type {!HTMLElement}
55 * @private
56 * @const
58 this.topToolbar_ = topToolbar;
60 /**
61 * @type {!HTMLElement}
62 * @private
63 * @const
65 this.bottomToolbar_ = bottomToolbar;
67 /**
68 * @type {!ImageEditor.Prompt}
69 * @private
70 * @const
72 this.prompt_ = prompt;
74 /**
75 * @type {!ErrorBanner}
76 * @private
77 * @const
79 this.errorBanner_ = errorBanner;
81 /**
82 * @type {!cr.ui.ArrayDataModel}
83 * @private
84 * @const
86 this.dataModel_ = dataModel;
88 /**
89 * @type {!cr.ui.ListSelectionModel}
90 * @private
91 * @const
93 this.selectionModel_ = selectionModel;
95 /**
96 * @type {!Object}
97 * @private
98 * @const
100 this.context_ = context;
103 * @type {!VolumeManagerWrapper}
104 * @private
105 * @const
107 this.volumeManager_ = volumeManager;
110 * @type {function(function())}
111 * @private
112 * @const
114 this.toggleMode_ = toggleMode;
117 * @type {function(string):string}
118 * @private
119 * @const
121 this.displayStringFunction_ = displayStringFunction;
124 * @private {!DimmableUIController}
125 * @const
127 this.dimmableUIController_ = dimmableUIController;
130 * @type {function(this:SlideMode)}
131 * @private
132 * @const
134 this.onSelectionBound_ = this.onSelection_.bind(this);
137 * @type {function(this:SlideMode,!Event)}
138 * @private
139 * @const
141 this.onSpliceBound_ = this.onSplice_.bind(this);
144 * Unique numeric key, incremented per each load attempt used to discard
145 * old attempts. This can happen especially when changing selection fast or
146 * Internet connection is slow.
148 * @type {number}
149 * @private
151 this.currentUniqueKey_ = 0;
154 * @type {number}
155 * @private
157 this.sequenceDirection_ = 0;
160 * @type {number}
161 * @private
163 this.sequenceLength_ = 0;
166 * @type {Array<number>}
167 * @private
169 this.savedSelection_ = null;
172 * @type {Gallery.Item}
173 * @private
175 this.displayedItem_ = null;
178 * @type {?number}
179 * @private
181 this.slideHint_ = null;
184 * @type {boolean}
185 * @private
187 this.active_ = false;
190 * @type {boolean}
191 * @private
193 this.leaveAfterSlideshow_ = false;
196 * @type {boolean}
197 * @private
199 this.fullscreenBeforeSlideshow_ = false;
202 * @type {?number}
203 * @private
205 this.slideShowTimeout_ = null;
208 * @type {?number}
209 * @private
211 this.spinnerTimer_ = null;
213 window.addEventListener('resize', this.onResize_.bind(this));
215 // ----------------------------------------------------------------
216 // Initializes the UI.
219 * Container for displayed image.
220 * @type {!HTMLElement}
221 * @private
222 * @const
224 this.imageContainer_ = util.createChild(queryRequiredElement(
225 '.content', this.document_), 'image-container');
227 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
230 * Overwrite options and info bubble.
231 * @type {!HTMLElement}
232 * @private
233 * @const
235 this.options_ = queryRequiredElement('.options', this.bottomToolbar_);
238 * @type {!HTMLElement}
239 * @private
240 * @const
242 this.savedLabel_ = queryRequiredElement('.saved', this.options_);
245 * @private {!PaperCheckboxElement}
246 * @const
248 this.overwriteOriginalCheckbox_ = /** @type {!PaperCheckboxElement} */
249 (queryRequiredElement('.overwrite-original', this.options_));
250 this.overwriteOriginalCheckbox_.addEventListener('change',
251 this.onOverwriteOriginalCheckboxChanged_.bind(this));
254 * @private {!FilesToast}
255 * @const
257 this.filesToast_ = /** @type {!FilesToast} */
258 (queryRequiredElement('files-toast'));
261 * @private {!HTMLElement}
262 * @const
264 this.bubble_ = queryRequiredElement('.bubble', this.bottomToolbar_);
266 var bubbleContent = queryRequiredElement('.content', this.bubble_);
267 // GALLERY_OVERWRITE_BUBBLE contains <br> tag inside message.
268 bubbleContent.innerHTML = strf('GALLERY_OVERWRITE_BUBBLE');
270 var bubbleClose = queryRequiredElement('.close-x', this.bubble_);
271 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
274 * Ribbon and related controls.
275 * @type {!HTMLElement}
276 * @private
277 * @const
279 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
282 * @type {!HTMLElement}
283 * @private
284 * @const
286 this.arrowLeft_ = util.createChild(
287 this.arrowBox_, 'arrow left tool dimmable');
288 this.arrowLeft_.addEventListener('click',
289 this.advanceManually.bind(this, -1));
290 util.createChild(this.arrowLeft_);
293 * @type {!HTMLElement}
294 * @private
295 * @const
297 this.arrowRight_ = util.createChild(
298 this.arrowBox_, 'arrow right tool dimmable');
299 this.arrowRight_.addEventListener('click',
300 this.advanceManually.bind(this, 1));
301 util.createChild(this.arrowRight_);
304 * @type {!HTMLElement}
305 * @private
306 * @const
308 this.ribbonSpacer_ = queryRequiredElement('.ribbon-spacer',
309 this.bottomToolbar_);
312 * @type {!Ribbon}
313 * @private
314 * @const
316 this.ribbon_ = new Ribbon(this.document_, window, this.dataModel_,
317 this.selectionModel_, thumbnailModel);
318 this.ribbonSpacer_.appendChild(this.ribbon_);
320 util.createChild(this.container_, 'spinner');
323 * @type {!HTMLElement}
324 * @const
326 var slideShowButton = queryRequiredElement('paper-button.slideshow',
327 this.topToolbar_);
328 slideShowButton.addEventListener('click',
329 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
332 * @type {!HTMLElement}
333 * @const
335 var slideShowToolbar = util.createChild(
336 this.container_, 'tool slideshow-toolbar');
337 util.createChild(slideShowToolbar, 'slideshow-play').
338 addEventListener('click', this.toggleSlideshowPause_.bind(this));
339 util.createChild(slideShowToolbar, 'slideshow-end').
340 addEventListener('click', this.stopSlideshow_.bind(this));
342 // Editor.
344 * @type {!HTMLElement}
345 * @private
346 * @const
348 this.editButton_ = queryRequiredElement('button.edit', this.topToolbar_);
349 GalleryUtil.decorateMouseFocusHandling(this.editButton_);
350 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
353 * @private {!FilesToggleRipple}
354 * @const
356 this.editButtonToggleRipple_ = /** @type {!FilesToggleRipple} */
357 (assert(this.editButton_.querySelector('files-toggle-ripple')));
360 * @type {!HTMLElement}
361 * @private
362 * @const
364 this.printButton_ = queryRequiredElement('paper-button.print',
365 this.topToolbar_);
366 this.printButton_.addEventListener('click', this.print_.bind(this));
369 * @type {!HTMLElement}
370 * @private
371 * @const
373 this.editBarSpacer_ = queryRequiredElement('.edit-bar-spacer',
374 this.bottomToolbar_);
377 * @type {!HTMLElement}
378 * @private
379 * @const
381 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
384 * @type {!HTMLElement}
385 * @private
386 * @const
388 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
391 * @type {!HTMLElement}
392 * @private
393 * @const
395 this.editBarModeWrapper_ = util.createChild(
396 this.editBarMode_, 'edit-modal-wrapper dimmable');
397 this.editBarModeWrapper_.hidden = true;
400 * Objects supporting image display and editing.
401 * @type {!Viewport}
402 * @private
403 * @const
405 this.viewport_ = new Viewport(window);
406 this.viewport_.addEventListener('resize', this.onViewportResize_.bind(this));
409 * @type {!ImageView}
410 * @private
411 * @const
413 this.imageView_ = new ImageView(
414 this.imageContainer_,
415 this.viewport_,
416 metadataModel);
419 * @type {!ImageEditor}
420 * @private
421 * @const
423 this.editor_ = new ImageEditor(
424 this.viewport_,
425 this.imageView_,
426 this.prompt_,
428 root: this.container_,
429 image: this.imageContainer_,
430 toolbar: this.editBarMain_,
431 mode: this.editBarModeWrapper_
433 SlideMode.EDITOR_MODES,
434 this.displayStringFunction_);
435 this.editor_.addEventListener('exit-clicked', this.onExitClicked_.bind(this));
438 * @type {!TouchHandler}
439 * @private
440 * @const
442 this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
445 * @private {!ChromeVoxStateWatcher}
446 * @const
448 this.chromeVoxStateWatcher_ = new ChromeVoxStateWatcher();
449 this.chromeVoxStateWatcher_.addEventListener('chromevox-navigation-begin',
450 this.onChromeVoxNavigationBegin_.bind(this));
451 this.chromeVoxStateWatcher_.addEventListener('chromevox-navigation-end',
452 this.onChromeVoxNavigationEnd_.bind(this));
456 * List of available editor modes.
457 * @type {!Array<ImageEditor.Mode>}
458 * @const
460 SlideMode.EDITOR_MODES = [
461 new ImageEditor.Mode.InstantAutofix(),
462 new ImageEditor.Mode.Crop(),
463 new ImageEditor.Mode.Exposure(),
464 new ImageEditor.Mode.OneClick(
465 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
466 new ImageEditor.Mode.OneClick(
467 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
471 * Map of the key identifier and offset delta.
472 * @enum {!Array<number>})
473 * @const
475 SlideMode.KEY_OFFSET_MAP = {
476 'Up': [0, 20],
477 'Down': [0, -20],
478 'Left': [20, 0],
479 'Right': [-20, 0]
483 * Returns editor warning message if it should be shown.
484 * @param {!Gallery.Item} item
485 * @param {string} readonlyDirName Name of read only volume. Pass empty string
486 * if volume is writable.
487 * @param {!DirectoryEntry} fallbackSaveDirectory
488 * @return {!Promise<?string>} Warning message. null if no warning message
489 * should be shown.
491 SlideMode.getEditorWarningMessage = function(
492 item, readonlyDirName, fallbackSaveDirectory) {
493 var isReadOnlyVolume = !!readonlyDirName;
494 var isWritableFormat = item.isWritableFormat();
496 if (isReadOnlyVolume && !isWritableFormat) {
497 return item.getCopyName(fallbackSaveDirectory).then(function(copyName) {
498 return strf('GALLERY_READONLY_AND_NON_WRITABLE_FORMAT_WARNING',
499 readonlyDirName, copyName);
501 } else if (isReadOnlyVolume) {
502 return Promise.resolve(/** @type {?string} */
503 (strf('GALLERY_READONLY_WARNING', readonlyDirName)));
504 } else if (!isWritableFormat) {
505 var entry = item.getEntry();
506 return new Promise(entry.getParent.bind(entry)).then(function(parentDir) {
507 return item.getCopyName(parentDir);
508 }).then(function(copyName) {
509 return strf('GALLERY_NON_WRITABLE_FORMAT_WARNING', copyName);
511 } else {
512 return Promise.resolve(/** @type {?string} */ (null));
517 * SlideMode extends cr.EventTarget.
519 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
522 * Handles chromevox-navigation-begin event. While user is navigating with
523 * ChromeVox, we should not hide the tools.
524 * @private
526 SlideMode.prototype.onChromeVoxNavigationBegin_ = function() {
527 this.dimmableUIController_.setDisabled(true);
531 * Handles chromevox-navigation-end event.
532 * @private
534 SlideMode.prototype.onChromeVoxNavigationEnd_ = function() {
535 this.dimmableUIController_.setDisabled(false);
539 * Handles exit-clicked event.
540 * @private
542 SlideMode.prototype.onExitClicked_ = function() {
543 if (this.isEditing())
544 this.toggleEditor();
548 * @return {string} Mode name.
550 SlideMode.prototype.getName = function() { return 'slide'; };
553 * @return {string} Mode title.
555 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
558 * @return {!Viewport} Viewport.
560 SlideMode.prototype.getViewport = function() { return this.viewport_; };
563 * Load items, display the selected item.
564 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
565 * @param {function()} displayCallback Called when the image is displayed.
566 * @param {function()} loadCallback Called when the image is displayed.
568 SlideMode.prototype.enter = function(
569 zoomFromRect, displayCallback, loadCallback) {
570 this.sequenceDirection_ = 0;
571 this.sequenceLength_ = 0;
573 // The latest |leave| call might have left the image animating. Remove it.
574 this.unloadImage_();
575 this.errorBanner_.clear();
577 new Promise(function(fulfill) {
578 // If the items are empty, just show the error message.
579 if (this.getItemCount_() === 0) {
580 this.displayedItem_ = null;
581 this.errorBanner_.show('GALLERY_NO_IMAGES');
582 fulfill();
583 return;
586 // Remember the selection if it is empty or multiple. It will be restored
587 // in |leave| if the user did not changing the selection manually.
588 var currentSelection = this.selectionModel_.selectedIndexes;
589 if (currentSelection.length === 1)
590 this.savedSelection_ = null;
591 else
592 this.savedSelection_ = currentSelection;
594 // Ensure valid single selection.
595 // Note that the SlideMode object is not listening to selection change yet.
596 this.select(Math.max(0, this.getSelectedIndex()));
598 // Show the selected item ASAP, then complete the initialization
599 // (loading the ribbon thumbnails can take some time).
600 var selectedItem = this.getSelectedItem();
601 this.displayedItem_ = selectedItem;
603 // Load the image of the item.
604 this.loadItem_(
605 assert(selectedItem),
606 zoomFromRect ?
607 this.imageView_.createZoomEffect(zoomFromRect) :
608 new ImageView.Effect.None(),
609 displayCallback,
610 function(loadType, delay) {
611 fulfill(delay);
613 }.bind(this)).then(function(delay) {
614 // Turn the mode active.
615 this.active_ = true;
616 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
617 this.ribbon_.enable();
619 // Register handlers.
620 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
621 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
622 this.touchHandlers_.enabled = true;
624 // Wait 1000ms after the animation is done, then prefetch the next image.
625 this.requestPrefetch(1, delay + 1000);
627 // Call load callback.
628 if (loadCallback)
629 loadCallback();
630 }.bind(this)).catch(function(error) {
631 console.error(error.stack, error);
636 * Leave the mode.
637 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
638 * @param {function()} callback Called when the image is committed and
639 * the zoom-out animation has started.
641 SlideMode.prototype.leave = function(zoomToRect, callback) {
642 var commitDone = function() {
643 this.stopEditing_();
644 this.stopSlideshow_();
645 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
646 this.selectionModel_.removeEventListener(
647 'change', this.onSelectionBound_);
648 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
649 this.ribbon_.disable();
650 this.active_ = false;
651 if (this.savedSelection_)
652 this.selectionModel_.selectedIndexes = this.savedSelection_;
653 this.unloadImage_(zoomToRect);
654 callback();
655 }.bind(this);
657 this.viewport_.resetView();
658 if (this.getItemCount_() === 0) {
659 this.errorBanner_.clear();
660 commitDone();
661 } else {
662 this.commitItem_(commitDone);
665 // Disable the slide-mode only buttons when leaving.
666 this.editButton_.disabled = true;
667 this.printButton_.disabled = true;
669 // Disable touch operation.
670 this.touchHandlers_.enabled = false;
675 * Execute an action when the editor is not busy.
677 * @param {function()} action Function to execute.
679 SlideMode.prototype.executeWhenReady = function(action) {
680 this.editor_.executeWhenReady(action);
684 * @return {boolean} True if the mode has active tools (that should not fade).
686 SlideMode.prototype.hasActiveTool = function() {
687 return this.isEditing();
691 * @return {number} Item count.
692 * @private
694 SlideMode.prototype.getItemCount_ = function() {
695 return this.dataModel_.length;
699 * @param {number} index Index.
700 * @return {Gallery.Item} Item.
702 SlideMode.prototype.getItem = function(index) {
703 var item =
704 /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
705 return item === undefined ? null : item;
709 * @return {number} Selected index.
711 SlideMode.prototype.getSelectedIndex = function() {
712 return this.selectionModel_.selectedIndex;
716 * @return {ImageRect} Screen rectangle of the selected image.
718 SlideMode.prototype.getSelectedImageRect = function() {
719 if (this.getSelectedIndex() < 0)
720 return null;
721 else
722 return this.viewport_.getImageBoundsOnScreen();
726 * @return {Gallery.Item} Selected item.
728 SlideMode.prototype.getSelectedItem = function() {
729 return this.getItem(this.getSelectedIndex());
733 * Toggles the full screen mode.
734 * @private
736 SlideMode.prototype.toggleFullScreen_ = function() {
737 util.toggleFullScreen(this.context_.appWindow,
738 !util.isFullScreen(this.context_.appWindow));
742 * Selection change handler.
744 * Commits the current image and displays the newly selected image.
745 * @private
747 SlideMode.prototype.onSelection_ = function() {
748 if (this.selectionModel_.selectedIndexes.length === 0)
749 return; // Ignore temporary empty selection.
751 // Forget the saved selection if the user changed the selection manually.
752 if (!this.isSlideshowOn_())
753 this.savedSelection_ = null;
755 if (this.getSelectedItem() === this.displayedItem_)
756 return; // Do not reselect.
758 this.commitItem_(this.loadSelectedItem_.bind(this));
762 * Change the selection.
764 * @param {number} index New selected index.
765 * @param {number=} opt_slideHint Slide animation direction (-1|1).
767 SlideMode.prototype.select = function(index, opt_slideHint) {
768 this.slideHint_ = opt_slideHint || null;
769 this.selectionModel_.selectedIndex = index;
770 this.selectionModel_.leadIndex = index;
774 * Load the selected item.
776 * @private
778 SlideMode.prototype.loadSelectedItem_ = function() {
779 var slideHint = this.slideHint_;
780 this.slideHint_ = null;
782 if (this.getSelectedItem() === this.displayedItem_)
783 return; // Do not reselect.
785 var index = this.getSelectedIndex();
786 if (index < 0)
787 return;
789 var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
790 var step =
791 slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);
793 if (Math.abs(step) != 1) {
794 // Long leap, the sequence is broken, we have no good prefetch candidate.
795 this.sequenceDirection_ = 0;
796 this.sequenceLength_ = 0;
797 } else if (this.sequenceDirection_ === step) {
798 // Keeping going in sequence.
799 this.sequenceLength_++;
800 } else {
801 // Reversed the direction. Reset the counter.
802 this.sequenceDirection_ = step;
803 this.sequenceLength_ = 1;
806 this.displayedItem_ = this.getSelectedItem();
807 var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);
809 function shouldPrefetch(loadType, step, sequenceLength) {
810 // Never prefetch when selecting out of sequence.
811 if (Math.abs(step) != 1)
812 return false;
814 // Always prefetch if the previous load was from cache.
815 if (loadType === ImageView.LoadType.CACHED_FULL)
816 return true;
818 // Prefetch if we have been going in the same direction for long enough.
819 return sequenceLength >= 3;
822 this.currentUniqueKey_++;
823 var selectedUniqueKey = this.currentUniqueKey_;
825 // Discard, since another load has been invoked after this one.
826 if (selectedUniqueKey != this.currentUniqueKey_)
827 return;
829 this.loadItem_(
830 selectedItem,
831 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
832 function() {} /* no displayCallback */,
833 function(loadType, delay) {
834 // Discard, since another load has been invoked after this one.
835 if (selectedUniqueKey != this.currentUniqueKey_)
836 return;
837 if (shouldPrefetch(loadType, step, this.sequenceLength_))
838 this.requestPrefetch(step, delay);
839 if (this.isSlideshowPlaying_())
840 this.scheduleNextSlide_();
841 }.bind(this));
845 * Unload the current image.
847 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
848 * @private
850 SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
851 this.imageView_.unload(opt_zoomToRect);
855 * Data model 'splice' event handler.
856 * @param {!Event} event Event.
857 * @this {SlideMode}
858 * @private
860 SlideMode.prototype.onSplice_ = function(event) {
861 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
863 // Splice invalidates saved indices, drop the saved selection.
864 this.savedSelection_ = null;
866 if (event.removed.length != 1)
867 return;
869 // Delay the selection to let the ribbon splice handler work first.
870 setTimeout(function() {
871 if (this.dataModel_.length === 0) {
872 // No items left. Unload the image, disable edit and print button, and
873 // show the banner.
874 this.commitItem_(function() {
875 this.unloadImage_();
876 this.printButton_.disabled = true;
877 this.editButton_.disabled = true;
878 this.errorBanner_.show('GALLERY_NO_IMAGES');
879 }.bind(this));
880 return;
883 var displayedItemNotRemvoed = event.removed.every(function(item) {
884 return item !== this.displayedItem_;
885 }.bind(this));
886 if (!displayedItemNotRemvoed) {
887 // There is the next item, select it. Otherwise, select the last item.
888 var nextIndex = Math.min(event.index, this.dataModel_.length - 1);
889 // To force to dispatch a selection change event, clear selection before.
890 this.selectionModel_.clear();
891 this.select(nextIndex);
893 }.bind(this), 0);
897 * @param {number} direction -1 for left, 1 for right.
898 * @return {number} Next index in the given direction, with wrapping.
899 * @private
901 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
902 function advance(index, limit) {
903 index += (direction > 0 ? 1 : -1);
904 if (index < 0)
905 return limit - 1;
906 if (index === limit)
907 return 0;
908 return index;
911 // If the saved selection is multiple the Slideshow should cycle through
912 // the saved selection.
913 if (this.isSlideshowOn_() &&
914 this.savedSelection_ && this.savedSelection_.length > 1) {
915 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
916 this.savedSelection_.length);
917 return this.savedSelection_[pos];
918 } else {
919 return advance(this.getSelectedIndex(), this.getItemCount_());
924 * Advance the selection based on the pressed key ID.
925 * @param {string} keyID Key identifier.
927 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
928 var prev = (keyID === 'Up' ||
929 keyID === 'Left' ||
930 keyID === 'MediaPreviousTrack');
931 this.advanceManually(prev ? -1 : 1);
935 * Advance the selection as a result of a user action (as opposed to an
936 * automatic change in the slideshow mode).
937 * @param {number} direction -1 for left, 1 for right.
939 SlideMode.prototype.advanceManually = function(direction) {
940 if (this.isSlideshowPlaying_())
941 this.pauseSlideshow_();
942 cr.dispatchSimpleEvent(this, 'useraction');
943 this.selectNext(direction);
947 * Select the next item.
948 * @param {number} direction -1 for left, 1 for right.
950 SlideMode.prototype.selectNext = function(direction) {
951 this.select(this.getNextSelectedIndex_(direction), direction);
955 * Select the first item.
957 SlideMode.prototype.selectFirst = function() {
958 this.select(0);
962 * Select the last item.
964 SlideMode.prototype.selectLast = function() {
965 this.select(this.getItemCount_() - 1);
968 // Loading/unloading
971 * Load and display an item.
973 * @param {!Gallery.Item} item Item.
974 * @param {!ImageView.Effect} effect Transition effect object.
975 * @param {function()} displayCallback Called when the image is displayed
976 * (which can happen before the image load due to caching).
977 * @param {function(number, number)} loadCallback Called when the image is fully
978 * loaded.
979 * @private
981 SlideMode.prototype.loadItem_ = function(
982 item, effect, displayCallback, loadCallback) {
983 this.showSpinner_(true);
985 var loadDone = this.itemLoaded_.bind(this, item, loadCallback);
987 var displayDone = function() {
988 cr.dispatchSimpleEvent(this, 'image-displayed');
989 displayCallback();
990 }.bind(this);
992 this.editor_.openSession(
993 item,
994 effect,
995 this.saveCurrentImage_.bind(this, item),
996 displayDone,
997 loadDone);
1001 * A callback function when the editor opens a editing session for an image.
1002 * @param {!Gallery.Item} item Gallery item.
1003 * @param {function(number, number)} loadCallback Called when the image is fully
1004 * loaded.
1005 * @param {number} loadType Load type.
1006 * @param {number} delay Delay.
1007 * @param {*=} opt_error Error.
1008 * @private
1010 SlideMode.prototype.itemLoaded_ = function(
1011 item, loadCallback, loadType, delay, opt_error) {
1012 var entry = item.getEntry();
1014 this.showSpinner_(false);
1015 if (loadType === ImageView.LoadType.ERROR) {
1016 // if we have a specific error, then display it
1017 if (opt_error) {
1018 this.errorBanner_.show(/** @type {string} */ (opt_error));
1019 } else {
1020 // otherwise try to infer general error
1021 this.errorBanner_.show('GALLERY_IMAGE_ERROR');
1023 } else if (loadType === ImageView.LoadType.OFFLINE) {
1024 this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
1027 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
1029 var toMillions = function(number) {
1030 return Math.round(number / (1000 * 1000));
1033 var metadata = item.getMetadataItem();
1034 if (metadata) {
1035 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
1036 toMillions(metadata.size));
1039 var canvas = this.imageView_.getCanvas();
1040 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
1041 toMillions(canvas.width * canvas.height));
1043 var extIndex = entry.name.lastIndexOf('.');
1044 var ext = extIndex < 0 ? '' :
1045 entry.name.substr(extIndex + 1).toLowerCase();
1046 if (ext === 'jpeg') ext = 'jpg';
1047 ImageUtil.metrics.recordEnum(
1048 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
1050 // Enable or disable buttons for editing and printing.
1051 if (opt_error) {
1052 this.editButton_.disabled = true;
1053 this.printButton_.disabled = true;
1054 } else {
1055 this.editButton_.disabled = false;
1056 this.printButton_.disabled = false;
1059 // Saved label is hidden by default.
1060 this.savedLabel_.hidden = true;
1062 // Disable overwrite original checkbox until settings is loaded.
1063 this.overwriteOriginalCheckbox_.disabled = true;
1064 this.overwriteOriginalCheckbox_.checked = false;
1066 var keys = {};
1067 keys[SlideMode.OVERWRITE_ORIGINAL_KEY] = true;
1068 chrome.storage.local.get(keys,
1069 function(values) {
1070 // Users can overwrite original file only if loaded image is original
1071 // and writable.
1072 if (item.isOriginal() &&
1073 item.isWritableFile(this.volumeManager_)) {
1074 this.overwriteOriginalCheckbox_.disabled = false;
1075 this.overwriteOriginalCheckbox_.checked =
1076 values[SlideMode.OVERWRITE_ORIGINAL_KEY];
1078 }.bind(this));
1080 loadCallback(loadType, delay);
1084 * Commit changes to the current item and reset all messages/indicators.
1086 * @param {function()} callback Callback.
1087 * @private
1089 SlideMode.prototype.commitItem_ = function(callback) {
1090 this.showSpinner_(false);
1091 this.errorBanner_.clear();
1092 this.editor_.getPrompt().hide();
1093 this.editor_.closeSession(callback);
1097 * Request a prefetch for the next image.
1099 * @param {number} direction -1 or 1.
1100 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1101 * loading from disrupting the animation that might be still in progress.
1103 SlideMode.prototype.requestPrefetch = function(direction, delay) {
1104 if (this.getItemCount_() <= 1) return;
1106 var index = this.getNextSelectedIndex_(direction);
1107 this.imageView_.prefetch(assert(this.getItem(index)), delay);
1110 // Event handlers.
1113 * Click handler for the entire document.
1114 * @param {!Event} event Mouse click event.
1115 * @private
1117 SlideMode.prototype.onDocumentClick_ = function(event) {
1118 // Events created in fakeMouseClick in test util don't pass this test.
1119 if (!window.IN_TEST)
1120 event = assertInstanceof(event, MouseEvent);
1122 var targetElement = assertInstanceof(event.target, HTMLElement);
1123 // Close the bubble if clicked outside of it and if it is visible.
1124 if (!this.bubble_.contains(targetElement) &&
1125 !this.editButton_.contains(targetElement) &&
1126 !this.arrowLeft_.contains(targetElement) &&
1127 !this.arrowRight_.contains(targetElement) &&
1128 !this.bubble_.hidden) {
1129 this.bubble_.hidden = true;
1134 * Keydown handler.
1136 * @param {!Event} event Event.
1137 * @return {boolean} True if handled.
1139 SlideMode.prototype.onKeyDown = function(event) {
1140 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
1142 if (this.isSlideshowOn_()) {
1143 switch (keyID) {
1144 case 'U+001B': // Escape
1145 case 'MediaStop':
1146 this.stopSlideshow_(event);
1147 break;
1149 case 'U+0020': // Space pauses/resumes the slideshow.
1150 case 'MediaPlayPause':
1151 this.toggleSlideshowPause_();
1152 break;
1154 case 'Up':
1155 case 'Down':
1156 case 'Left':
1157 case 'Right':
1158 case 'MediaNextTrack':
1159 case 'MediaPreviousTrack':
1160 this.advanceWithKeyboard(keyID);
1161 break;
1163 return true; // Consume all keystrokes in the slideshow mode.
1166 // Handles shortcut keys common for both modes (editing and not-editing).
1167 switch (keyID) {
1168 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
1169 if (!this.printButton_.disabled)
1170 this.print_();
1171 return true;
1173 case 'U+0045': // 'e' toggles the editor.
1174 if (!this.editButton_.disabled)
1175 this.toggleEditor(event);
1176 return true;
1179 // Handles shortcurt keys for editing mode.
1180 if (this.isEditing()) {
1181 if (this.editor_.onKeyDown(event))
1182 return true;
1184 if (keyID === 'U+001B') { // Escape
1185 this.toggleEditor(event);
1186 return true;
1189 return false;
1192 // Handles shortcut keys for not-editing mode.
1193 switch (keyID) {
1194 case 'U+001B': // Escape
1195 if (this.viewport_.isZoomed()) {
1196 this.viewport_.resetView();
1197 this.touchHandlers_.stopOperation();
1198 this.imageView_.applyViewportChange();
1199 return true;
1201 break;
1203 case 'Home':
1204 this.selectFirst();
1205 return true;
1207 case 'End':
1208 this.selectLast();
1209 return true;
1211 case 'Up':
1212 case 'Down':
1213 case 'Left':
1214 case 'Right':
1215 if (this.viewport_.isZoomed()) {
1216 var delta = SlideMode.KEY_OFFSET_MAP[keyID];
1217 this.viewport_.setOffset(
1218 ~~(this.viewport_.getOffsetX() +
1219 delta[0] * this.viewport_.getZoom()),
1220 ~~(this.viewport_.getOffsetY() +
1221 delta[1] * this.viewport_.getZoom()));
1222 this.touchHandlers_.stopOperation();
1223 this.imageView_.applyViewportChange();
1224 } else {
1225 this.advanceWithKeyboard(keyID);
1227 return true;
1229 case 'MediaNextTrack':
1230 case 'MediaPreviousTrack':
1231 this.advanceWithKeyboard(keyID);
1232 return true;
1234 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
1235 this.viewport_.zoomIn();
1236 this.touchHandlers_.stopOperation();
1237 this.imageView_.applyViewportChange();
1238 return true;
1240 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
1241 this.viewport_.zoomOut();
1242 this.touchHandlers_.stopOperation();
1243 this.imageView_.applyViewportChange();
1244 return true;
1246 case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1247 this.viewport_.setZoom(1.0);
1248 this.touchHandlers_.stopOperation();
1249 this.imageView_.applyViewportChange();
1250 return true;
1253 return false;
1257 * Resize handler.
1258 * @private
1260 SlideMode.prototype.onResize_ = function() {
1261 this.touchHandlers_.stopOperation();
1265 * Handles resize event of viewport.
1266 * @private
1268 SlideMode.prototype.onViewportResize_ = function() {
1269 // This method must be called after the resize of viewport.
1270 this.editor_.getBuffer().draw();
1274 * Update thumbnails.
1276 SlideMode.prototype.updateThumbnails = function() {
1277 this.ribbon_.reset();
1278 if (this.active_)
1279 this.ribbon_.redraw();
1282 // Saving
1285 * Save the current image to a file.
1287 * @param {!Gallery.Item} item Item to save the image.
1288 * @param {function()} callback Callback.
1289 * @private
1291 SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
1292 this.showSpinner_(true);
1294 var savedPromise = this.dataModel_.saveItem(
1295 this.volumeManager_,
1296 item,
1297 this.imageView_.getCanvas(),
1298 this.overwriteOriginalCheckbox_.checked);
1300 savedPromise.then(function() {
1301 this.showSpinner_(false);
1302 this.flashSavedLabel_();
1304 // Record UMA for the first edit.
1305 if (this.imageView_.getContentRevision() === 1)
1306 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
1308 // Users can change overwrite original setting only if there is no undo
1309 // stack and item is original and writable.
1310 var ableToChangeOverwriteOriginalSetting = !this.editor_.canUndo() &&
1311 item.isOriginal() && item.isWritableFile(this.volumeManager_);
1312 this.overwriteOriginalCheckbox_.disabled =
1313 !ableToChangeOverwriteOriginalSetting;
1315 callback();
1316 }.bind(this)).catch(function(error) {
1317 console.error(error.stack || error);
1319 this.showSpinner_(false);
1320 this.errorBanner_.show('GALLERY_SAVE_FAILED');
1322 callback();
1323 }.bind(this));
1327 * Flash 'Saved' label briefly to indicate that the image has been saved.
1328 * @private
1330 SlideMode.prototype.flashSavedLabel_ = function() {
1331 this.savedLabel_.hidden = false;
1332 var setLabelHighlighted =
1333 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1334 setTimeout(setLabelHighlighted.bind(null, true), 0);
1335 setTimeout(setLabelHighlighted.bind(null, false), 300);
1339 * Local storage key for the number of times that
1340 * the overwrite info bubble has been displayed.
1341 * @const {string}
1343 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1346 * Local storage key for overwrite original checkbox value.
1347 * @const {string}
1349 SlideMode.OVERWRITE_ORIGINAL_KEY = 'gallery-overwrite-original';
1352 * Max number that the overwrite info bubble is shown.
1353 * @const {number}
1355 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1358 * Handles change event of overwrite original checkbox.
1360 SlideMode.prototype.onOverwriteOriginalCheckboxChanged_ = function() {
1361 var items = {};
1362 items[SlideMode.OVERWRITE_ORIGINAL_KEY] =
1363 this.overwriteOriginalCheckbox_.checked;
1364 chrome.storage.local.set(items);
1368 * Overwrite info bubble close handler.
1369 * @private
1371 SlideMode.prototype.onCloseBubble_ = function() {
1372 this.bubble_.hidden = true;
1373 this.setOverwriteBubbleCount_(SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1376 // Slideshow
1379 * Slideshow interval in ms.
1381 SlideMode.SLIDESHOW_INTERVAL = 5000;
1384 * First slideshow interval in ms. It should be shorter so that the user
1385 * is not guessing whether the button worked.
1387 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1390 * Empirically determined duration of the fullscreen toggle animation.
1392 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1395 * @return {boolean} True if the slideshow is on.
1396 * @private
1398 SlideMode.prototype.isSlideshowOn_ = function() {
1399 return this.container_.hasAttribute('slideshow');
1403 * Starts the slideshow.
1404 * @param {number=} opt_interval First interval in ms.
1405 * @param {Event=} opt_event Event.
1407 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1408 // Reset zoom.
1409 this.viewport_.resetView();
1410 this.imageView_.applyViewportChange();
1412 // Disable touch operation.
1413 this.touchHandlers_.enabled = false;
1415 // Set the attribute early to prevent the toolbar from flashing when
1416 // the slideshow is being started from the mosaic view.
1417 this.container_.setAttribute('slideshow', 'playing');
1419 if (this.active_) {
1420 this.stopEditing_();
1421 } else {
1422 // We are in the Mosaic mode. Toggle the mode but remember to return.
1423 this.leaveAfterSlideshow_ = true;
1425 // Wait until the zoom animation from the mosaic mode is done.
1426 var startSlideshowAfterTransition = function() {
1427 setTimeout(function() {
1428 this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
1429 }.bind(this), ImageView.MODE_TRANSITION_DURATION);
1430 }.bind(this);
1431 this.toggleMode_(startSlideshowAfterTransition);
1432 return;
1435 if (opt_event) // Caused by user action, notify the Gallery.
1436 cr.dispatchSimpleEvent(this, 'useraction');
1438 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1439 if (!this.fullscreenBeforeSlideshow_) {
1440 this.toggleFullScreen_();
1441 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1442 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1445 // This is a workaround. Mouseout event is not dispatched when window becomes
1446 // fullscreen and cursor gets out of the element
1447 // TODO(yawano): Find better implementation.
1448 this.dimmableUIController_.setCursorOutOfTools();
1450 this.resumeSlideshow_(opt_interval);
1454 * Stops the slideshow.
1455 * @param {Event=} opt_event Event.
1456 * @private
1458 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1459 if (!this.isSlideshowOn_())
1460 return;
1462 if (opt_event) // Caused by user action, notify the Gallery.
1463 cr.dispatchSimpleEvent(this, 'useraction');
1465 this.pauseSlideshow_();
1466 this.container_.removeAttribute('slideshow');
1468 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1469 var fullscreen = util.isFullScreen(this.context_.appWindow);
1470 var toggleModeDelay = 0;
1471 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1472 this.toggleFullScreen_();
1473 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1475 if (this.leaveAfterSlideshow_) {
1476 this.leaveAfterSlideshow_ = false;
1477 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1480 // Re-enable touch operation.
1481 this.touchHandlers_.enabled = true;
1485 * @return {boolean} True if the slideshow is playing (not paused).
1486 * @private
1488 SlideMode.prototype.isSlideshowPlaying_ = function() {
1489 return this.container_.getAttribute('slideshow') === 'playing';
1493 * Pauses/resumes the slideshow.
1494 * @private
1496 SlideMode.prototype.toggleSlideshowPause_ = function() {
1497 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1498 if (this.isSlideshowPlaying_()) {
1499 this.pauseSlideshow_();
1500 } else {
1501 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1506 * @param {number=} opt_interval Slideshow interval in ms.
1507 * @private
1509 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1510 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1512 if (this.slideShowTimeout_)
1513 clearTimeout(this.slideShowTimeout_);
1515 this.slideShowTimeout_ = setTimeout(function() {
1516 this.slideShowTimeout_ = null;
1517 this.selectNext(1);
1518 }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1522 * Resumes the slideshow.
1523 * @param {number=} opt_interval Slideshow interval in ms.
1524 * @private
1526 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1527 this.container_.setAttribute('slideshow', 'playing');
1528 this.scheduleNextSlide_(opt_interval);
1532 * Pauses the slideshow.
1533 * @private
1535 SlideMode.prototype.pauseSlideshow_ = function() {
1536 this.container_.setAttribute('slideshow', 'paused');
1537 if (this.slideShowTimeout_) {
1538 clearTimeout(this.slideShowTimeout_);
1539 this.slideShowTimeout_ = null;
1544 * @return {boolean} True if the editor is active.
1546 SlideMode.prototype.isEditing = function() {
1547 return this.container_.hasAttribute('editing');
1551 * Stops editing.
1552 * @private
1554 SlideMode.prototype.stopEditing_ = function() {
1555 if (this.isEditing())
1556 this.toggleEditor();
1560 * Activate/deactivate editor.
1561 * @param {Event=} opt_event Event.
1563 SlideMode.prototype.toggleEditor = function(opt_event) {
1564 if (opt_event) // Caused by user action, notify the Gallery.
1565 cr.dispatchSimpleEvent(this, 'useraction');
1567 if (!this.active_) {
1568 this.toggleMode_(this.toggleEditor.bind(this));
1569 return;
1572 this.stopSlideshow_();
1574 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1575 this.editButtonToggleRipple_.activated = this.isEditing();
1577 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1578 // Reset zoom.
1579 this.viewport_.resetView();
1581 // Scale the screen so that it doesn't overlap the toolbars.
1582 this.viewport_.setScreenTop(ImageEditor.Toolbar.HEIGHT);
1583 this.viewport_.setScreenBottom(ImageEditor.Toolbar.HEIGHT);
1585 this.imageView_.applyViewportChange();
1587 this.touchHandlers_.enabled = false;
1588 this.dimmableUIController_.setDisabled(true);
1590 // Show editor warning message.
1591 SlideMode.getEditorWarningMessage(
1592 assert(this.getItem(this.getSelectedIndex())),
1593 this.context_.readonlyDirName,
1594 assert(this.dataModel_.fallbackSaveDirectory)
1595 ).then(function(warningMessage) {
1596 if (!warningMessage)
1597 return;
1599 this.filesToast_.show(warningMessage);
1600 }.bind(this));
1602 // Show overwrite original bubble if it hasn't been shown for max times.
1603 this.getOverwriteBubbleCount_().then(function(count) {
1604 if (count >= SlideMode.OVERWRITE_BUBBLE_MAX_TIMES)
1605 return;
1607 this.setOverwriteBubbleCount_(count + 1);
1608 this.bubble_.hidden = false;
1609 }.bind(this));
1610 } else {
1611 this.editor_.getPrompt().hide();
1612 this.editor_.leaveModeGently();
1614 this.viewport_.setScreenTop(0);
1615 this.viewport_.setScreenBottom(0);
1616 this.imageView_.applyViewportChange();
1618 this.bubble_.hidden = true;
1620 this.touchHandlers_.enabled = true;
1621 this.dimmableUIController_.setDisabled(false);
1626 * Gets count of overwrite bubble.
1627 * @return {!Promise<number>}
1628 * @private
1630 SlideMode.prototype.getOverwriteBubbleCount_ = function() {
1631 return new Promise(function(resolve, reject) {
1632 var requests = {};
1633 requests[SlideMode.OVERWRITE_BUBBLE_KEY] = 0;
1635 chrome.storage.local.get(requests, function(results) {
1636 if (!!chrome.runtime.lastError) {
1637 reject(chrome.runtime.lastError);
1638 return;
1641 resolve(results[SlideMode.OVERWRITE_BUBBLE_KEY]);
1647 * Sets count of overwrite bubble.
1648 * @param {number} value
1649 * @private
1651 SlideMode.prototype.setOverwriteBubbleCount_ = function(value) {
1652 var requests = {};
1653 requests[SlideMode.OVERWRITE_BUBBLE_KEY] = value;
1654 chrome.storage.local.set(requests);
1658 * Prints the current item.
1659 * @private
1661 SlideMode.prototype.print_ = function() {
1662 cr.dispatchSimpleEvent(this, 'useraction');
1663 window.print();
1667 * Shows/hides the busy spinner.
1669 * @param {boolean} on True if show, false if hide.
1670 * @private
1672 SlideMode.prototype.showSpinner_ = function(on) {
1673 if (this.spinnerTimer_) {
1674 clearTimeout(this.spinnerTimer_);
1675 this.spinnerTimer_ = null;
1678 if (on) {
1679 this.spinnerTimer_ = setTimeout(function() {
1680 this.spinnerTimer_ = null;
1681 ImageUtil.setAttribute(this.container_, 'spinner', true);
1682 }.bind(this), 1000);
1683 } else {
1684 ImageUtil.setAttribute(this.container_, 'spinner', false);
1689 * Apply the change of viewport.
1691 SlideMode.prototype.applyViewportChange = function() {
1692 this.imageView_.applyViewportChange();
1696 * Touch handlers of the slide mode.
1697 * @param {!Element} targetElement Event source.
1698 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1699 * @struct
1700 * @constructor
1702 function TouchHandler(targetElement, slideMode) {
1704 * Event source.
1705 * @type {!Element}
1706 * @private
1707 * @const
1709 this.targetElement_ = targetElement;
1712 * Target of touch operations.
1713 * @type {!SlideMode}
1714 * @private
1715 * @const
1717 this.slideMode_ = slideMode;
1720 * Flag to enable/disable touch operation.
1721 * @type {boolean}
1722 * @private
1724 this.enabled_ = true;
1727 * Whether it is in a touch operation that is started from targetElement or
1728 * not.
1729 * @type {boolean}
1730 * @private
1732 this.touchStarted_ = false;
1735 * The swipe action that should happen only once in an operation is already
1736 * done or not.
1737 * @type {boolean}
1738 * @private
1740 this.done_ = false;
1743 * Event on beginning of the current gesture.
1744 * The variable is updated when the number of touch finger changed.
1745 * @type {TouchEvent}
1746 * @private
1748 this.gestureStartEvent_ = null;
1751 * Rotation value on beginning of the current gesture.
1752 * @type {number}
1753 * @private
1755 this.gestureStartRotation_ = 0;
1758 * Last touch event.
1759 * @type {TouchEvent}
1760 * @private
1762 this.lastEvent_ = null;
1765 * Zoom value just after last touch event.
1766 * @type {number}
1767 * @private
1769 this.lastZoom_ = 1.0;
1771 targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
1772 var onTouchEventBound = this.onTouchEvent_.bind(this);
1773 targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
1774 targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
1776 targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
1780 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1781 * horizontally it's considered as a swipe gesture (change the current image).
1782 * @type {number}
1783 * @const
1785 TouchHandler.SWIPE_THRESHOLD = 100;
1788 * Rotation threshold in degrees.
1789 * @type {number}
1790 * @const
1792 TouchHandler.ROTATION_THRESHOLD = 25;
1795 * Obtains distance between fingers.
1796 * @param {!TouchEvent} event Touch event. It should include more than two
1797 * touches.
1798 * @return {number} Distance between touch[0] and touch[1].
1800 TouchHandler.getDistance = function(event) {
1801 var touch1 = event.touches[0];
1802 var touch2 = event.touches[1];
1803 var dx = touch1.clientX - touch2.clientX;
1804 var dy = touch1.clientY - touch2.clientY;
1805 return Math.sqrt(dx * dx + dy * dy);
1809 * Obtains the degrees of the pinch twist angle.
1810 * @param {!TouchEvent} event1 Start touch event. It should include more than
1811 * two touches.
1812 * @param {!TouchEvent} event2 Current touch event. It should include more than
1813 * two touches.
1814 * @return {number} Degrees of the pinch twist angle.
1816 TouchHandler.getTwistAngle = function(event1, event2) {
1817 var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
1818 var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
1819 var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
1820 var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
1821 var innerProduct = dx1 * dx2 + dy1 * dy2; // |v1| * |v2| * cos(t) = x / r
1822 var outerProduct = dx1 * dy2 - dy1 * dx2; // |v1| * |v2| * sin(t) = y / r
1823 return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI; // atan(y / x)
1826 TouchHandler.prototype = /** @struct */ {
1828 * @param {boolean} flag New value.
1830 set enabled(flag) {
1831 this.enabled_ = flag;
1832 if (!this.enabled_)
1833 this.stopOperation();
1838 * Stops the current touch operation.
1840 TouchHandler.prototype.stopOperation = function() {
1841 this.touchStarted_ = false;
1842 this.done_ = false;
1843 this.gestureStartEvent_ = null;
1844 this.lastEvent_ = null;
1845 this.lastZoom_ = 1.0;
1849 * Handles touch start events.
1850 * @param {!Event} event Touch event.
1851 * @private
1853 TouchHandler.prototype.onTouchStart_ = function(event) {
1854 event = assertInstanceof(event, TouchEvent);
1855 if (this.enabled_ && event.touches.length === 1)
1856 this.touchStarted_ = true;
1860 * Handles touch move and touch end events.
1861 * @param {!Event} event Touch event.
1862 * @private
1864 TouchHandler.prototype.onTouchEvent_ = function(event) {
1865 event = assertInstanceof(event, TouchEvent);
1866 // Check if the current touch operation started from the target element or
1867 // not.
1868 if (!this.touchStarted_)
1869 return;
1871 // Check if the current touch operation ends with the event.
1872 if (event.touches.length === 0) {
1873 this.stopOperation();
1874 return;
1877 // Check if a new gesture started or not.
1878 var viewport = this.slideMode_.getViewport();
1879 if (!this.lastEvent_ ||
1880 this.lastEvent_.touches.length !== event.touches.length) {
1881 if (event.touches.length === 2 ||
1882 event.touches.length === 1) {
1883 this.gestureStartEvent_ = event;
1884 this.gestureStartRotation_ = viewport.getRotation();
1885 this.lastEvent_ = event;
1886 this.lastZoom_ = viewport.getZoom();
1887 } else {
1888 this.gestureStartEvent_ = null;
1889 this.gestureStartRotation_ = 0;
1890 this.lastEvent_ = null;
1891 this.lastZoom_ = 1.0;
1893 return;
1896 // Handle the gesture movement.
1897 switch (event.touches.length) {
1898 case 1:
1899 if (viewport.isZoomed()) {
1900 // Scrolling an image by swipe.
1901 var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
1902 var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
1903 viewport.setOffset(
1904 viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
1905 this.slideMode_.applyViewportChange();
1906 } else {
1907 // Traversing images by swipe.
1908 if (this.done_)
1909 break;
1910 var dx =
1911 event.touches[0].clientX -
1912 this.gestureStartEvent_.touches[0].clientX;
1913 if (dx > TouchHandler.SWIPE_THRESHOLD) {
1914 this.slideMode_.advanceManually(-1);
1915 this.done_ = true;
1916 } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
1917 this.slideMode_.advanceManually(1);
1918 this.done_ = true;
1921 break;
1923 case 2:
1924 // Pinch zoom.
1925 var distance1 = TouchHandler.getDistance(this.lastEvent_);
1926 var distance2 = TouchHandler.getDistance(event);
1927 if (distance1 === 0)
1928 break;
1929 var zoom = distance2 / distance1 * this.lastZoom_;
1930 viewport.setZoom(zoom);
1932 // Pinch rotation.
1933 assert(this.gestureStartEvent_);
1934 var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
1935 if (angle > TouchHandler.ROTATION_THRESHOLD)
1936 viewport.setRotation(this.gestureStartRotation_ + 1);
1937 else if (angle < -TouchHandler.ROTATION_THRESHOLD)
1938 viewport.setRotation(this.gestureStartRotation_ - 1);
1939 else
1940 viewport.setRotation(this.gestureStartRotation_);
1941 this.slideMode_.applyViewportChange();
1942 break;
1945 // Update the last event.
1946 this.lastEvent_ = event;
1947 this.lastZoom_ = viewport.getZoom();
1951 * Handles mouse wheel events.
1952 * @param {!Event} event Wheel event.
1953 * @private
1955 TouchHandler.prototype.onMouseWheel_ = function(event) {
1956 var event = assertInstanceof(event, MouseEvent);
1957 var viewport = this.slideMode_.getViewport();
1958 if (!this.enabled_ || !viewport.isZoomed())
1959 return;
1960 this.stopOperation();
1961 viewport.setOffset(
1962 viewport.getOffsetX() + event.wheelDeltaX,
1963 viewport.getOffsetY() + event.wheelDeltaY);
1964 this.slideMode_.applyViewportChange();