Remove @suppress {checkStructDictInheritance} from ui/file_manager
[chromium-blink-merge.git] / ui / file_manager / gallery / js / slide_mode.js
blob1d627cbb5f9143495536f5d3f8c792c94cde5e92
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.
8  *
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}
28  */
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
37    */
38   this.container_ = container;
40   /**
41    * @type {!Document}
42    * @private
43    * @const
44    */
45   this.document_ = assert(container.ownerDocument);
47   /**
48    * @type {!HTMLElement}
49    * @const
50    */
51   this.content = content;
53   /**
54    * @type {!HTMLElement}
55    * @private
56    * @const
57    */
58   this.topToolbar_ = topToolbar;
60   /**
61    * @type {!HTMLElement}
62    * @private
63    * @const
64    */
65   this.bottomToolbar_ = bottomToolbar;
67   /**
68    * @type {!ImageEditor.Prompt}
69    * @private
70    * @const
71    */
72   this.prompt_ = prompt;
74   /**
75    * @type {!ErrorBanner}
76    * @private
77    * @const
78    */
79   this.errorBanner_ = errorBanner;
81   /**
82    * @type {!cr.ui.ArrayDataModel}
83    * @private
84    * @const
85    */
86   this.dataModel_ = dataModel;
88   /**
89    * @type {!cr.ui.ListSelectionModel}
90    * @private
91    * @const
92    */
93   this.selectionModel_ = selectionModel;
95   /**
96    * @type {!Object}
97    * @private
98    * @const
99    */
100   this.context_ = context;
102   /**
103    * @type {!VolumeManagerWrapper}
104    * @private
105    * @const
106    */
107   this.volumeManager_ = volumeManager;
109   /**
110    * @type {function(function())}
111    * @private
112    * @const
113    */
114   this.toggleMode_ = toggleMode;
116   /**
117    * @type {function(string):string}
118    * @private
119    * @const
120    */
121   this.displayStringFunction_ = displayStringFunction;
123   /**
124    * @private {!DimmableUIController}
125    * @const
126    */
127   this.dimmableUIController_ = dimmableUIController;
129   /**
130    * @type {function(this:SlideMode)}
131    * @private
132    * @const
133    */
134   this.onSelectionBound_ = this.onSelection_.bind(this);
136   /**
137    * @type {function(this:SlideMode,!Event)}
138    * @private
139    * @const
140    */
141   this.onSpliceBound_ = this.onSplice_.bind(this);
143   /**
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.
147    *
148    * @type {number}
149    * @private
150    */
151   this.currentUniqueKey_ = 0;
153   /**
154    * @type {number}
155    * @private
156    */
157   this.sequenceDirection_ = 0;
159   /**
160    * @type {number}
161    * @private
162    */
163   this.sequenceLength_ = 0;
165   /**
166    * @type {Array<number>}
167    * @private
168    */
169   this.savedSelection_ = null;
171   /**
172    * @type {Gallery.Item}
173    * @private
174    */
175   this.displayedItem_ = null;
177   /**
178    * @type {?number}
179    * @private
180    */
181   this.slideHint_ = null;
183   /**
184    * @type {boolean}
185    * @private
186    */
187   this.active_ = false;
189   /**
190    * @type {boolean}
191    * @private
192    */
193   this.leaveAfterSlideshow_ = false;
195   /**
196    * @type {boolean}
197    * @private
198    */
199   this.fullscreenBeforeSlideshow_ = false;
201   /**
202    * @type {?number}
203    * @private
204    */
205   this.slideShowTimeout_ = null;
207   /**
208    * @type {?number}
209    * @private
210    */
211   this.spinnerTimer_ = null;
213   window.addEventListener('resize', this.onResize_.bind(this));
215   // ----------------------------------------------------------------
216   // Initializes the UI.
218   /**
219    * Container for displayed image.
220    * @type {!HTMLElement}
221    * @private
222    * @const
223    */
224   this.imageContainer_ = util.createChild(queryRequiredElement(
225       this.document_, '.content'), 'image-container');
227   this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
229   /**
230    * Overwrite options and info bubble.
231    * @type {!HTMLElement}
232    * @private
233    * @const
234    */
235   this.options_ = queryRequiredElement(this.bottomToolbar_, '.options');
237   /**
238    * @type {!HTMLElement}
239    * @private
240    * @const
241    */
242   this.savedLabel_ = queryRequiredElement(this.options_, '.saved');
244   /**
245    * @private {!PaperCheckboxElement}
246    * @const
247    */
248   this.overwriteOriginalCheckbox_ = /** @type {!PaperCheckboxElement} */
249       (queryRequiredElement(this.options_, '.overwrite-original'));
250   this.overwriteOriginalCheckbox_.addEventListener('change',
251       this.onOverwriteOriginalCheckboxChanged_.bind(this));
253   /**
254    * @private {!FilesToast}
255    * @const
256    */
257   this.filesToast_ = /** @type {!FilesToast} */
258       (queryRequiredElement(document, 'files-toast'));
260   /**
261    * @private {!HTMLElement}
262    * @const
263    */
264   this.bubble_ = queryRequiredElement(this.bottomToolbar_, '.bubble');
266   var bubbleContent = queryRequiredElement(this.bubble_, '.content');
267   // GALLERY_OVERWRITE_BUBBLE contains <br> tag inside message.
268   bubbleContent.innerHTML = strf('GALLERY_OVERWRITE_BUBBLE');
270   var bubbleClose = queryRequiredElement(this.bubble_, '.close-x');
271   bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
273   /**
274    * Ribbon and related controls.
275    * @type {!HTMLElement}
276    * @private
277    * @const
278    */
279   this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
281   /**
282    * @type {!HTMLElement}
283    * @private
284    * @const
285    */
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_);
292   /**
293    * @type {!HTMLElement}
294    * @private
295    * @const
296    */
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_);
303   /**
304    * @type {!HTMLElement}
305    * @private
306    * @const
307    */
308   this.ribbonSpacer_ = queryRequiredElement(this.bottomToolbar_,
309       '.ribbon-spacer');
311   /**
312    * @type {!Ribbon}
313    * @private
314    * @const
315    */
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');
322   /**
323    * @type {!HTMLElement}
324    * @const
325    */
326   var slideShowButton = queryRequiredElement(this.topToolbar_,
327       'paper-button.slideshow');
328   slideShowButton.addEventListener('click',
329       this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
331   /**
332    * @type {!HTMLElement}
333    * @const
334    */
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.
343   /**
344    * @type {!HTMLElement}
345    * @private
346    * @const
347    */
348   this.editButton_ = queryRequiredElement(this.topToolbar_, 'button.edit');
349   GalleryUtil.decorateMouseFocusHandling(this.editButton_);
350   this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
352   /**
353    * @private {!FilesToggleRipple}
354    * @const
355    */
356   this.editButtonToggleRipple_ = /** @type {!FilesToggleRipple} */
357       (assert(this.editButton_.querySelector('files-toggle-ripple')));
359   /**
360    * @type {!HTMLElement}
361    * @private
362    * @const
363    */
364   this.printButton_ = queryRequiredElement(
365       this.topToolbar_, 'paper-button.print');
366   this.printButton_.addEventListener('click', this.print_.bind(this));
368   /**
369    * @type {!HTMLElement}
370    * @private
371    * @const
372    */
373   this.editBarSpacer_ = queryRequiredElement(this.bottomToolbar_,
374       '.edit-bar-spacer');
376   /**
377    * @type {!HTMLElement}
378    * @private
379    * @const
380    */
381   this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
383   /**
384    * @type {!HTMLElement}
385    * @private
386    * @const
387    */
388   this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
390   /**
391    * @type {!HTMLElement}
392    * @private
393    * @const
394    */
395   this.editBarModeWrapper_ = util.createChild(
396       this.editBarMode_, 'edit-modal-wrapper dimmable');
397   this.editBarModeWrapper_.hidden = true;
399   /**
400    * Objects supporting image display and editing.
401    * @type {!Viewport}
402    * @private
403    * @const
404    */
405   this.viewport_ = new Viewport(window);
406   this.viewport_.addEventListener('resize', this.onViewportResize_.bind(this));
408   /**
409    * @type {!ImageView}
410    * @private
411    * @const
412    */
413   this.imageView_ = new ImageView(
414       this.imageContainer_,
415       this.viewport_,
416       metadataModel);
418   /**
419    * @type {!ImageEditor}
420    * @private
421    * @const
422    */
423   this.editor_ = new ImageEditor(
424       this.viewport_,
425       this.imageView_,
426       this.prompt_,
427       {
428         root: this.container_,
429         image: this.imageContainer_,
430         toolbar: this.editBarMain_,
431         mode: this.editBarModeWrapper_
432       },
433       SlideMode.EDITOR_MODES,
434       this.displayStringFunction_);
435   this.editor_.addEventListener('exit-clicked', this.onExitClicked_.bind(this));
437   /**
438    * @type {!TouchHandler}
439    * @private
440    * @const
441    */
442   this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
444   /**
445    * @private {!ChromeVoxStateWatcher}
446    * @const
447    */
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
459  */
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
474  */
475 SlideMode.KEY_OFFSET_MAP = {
476   'Up': [0, 20],
477   'Down': [0, -20],
478   'Left': [20, 0],
479   'Right': [-20, 0]
483  * SlideMode extends cr.EventTarget.
484  */
485 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
488  * Handles chromevox-navigation-begin event. While user is navigating with
489  * ChromeVox, we should not hide the tools.
490  * @private
491  */
492 SlideMode.prototype.onChromeVoxNavigationBegin_ = function() {
493   this.dimmableUIController_.setDisabled(true);
497  * Handles chromevox-navigation-end event.
498  * @private
499  */
500 SlideMode.prototype.onChromeVoxNavigationEnd_ = function() {
501   this.dimmableUIController_.setDisabled(false);
505  * Handles exit-clicked event.
506  * @private
507  */
508 SlideMode.prototype.onExitClicked_ = function() {
509   if (this.isEditing())
510     this.toggleEditor();
514  * @return {string} Mode name.
515  */
516 SlideMode.prototype.getName = function() { return 'slide'; };
519  * @return {string} Mode title.
520  */
521 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
524  * @return {!Viewport} Viewport.
525  */
526 SlideMode.prototype.getViewport = function() { return this.viewport_; };
529  * Load items, display the selected item.
530  * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
531  * @param {function()} displayCallback Called when the image is displayed.
532  * @param {function()} loadCallback Called when the image is displayed.
533  */
534 SlideMode.prototype.enter = function(
535     zoomFromRect, displayCallback, loadCallback) {
536   this.sequenceDirection_ = 0;
537   this.sequenceLength_ = 0;
539   // The latest |leave| call might have left the image animating. Remove it.
540   this.unloadImage_();
541   this.errorBanner_.clear();
543   new Promise(function(fulfill) {
544     // If the items are empty, just show the error message.
545     if (this.getItemCount_() === 0) {
546       this.displayedItem_ = null;
547       this.errorBanner_.show('GALLERY_NO_IMAGES');
548       fulfill();
549       return;
550     }
552     // Remember the selection if it is empty or multiple. It will be restored
553     // in |leave| if the user did not changing the selection manually.
554     var currentSelection = this.selectionModel_.selectedIndexes;
555     if (currentSelection.length === 1)
556       this.savedSelection_ = null;
557     else
558       this.savedSelection_ = currentSelection;
560     // Ensure valid single selection.
561     // Note that the SlideMode object is not listening to selection change yet.
562     this.select(Math.max(0, this.getSelectedIndex()));
564     // Show the selected item ASAP, then complete the initialization
565     // (loading the ribbon thumbnails can take some time).
566     var selectedItem = this.getSelectedItem();
567     this.displayedItem_ = selectedItem;
569     // Load the image of the item.
570     this.loadItem_(
571         assert(selectedItem),
572         zoomFromRect ?
573             this.imageView_.createZoomEffect(zoomFromRect) :
574             new ImageView.Effect.None(),
575         displayCallback,
576         function(loadType, delay) {
577           fulfill(delay);
578         });
579   }.bind(this)).then(function(delay) {
580     // Turn the mode active.
581     this.active_ = true;
582     ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
583     this.ribbon_.enable();
585     // Register handlers.
586     this.selectionModel_.addEventListener('change', this.onSelectionBound_);
587     this.dataModel_.addEventListener('splice', this.onSpliceBound_);
588     this.touchHandlers_.enabled = true;
590     // Wait 1000ms after the animation is done, then prefetch the next image.
591     this.requestPrefetch(1, delay + 1000);
593     // Call load callback.
594     if (loadCallback)
595       loadCallback();
596   }.bind(this)).catch(function(error) {
597     console.error(error.stack, error);
598   });
602  * Leave the mode.
603  * @param {ImageRect} zoomToRect Rectangle for zoom effect.
604  * @param {function()} callback Called when the image is committed and
605  *   the zoom-out animation has started.
606  */
607 SlideMode.prototype.leave = function(zoomToRect, callback) {
608   var commitDone = function() {
609     this.stopEditing_();
610     this.stopSlideshow_();
611     ImageUtil.setAttribute(this.arrowBox_, 'active', false);
612     this.selectionModel_.removeEventListener(
613         'change', this.onSelectionBound_);
614     this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
615     this.ribbon_.disable();
616     this.active_ = false;
617     if (this.savedSelection_)
618       this.selectionModel_.selectedIndexes = this.savedSelection_;
619     this.unloadImage_(zoomToRect);
620     callback();
621   }.bind(this);
623   this.viewport_.resetView();
624   if (this.getItemCount_() === 0) {
625     this.errorBanner_.clear();
626     commitDone();
627   } else {
628     this.commitItem_(commitDone);
629   }
631   // Disable the slide-mode only buttons when leaving.
632   this.editButton_.disabled = true;
633   this.printButton_.disabled = true;
635   // Disable touch operation.
636   this.touchHandlers_.enabled = false;
641  * Execute an action when the editor is not busy.
643  * @param {function()} action Function to execute.
644  */
645 SlideMode.prototype.executeWhenReady = function(action) {
646   this.editor_.executeWhenReady(action);
650  * @return {boolean} True if the mode has active tools (that should not fade).
651  */
652 SlideMode.prototype.hasActiveTool = function() {
653   return this.isEditing();
657  * @return {number} Item count.
658  * @private
659  */
660 SlideMode.prototype.getItemCount_ = function() {
661   return this.dataModel_.length;
665  * @param {number} index Index.
666  * @return {Gallery.Item} Item.
667  */
668 SlideMode.prototype.getItem = function(index) {
669   var item =
670       /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
671   return item === undefined ? null : item;
675  * @return {number} Selected index.
676  */
677 SlideMode.prototype.getSelectedIndex = function() {
678   return this.selectionModel_.selectedIndex;
682  * @return {ImageRect} Screen rectangle of the selected image.
683  */
684 SlideMode.prototype.getSelectedImageRect = function() {
685   if (this.getSelectedIndex() < 0)
686     return null;
687   else
688     return this.viewport_.getImageBoundsOnScreen();
692  * @return {Gallery.Item} Selected item.
693  */
694 SlideMode.prototype.getSelectedItem = function() {
695   return this.getItem(this.getSelectedIndex());
699  * Toggles the full screen mode.
700  * @private
701  */
702 SlideMode.prototype.toggleFullScreen_ = function() {
703   util.toggleFullScreen(this.context_.appWindow,
704                         !util.isFullScreen(this.context_.appWindow));
708  * Selection change handler.
710  * Commits the current image and displays the newly selected image.
711  * @private
712  */
713 SlideMode.prototype.onSelection_ = function() {
714   if (this.selectionModel_.selectedIndexes.length === 0)
715     return;  // Ignore temporary empty selection.
717   // Forget the saved selection if the user changed the selection manually.
718   if (!this.isSlideshowOn_())
719     this.savedSelection_ = null;
721   if (this.getSelectedItem() === this.displayedItem_)
722     return;  // Do not reselect.
724   this.commitItem_(this.loadSelectedItem_.bind(this));
728  * Change the selection.
730  * @param {number} index New selected index.
731  * @param {number=} opt_slideHint Slide animation direction (-1|1).
732  */
733 SlideMode.prototype.select = function(index, opt_slideHint) {
734   this.slideHint_ = opt_slideHint || null;
735   this.selectionModel_.selectedIndex = index;
736   this.selectionModel_.leadIndex = index;
740  * Load the selected item.
742  * @private
743  */
744 SlideMode.prototype.loadSelectedItem_ = function() {
745   var slideHint = this.slideHint_;
746   this.slideHint_ = null;
748   if (this.getSelectedItem() === this.displayedItem_)
749     return;  // Do not reselect.
751   var index = this.getSelectedIndex();
752   if (index < 0)
753     return;
755   var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
756   var step =
757       slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);
759   if (Math.abs(step) != 1) {
760     // Long leap, the sequence is broken, we have no good prefetch candidate.
761     this.sequenceDirection_ = 0;
762     this.sequenceLength_ = 0;
763   } else if (this.sequenceDirection_ === step) {
764     // Keeping going in sequence.
765     this.sequenceLength_++;
766   } else {
767     // Reversed the direction. Reset the counter.
768     this.sequenceDirection_ = step;
769     this.sequenceLength_ = 1;
770   }
772   this.displayedItem_ = this.getSelectedItem();
773   var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);
775   function shouldPrefetch(loadType, step, sequenceLength) {
776     // Never prefetch when selecting out of sequence.
777     if (Math.abs(step) != 1)
778       return false;
780     // Always prefetch if the previous load was from cache.
781     if (loadType === ImageView.LoadType.CACHED_FULL)
782       return true;
784     // Prefetch if we have been going in the same direction for long enough.
785     return sequenceLength >= 3;
786   }
788   this.currentUniqueKey_++;
789   var selectedUniqueKey = this.currentUniqueKey_;
791   // Discard, since another load has been invoked after this one.
792   if (selectedUniqueKey != this.currentUniqueKey_)
793     return;
795   this.loadItem_(
796       selectedItem,
797       new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
798       function() {} /* no displayCallback */,
799       function(loadType, delay) {
800         // Discard, since another load has been invoked after this one.
801         if (selectedUniqueKey != this.currentUniqueKey_)
802           return;
803         if (shouldPrefetch(loadType, step, this.sequenceLength_))
804           this.requestPrefetch(step, delay);
805         if (this.isSlideshowPlaying_())
806           this.scheduleNextSlide_();
807       }.bind(this));
811  * Unload the current image.
813  * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
814  * @private
815  */
816 SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
817   this.imageView_.unload(opt_zoomToRect);
821  * Data model 'splice' event handler.
822  * @param {!Event} event Event.
823  * @this {SlideMode}
824  * @private
825  */
826 SlideMode.prototype.onSplice_ = function(event) {
827   ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
829   // Splice invalidates saved indices, drop the saved selection.
830   this.savedSelection_ = null;
832   if (event.removed.length != 1)
833     return;
835   // Delay the selection to let the ribbon splice handler work first.
836   setTimeout(function() {
837     if (this.dataModel_.length === 0) {
838       // No items left. Unload the image, disable edit and print button, and
839       // show the banner.
840       this.commitItem_(function() {
841         this.unloadImage_();
842         this.printButton_.disabled = true;
843         this.editButton_.disabled = true;
844         this.errorBanner_.show('GALLERY_NO_IMAGES');
845       }.bind(this));
846       return;
847     }
849     var displayedItemNotRemvoed = event.removed.every(function(item) {
850       return item !== this.displayedItem_;
851     }.bind(this));
852     if (!displayedItemNotRemvoed) {
853       // There is the next item, select it. Otherwise, select the last item.
854       var nextIndex = Math.min(event.index, this.dataModel_.length - 1);
855       // To force to dispatch a selection change event, clear selection before.
856       this.selectionModel_.clear();
857       this.select(nextIndex);
858     }
859   }.bind(this), 0);
863  * @param {number} direction -1 for left, 1 for right.
864  * @return {number} Next index in the given direction, with wrapping.
865  * @private
866  */
867 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
868   function advance(index, limit) {
869     index += (direction > 0 ? 1 : -1);
870     if (index < 0)
871       return limit - 1;
872     if (index === limit)
873       return 0;
874     return index;
875   }
877   // If the saved selection is multiple the Slideshow should cycle through
878   // the saved selection.
879   if (this.isSlideshowOn_() &&
880       this.savedSelection_ && this.savedSelection_.length > 1) {
881     var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
882         this.savedSelection_.length);
883     return this.savedSelection_[pos];
884   } else {
885     return advance(this.getSelectedIndex(), this.getItemCount_());
886   }
890  * Advance the selection based on the pressed key ID.
891  * @param {string} keyID Key identifier.
892  */
893 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
894   var prev = (keyID === 'Up' ||
895               keyID === 'Left' ||
896               keyID === 'MediaPreviousTrack');
897   this.advanceManually(prev ? -1 : 1);
901  * Advance the selection as a result of a user action (as opposed to an
902  * automatic change in the slideshow mode).
903  * @param {number} direction -1 for left, 1 for right.
904  */
905 SlideMode.prototype.advanceManually = function(direction) {
906   if (this.isSlideshowPlaying_())
907     this.pauseSlideshow_();
908   cr.dispatchSimpleEvent(this, 'useraction');
909   this.selectNext(direction);
913  * Select the next item.
914  * @param {number} direction -1 for left, 1 for right.
915  */
916 SlideMode.prototype.selectNext = function(direction) {
917   this.select(this.getNextSelectedIndex_(direction), direction);
921  * Select the first item.
922  */
923 SlideMode.prototype.selectFirst = function() {
924   this.select(0);
928  * Select the last item.
929  */
930 SlideMode.prototype.selectLast = function() {
931   this.select(this.getItemCount_() - 1);
934 // Loading/unloading
937  * Load and display an item.
939  * @param {!Gallery.Item} item Item.
940  * @param {!ImageView.Effect} effect Transition effect object.
941  * @param {function()} displayCallback Called when the image is displayed
942  *     (which can happen before the image load due to caching).
943  * @param {function(number, number)} loadCallback Called when the image is fully
944  *     loaded.
945  * @private
946  */
947 SlideMode.prototype.loadItem_ = function(
948     item, effect, displayCallback, loadCallback) {
949   this.showSpinner_(true);
951   var loadDone = this.itemLoaded_.bind(this, item, loadCallback);
953   var displayDone = function() {
954     cr.dispatchSimpleEvent(this, 'image-displayed');
955     displayCallback();
956   }.bind(this);
958   this.editor_.openSession(
959       item,
960       effect,
961       this.saveCurrentImage_.bind(this, item),
962       displayDone,
963       loadDone);
967  * A callback function when the editor opens a editing session for an image.
968  * @param {!Gallery.Item} item Gallery item.
969  * @param {function(number, number)} loadCallback Called when the image is fully
970  *     loaded.
971  * @param {number} loadType Load type.
972  * @param {number} delay Delay.
973  * @param {*=} opt_error Error.
974  * @private
975  */
976 SlideMode.prototype.itemLoaded_ = function(
977     item, loadCallback, loadType, delay, opt_error) {
978   var entry = item.getEntry();
980   this.showSpinner_(false);
981   if (loadType === ImageView.LoadType.ERROR) {
982     // if we have a specific error, then display it
983     if (opt_error) {
984       this.errorBanner_.show(/** @type {string} */ (opt_error));
985     } else {
986       // otherwise try to infer general error
987       this.errorBanner_.show('GALLERY_IMAGE_ERROR');
988     }
989   } else if (loadType === ImageView.LoadType.OFFLINE) {
990     this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
991   }
993   ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
995   var toMillions = function(number) {
996     return Math.round(number / (1000 * 1000));
997   };
999   var metadata = item.getMetadataItem();
1000   if (metadata) {
1001     ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
1002         toMillions(metadata.size));
1003   }
1005   var canvas = this.imageView_.getCanvas();
1006   ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
1007       toMillions(canvas.width * canvas.height));
1009   var extIndex = entry.name.lastIndexOf('.');
1010   var ext = extIndex < 0 ? '' :
1011       entry.name.substr(extIndex + 1).toLowerCase();
1012   if (ext === 'jpeg') ext = 'jpg';
1013   ImageUtil.metrics.recordEnum(
1014       ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
1016   // Enable or disable buttons for editing and printing.
1017   if (opt_error) {
1018     this.editButton_.disabled = true;
1019     this.printButton_.disabled = true;
1020   } else {
1021     this.editButton_.disabled = false;
1022     this.printButton_.disabled = false;
1023   }
1025   // Saved label is hidden by default.
1026   this.savedLabel_.hidden = true;
1028   // Disable overwrite original checkbox until settings is loaded.
1029   this.overwriteOriginalCheckbox_.disabled = true;
1030   this.overwriteOriginalCheckbox_.checked = false;
1032   var keys = {};
1033   keys[SlideMode.OVERWRITE_ORIGINAL_KEY] = true;
1034   chrome.storage.local.get(keys,
1035       function(values) {
1036         // Users can overwrite original file only if loaded image is original
1037         // and writable.
1038         if (item.isOriginal() &&
1039             item.isWritableFile(this.volumeManager_)) {
1040           this.overwriteOriginalCheckbox_.disabled = false;
1041           this.overwriteOriginalCheckbox_.checked =
1042               values[SlideMode.OVERWRITE_ORIGINAL_KEY];
1043         }
1044       }.bind(this));
1046   loadCallback(loadType, delay);
1050  * Commit changes to the current item and reset all messages/indicators.
1052  * @param {function()} callback Callback.
1053  * @private
1054  */
1055 SlideMode.prototype.commitItem_ = function(callback) {
1056   this.showSpinner_(false);
1057   this.errorBanner_.clear();
1058   this.editor_.getPrompt().hide();
1059   this.editor_.closeSession(callback);
1063  * Request a prefetch for the next image.
1065  * @param {number} direction -1 or 1.
1066  * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1067  *   loading from disrupting the animation that might be still in progress.
1068  */
1069 SlideMode.prototype.requestPrefetch = function(direction, delay) {
1070   if (this.getItemCount_() <= 1) return;
1072   var index = this.getNextSelectedIndex_(direction);
1073   this.imageView_.prefetch(assert(this.getItem(index)), delay);
1076 // Event handlers.
1079  * Click handler for the entire document.
1080  * @param {!Event} event Mouse click event.
1081  * @private
1082  */
1083 SlideMode.prototype.onDocumentClick_ = function(event) {
1084   // Events created in fakeMouseClick in test util don't pass this test.
1085   if (!window.IN_TEST)
1086     event = assertInstanceof(event, MouseEvent);
1088   var targetElement = assertInstanceof(event.target, HTMLElement);
1089   // Close the bubble if clicked outside of it and if it is visible.
1090   if (!this.bubble_.contains(targetElement) &&
1091       !this.editButton_.contains(targetElement) &&
1092       !this.arrowLeft_.contains(targetElement) &&
1093       !this.arrowRight_.contains(targetElement) &&
1094       !this.bubble_.hidden) {
1095     this.bubble_.hidden = true;
1096   }
1100  * Keydown handler.
1102  * @param {!Event} event Event.
1103  * @return {boolean} True if handled.
1104  */
1105 SlideMode.prototype.onKeyDown = function(event) {
1106   var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
1108   if (this.isSlideshowOn_()) {
1109     switch (keyID) {
1110       case 'U+001B':  // Escape
1111       case 'MediaStop':
1112         this.stopSlideshow_(event);
1113         break;
1115       case 'U+0020':  // Space pauses/resumes the slideshow.
1116       case 'MediaPlayPause':
1117         this.toggleSlideshowPause_();
1118         break;
1120       case 'Up':
1121       case 'Down':
1122       case 'Left':
1123       case 'Right':
1124       case 'MediaNextTrack':
1125       case 'MediaPreviousTrack':
1126         this.advanceWithKeyboard(keyID);
1127         break;
1128     }
1129     return true;  // Consume all keystrokes in the slideshow mode.
1130   }
1132   // Handles shortcut keys common for both modes (editing and not-editing).
1133   switch (keyID) {
1134     case 'Ctrl-U+0050':  // Ctrl+'p' prints the current image.
1135       if (!this.printButton_.disabled)
1136         this.print_();
1137       return true;
1139     case 'U+0045':  // 'e' toggles the editor.
1140       if (!this.editButton_.disabled)
1141         this.toggleEditor(event);
1142       return true;
1143   }
1145   // Handles shortcurt keys for editing mode.
1146   if (this.isEditing()) {
1147     if (this.editor_.onKeyDown(event))
1148       return true;
1150     if (keyID === 'U+001B') { // Escape
1151       this.toggleEditor(event);
1152       return true;
1153     }
1155     return false;
1156   }
1158   // Handles shortcut keys for not-editing mode.
1159   switch (keyID) {
1160     case 'U+001B':  // Escape
1161       if (this.viewport_.isZoomed()) {
1162         this.viewport_.resetView();
1163         this.touchHandlers_.stopOperation();
1164         this.imageView_.applyViewportChange();
1165         return true;
1166       }
1167       break;
1169     case 'Home':
1170       this.selectFirst();
1171       return true;
1173     case 'End':
1174       this.selectLast();
1175       return true;
1177     case 'Up':
1178     case 'Down':
1179     case 'Left':
1180     case 'Right':
1181       if (this.viewport_.isZoomed()) {
1182         var delta = SlideMode.KEY_OFFSET_MAP[keyID];
1183         this.viewport_.setOffset(
1184             ~~(this.viewport_.getOffsetX() +
1185                delta[0] * this.viewport_.getZoom()),
1186             ~~(this.viewport_.getOffsetY() +
1187                delta[1] * this.viewport_.getZoom()));
1188         this.touchHandlers_.stopOperation();
1189         this.imageView_.applyViewportChange();
1190       } else {
1191         this.advanceWithKeyboard(keyID);
1192       }
1193       return true;
1195     case 'MediaNextTrack':
1196     case 'MediaPreviousTrack':
1197       this.advanceWithKeyboard(keyID);
1198       return true;
1200     case 'Ctrl-U+00BB':  // Ctrl+'=' zoom in.
1201       this.viewport_.zoomIn();
1202       this.touchHandlers_.stopOperation();
1203       this.imageView_.applyViewportChange();
1204       return true;
1206     case 'Ctrl-U+00BD':  // Ctrl+'-' zoom out.
1207       this.viewport_.zoomOut();
1208       this.touchHandlers_.stopOperation();
1209       this.imageView_.applyViewportChange();
1210       return true;
1212     case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1213       this.viewport_.setZoom(1.0);
1214       this.touchHandlers_.stopOperation();
1215       this.imageView_.applyViewportChange();
1216       return true;
1217   }
1219   return false;
1223  * Resize handler.
1224  * @private
1225  */
1226 SlideMode.prototype.onResize_ = function() {
1227   this.touchHandlers_.stopOperation();
1231  * Handles resize event of viewport.
1232  * @private
1233  */
1234 SlideMode.prototype.onViewportResize_ = function() {
1235   // This method must be called after the resize of viewport.
1236   this.editor_.getBuffer().draw();
1240  * Update thumbnails.
1241  */
1242 SlideMode.prototype.updateThumbnails = function() {
1243   this.ribbon_.reset();
1244   if (this.active_)
1245     this.ribbon_.redraw();
1248 // Saving
1251  * Save the current image to a file.
1253  * @param {!Gallery.Item} item Item to save the image.
1254  * @param {function()} callback Callback.
1255  * @private
1256  */
1257 SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
1258   this.showSpinner_(true);
1260   var savedPromise = this.dataModel_.saveItem(
1261       this.volumeManager_,
1262       item,
1263       this.imageView_.getCanvas(),
1264       this.overwriteOriginalCheckbox_.checked);
1266   savedPromise.then(function() {
1267     this.showSpinner_(false);
1268     this.flashSavedLabel_();
1270     // Record UMA for the first edit.
1271     if (this.imageView_.getContentRevision() === 1)
1272       ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
1274     // Users can change overwrite original setting only if there is no undo
1275     // stack and item is original and writable.
1276     var ableToChangeOverwriteOriginalSetting = !this.editor_.canUndo() &&
1277         item.isOriginal() && item.isWritableFile(this.volumeManager_);
1278     this.overwriteOriginalCheckbox_.disabled =
1279         !ableToChangeOverwriteOriginalSetting;
1281     callback();
1282   }.bind(this)).catch(function(error) {
1283     console.error(error.stack || error);
1285     this.showSpinner_(false);
1286     this.errorBanner_.show('GALLERY_SAVE_FAILED');
1288     callback();
1289   }.bind(this));
1293  * Flash 'Saved' label briefly to indicate that the image has been saved.
1294  * @private
1295  */
1296 SlideMode.prototype.flashSavedLabel_ = function() {
1297   this.savedLabel_.hidden = false;
1298   var setLabelHighlighted =
1299       ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1300   setTimeout(setLabelHighlighted.bind(null, true), 0);
1301   setTimeout(setLabelHighlighted.bind(null, false), 300);
1305  * Local storage key for the number of times that
1306  * the overwrite info bubble has been displayed.
1307  * @const {string}
1308  */
1309 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1312  * Local storage key for overwrite original checkbox value.
1313  * @const {string}
1314  */
1315 SlideMode.OVERWRITE_ORIGINAL_KEY = 'gallery-overwrite-original';
1318  * Max number that the overwrite info bubble is shown.
1319  * @const {number}
1320  */
1321 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1324  * Handles change event of overwrite original checkbox.
1325  */
1326 SlideMode.prototype.onOverwriteOriginalCheckboxChanged_ = function() {
1327   var items = {};
1328   items[SlideMode.OVERWRITE_ORIGINAL_KEY] =
1329       this.overwriteOriginalCheckbox_.checked;
1330   chrome.storage.local.set(items);
1334  * Overwrite info bubble close handler.
1335  * @private
1336  */
1337 SlideMode.prototype.onCloseBubble_ = function() {
1338   this.bubble_.hidden = true;
1339   this.setOverwriteBubbleCount_(SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1342 // Slideshow
1345  * Slideshow interval in ms.
1346  */
1347 SlideMode.SLIDESHOW_INTERVAL = 5000;
1350  * First slideshow interval in ms. It should be shorter so that the user
1351  * is not guessing whether the button worked.
1352  */
1353 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1356  * Empirically determined duration of the fullscreen toggle animation.
1357  */
1358 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1361  * @return {boolean} True if the slideshow is on.
1362  * @private
1363  */
1364 SlideMode.prototype.isSlideshowOn_ = function() {
1365   return this.container_.hasAttribute('slideshow');
1369  * Starts the slideshow.
1370  * @param {number=} opt_interval First interval in ms.
1371  * @param {Event=} opt_event Event.
1372  */
1373 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1374   // Reset zoom.
1375   this.viewport_.resetView();
1376   this.imageView_.applyViewportChange();
1378   // Disable touch operation.
1379   this.touchHandlers_.enabled = false;
1381   // Set the attribute early to prevent the toolbar from flashing when
1382   // the slideshow is being started from the mosaic view.
1383   this.container_.setAttribute('slideshow', 'playing');
1385   if (this.active_) {
1386     this.stopEditing_();
1387   } else {
1388     // We are in the Mosaic mode. Toggle the mode but remember to return.
1389     this.leaveAfterSlideshow_ = true;
1391     // Wait until the zoom animation from the mosaic mode is done.
1392     var startSlideshowAfterTransition = function() {
1393       setTimeout(function() {
1394         this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
1395       }.bind(this), ImageView.MODE_TRANSITION_DURATION);
1396     }.bind(this);
1397     this.toggleMode_(startSlideshowAfterTransition);
1398     return;
1399   }
1401   if (opt_event)  // Caused by user action, notify the Gallery.
1402     cr.dispatchSimpleEvent(this, 'useraction');
1404   this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1405   if (!this.fullscreenBeforeSlideshow_) {
1406     this.toggleFullScreen_();
1407     opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1408         SlideMode.FULLSCREEN_TOGGLE_DELAY;
1409   }
1411   // This is a workaround. Mouseout event is not dispatched when window becomes
1412   // fullscreen and cursor gets out of the element
1413   // TODO(yawano): Find better implementation.
1414   this.dimmableUIController_.setCursorOutOfTools();
1416   this.resumeSlideshow_(opt_interval);
1420  * Stops the slideshow.
1421  * @param {Event=} opt_event Event.
1422  * @private
1423  */
1424 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1425   if (!this.isSlideshowOn_())
1426     return;
1428   if (opt_event)  // Caused by user action, notify the Gallery.
1429     cr.dispatchSimpleEvent(this, 'useraction');
1431   this.pauseSlideshow_();
1432   this.container_.removeAttribute('slideshow');
1434   // Do not restore fullscreen if we exited fullscreen while in slideshow.
1435   var fullscreen = util.isFullScreen(this.context_.appWindow);
1436   var toggleModeDelay = 0;
1437   if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1438     this.toggleFullScreen_();
1439     toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1440   }
1441   if (this.leaveAfterSlideshow_) {
1442     this.leaveAfterSlideshow_ = false;
1443     setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1444   }
1446   // Re-enable touch operation.
1447   this.touchHandlers_.enabled = true;
1451  * @return {boolean} True if the slideshow is playing (not paused).
1452  * @private
1453  */
1454 SlideMode.prototype.isSlideshowPlaying_ = function() {
1455   return this.container_.getAttribute('slideshow') === 'playing';
1459  * Pauses/resumes the slideshow.
1460  * @private
1461  */
1462 SlideMode.prototype.toggleSlideshowPause_ = function() {
1463   cr.dispatchSimpleEvent(this, 'useraction');  // Show the tools.
1464   if (this.isSlideshowPlaying_()) {
1465     this.pauseSlideshow_();
1466   } else {
1467     this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1468   }
1472  * @param {number=} opt_interval Slideshow interval in ms.
1473  * @private
1474  */
1475 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1476   console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1478   if (this.slideShowTimeout_)
1479     clearTimeout(this.slideShowTimeout_);
1481   this.slideShowTimeout_ = setTimeout(function() {
1482     this.slideShowTimeout_ = null;
1483     this.selectNext(1);
1484   }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1488  * Resumes the slideshow.
1489  * @param {number=} opt_interval Slideshow interval in ms.
1490  * @private
1491  */
1492 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1493   this.container_.setAttribute('slideshow', 'playing');
1494   this.scheduleNextSlide_(opt_interval);
1498  * Pauses the slideshow.
1499  * @private
1500  */
1501 SlideMode.prototype.pauseSlideshow_ = function() {
1502   this.container_.setAttribute('slideshow', 'paused');
1503   if (this.slideShowTimeout_) {
1504     clearTimeout(this.slideShowTimeout_);
1505     this.slideShowTimeout_ = null;
1506   }
1510  * @return {boolean} True if the editor is active.
1511  */
1512 SlideMode.prototype.isEditing = function() {
1513   return this.container_.hasAttribute('editing');
1517  * Stops editing.
1518  * @private
1519  */
1520 SlideMode.prototype.stopEditing_ = function() {
1521   if (this.isEditing())
1522     this.toggleEditor();
1526  * Activate/deactivate editor.
1527  * @param {Event=} opt_event Event.
1528  */
1529 SlideMode.prototype.toggleEditor = function(opt_event) {
1530   if (opt_event)  // Caused by user action, notify the Gallery.
1531     cr.dispatchSimpleEvent(this, 'useraction');
1533   if (!this.active_) {
1534     this.toggleMode_(this.toggleEditor.bind(this));
1535     return;
1536   }
1538   this.stopSlideshow_();
1540   ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1541   this.editButtonToggleRipple_.activated = this.isEditing();
1543   if (this.isEditing()) { // isEditing has just been flipped to a new value.
1544     // Reset zoom.
1545     this.viewport_.resetView();
1547     // Scale the screen so that it doesn't overlap the toolbars.
1548     this.viewport_.setScreenTop(ImageEditor.Toolbar.HEIGHT);
1549     this.viewport_.setScreenBottom(ImageEditor.Toolbar.HEIGHT);
1551     this.imageView_.applyViewportChange();
1553     // TODO(yawano): Integarate this warning message to the non writable format
1554     //     warning message.
1555     if (this.context_.readonlyDirName) {
1556       this.editor_.getPrompt().showAt(
1557           'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1558     } else {
1559       // If image format is not writable format, show toast to let user know
1560       // that edits will be saved to a copy.
1561       var item = this.getItem(this.getSelectedIndex());
1562       if (!item.isWritableFormat()) {
1563         item.getCopyName().then(function(copyName) {
1564           this.filesToast_.show(
1565               strf('GALLERY_NON_WRITABLE_FORMAT_WARNING', copyName));
1566         }.bind(this));
1567       }
1568     }
1570     this.touchHandlers_.enabled = false;
1571     this.dimmableUIController_.setDisabled(true);
1573     // Show overwrite original bubble if it hasn't been shown for max times.
1574     this.getOverwriteBubbleCount_().then(function(count) {
1575       if (count >= SlideMode.OVERWRITE_BUBBLE_MAX_TIMES)
1576         return;
1578       this.setOverwriteBubbleCount_(count + 1);
1579       this.bubble_.hidden = false;
1580     }.bind(this));
1581   } else {
1582     this.editor_.getPrompt().hide();
1583     this.editor_.leaveModeGently();
1585     this.viewport_.setScreenTop(0);
1586     this.viewport_.setScreenBottom(0);
1587     this.imageView_.applyViewportChange();
1589     this.bubble_.hidden = true;
1591     this.touchHandlers_.enabled = true;
1592     this.dimmableUIController_.setDisabled(false);
1593   }
1597  * Gets count of overwrite bubble.
1598  * @return {!Promise<number>}
1599  * @private
1600  */
1601 SlideMode.prototype.getOverwriteBubbleCount_ = function() {
1602   return new Promise(function(resolve, reject) {
1603     var requests = {};
1604     requests[SlideMode.OVERWRITE_BUBBLE_KEY] = 0;
1606     chrome.storage.local.get(requests, function(results) {
1607       if (!!chrome.runtime.lastError) {
1608         reject(chrome.runtime.lastError);
1609         return;
1610       }
1612       resolve(results[SlideMode.OVERWRITE_BUBBLE_KEY]);
1613     });
1614   });
1618  * Sets count of overwrite bubble.
1619  * @param {number} value
1620  * @private
1621  */
1622 SlideMode.prototype.setOverwriteBubbleCount_ = function(value) {
1623   var requests = {};
1624   requests[SlideMode.OVERWRITE_BUBBLE_KEY] = value;
1625   chrome.storage.local.set(requests);
1629  * Prints the current item.
1630  * @private
1631  */
1632 SlideMode.prototype.print_ = function() {
1633   cr.dispatchSimpleEvent(this, 'useraction');
1634   window.print();
1638  * Shows/hides the busy spinner.
1640  * @param {boolean} on True if show, false if hide.
1641  * @private
1642  */
1643 SlideMode.prototype.showSpinner_ = function(on) {
1644   if (this.spinnerTimer_) {
1645     clearTimeout(this.spinnerTimer_);
1646     this.spinnerTimer_ = null;
1647   }
1649   if (on) {
1650     this.spinnerTimer_ = setTimeout(function() {
1651       this.spinnerTimer_ = null;
1652       ImageUtil.setAttribute(this.container_, 'spinner', true);
1653     }.bind(this), 1000);
1654   } else {
1655     ImageUtil.setAttribute(this.container_, 'spinner', false);
1656   }
1660  * Apply the change of viewport.
1661  */
1662 SlideMode.prototype.applyViewportChange = function() {
1663   this.imageView_.applyViewportChange();
1667  * Touch handlers of the slide mode.
1668  * @param {!Element} targetElement Event source.
1669  * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1670  * @struct
1671  * @constructor
1672  */
1673 function TouchHandler(targetElement, slideMode) {
1674   /**
1675    * Event source.
1676    * @type {!Element}
1677    * @private
1678    * @const
1679    */
1680   this.targetElement_ = targetElement;
1682   /**
1683    * Target of touch operations.
1684    * @type {!SlideMode}
1685    * @private
1686    * @const
1687    */
1688   this.slideMode_ = slideMode;
1690   /**
1691    * Flag to enable/disable touch operation.
1692    * @type {boolean}
1693    * @private
1694    */
1695   this.enabled_ = true;
1697   /**
1698    * Whether it is in a touch operation that is started from targetElement or
1699    * not.
1700    * @type {boolean}
1701    * @private
1702    */
1703   this.touchStarted_ = false;
1705   /**
1706    * The swipe action that should happen only once in an operation is already
1707    * done or not.
1708    * @type {boolean}
1709    * @private
1710    */
1711   this.done_ = false;
1713   /**
1714    * Event on beginning of the current gesture.
1715    * The variable is updated when the number of touch finger changed.
1716    * @type {TouchEvent}
1717    * @private
1718    */
1719   this.gestureStartEvent_ = null;
1721   /**
1722    * Rotation value on beginning of the current gesture.
1723    * @type {number}
1724    * @private
1725    */
1726   this.gestureStartRotation_ = 0;
1728   /**
1729    * Last touch event.
1730    * @type {TouchEvent}
1731    * @private
1732    */
1733   this.lastEvent_ = null;
1735   /**
1736    * Zoom value just after last touch event.
1737    * @type {number}
1738    * @private
1739    */
1740   this.lastZoom_ = 1.0;
1742   targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
1743   var onTouchEventBound = this.onTouchEvent_.bind(this);
1744   targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
1745   targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
1747   targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
1751  * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1752  * horizontally it's considered as a swipe gesture (change the current image).
1753  * @type {number}
1754  * @const
1755  */
1756 TouchHandler.SWIPE_THRESHOLD = 100;
1759  * Rotation threshold in degrees.
1760  * @type {number}
1761  * @const
1762  */
1763 TouchHandler.ROTATION_THRESHOLD = 25;
1766  * Obtains distance between fingers.
1767  * @param {!TouchEvent} event Touch event. It should include more than two
1768  *     touches.
1769  * @return {number} Distance between touch[0] and touch[1].
1770  */
1771 TouchHandler.getDistance = function(event) {
1772   var touch1 = event.touches[0];
1773   var touch2 = event.touches[1];
1774   var dx = touch1.clientX - touch2.clientX;
1775   var dy = touch1.clientY - touch2.clientY;
1776   return Math.sqrt(dx * dx + dy * dy);
1780  * Obtains the degrees of the pinch twist angle.
1781  * @param {!TouchEvent} event1 Start touch event. It should include more than
1782  *     two touches.
1783  * @param {!TouchEvent} event2 Current touch event. It should include more than
1784  *     two touches.
1785  * @return {number} Degrees of the pinch twist angle.
1786  */
1787 TouchHandler.getTwistAngle = function(event1, event2) {
1788   var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
1789   var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
1790   var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
1791   var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
1792   var innerProduct = dx1 * dx2 + dy1 * dy2;  // |v1| * |v2| * cos(t) = x / r
1793   var outerProduct = dx1 * dy2 - dy1 * dx2;  // |v1| * |v2| * sin(t) = y / r
1794   return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI;  // atan(y / x)
1797 TouchHandler.prototype = /** @struct */ {
1798   /**
1799    * @param {boolean} flag New value.
1800    */
1801   set enabled(flag) {
1802     this.enabled_ = flag;
1803     if (!this.enabled_)
1804       this.stopOperation();
1805   }
1809  * Stops the current touch operation.
1810  */
1811 TouchHandler.prototype.stopOperation = function() {
1812   this.touchStarted_ = false;
1813   this.done_ = false;
1814   this.gestureStartEvent_ = null;
1815   this.lastEvent_ = null;
1816   this.lastZoom_ = 1.0;
1820  * Handles touch start events.
1821  * @param {!Event} event Touch event.
1822  * @private
1823  */
1824 TouchHandler.prototype.onTouchStart_ = function(event) {
1825   event = assertInstanceof(event, TouchEvent);
1826   if (this.enabled_ && event.touches.length === 1)
1827     this.touchStarted_ = true;
1831  * Handles touch move and touch end events.
1832  * @param {!Event} event Touch event.
1833  * @private
1834  */
1835 TouchHandler.prototype.onTouchEvent_ = function(event) {
1836   event = assertInstanceof(event, TouchEvent);
1837   // Check if the current touch operation started from the target element or
1838   // not.
1839   if (!this.touchStarted_)
1840     return;
1842   // Check if the current touch operation ends with the event.
1843   if (event.touches.length === 0) {
1844     this.stopOperation();
1845     return;
1846   }
1848   // Check if a new gesture started or not.
1849   var viewport = this.slideMode_.getViewport();
1850   if (!this.lastEvent_ ||
1851       this.lastEvent_.touches.length !== event.touches.length) {
1852     if (event.touches.length === 2 ||
1853         event.touches.length === 1) {
1854       this.gestureStartEvent_ = event;
1855       this.gestureStartRotation_ = viewport.getRotation();
1856       this.lastEvent_ = event;
1857       this.lastZoom_ = viewport.getZoom();
1858     } else {
1859       this.gestureStartEvent_ = null;
1860       this.gestureStartRotation_ = 0;
1861       this.lastEvent_ = null;
1862       this.lastZoom_ = 1.0;
1863     }
1864     return;
1865   }
1867   // Handle the gesture movement.
1868   switch (event.touches.length) {
1869     case 1:
1870       if (viewport.isZoomed()) {
1871         // Scrolling an image by swipe.
1872         var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
1873         var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
1874         viewport.setOffset(
1875             viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
1876         this.slideMode_.applyViewportChange();
1877       } else {
1878         // Traversing images by swipe.
1879         if (this.done_)
1880           break;
1881         var dx =
1882             event.touches[0].clientX -
1883             this.gestureStartEvent_.touches[0].clientX;
1884         if (dx > TouchHandler.SWIPE_THRESHOLD) {
1885           this.slideMode_.advanceManually(-1);
1886           this.done_ = true;
1887         } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
1888           this.slideMode_.advanceManually(1);
1889           this.done_ = true;
1890         }
1891       }
1892       break;
1894     case 2:
1895       // Pinch zoom.
1896       var distance1 = TouchHandler.getDistance(this.lastEvent_);
1897       var distance2 = TouchHandler.getDistance(event);
1898       if (distance1 === 0)
1899         break;
1900       var zoom = distance2 / distance1 * this.lastZoom_;
1901       viewport.setZoom(zoom);
1903       // Pinch rotation.
1904       assert(this.gestureStartEvent_);
1905       var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
1906       if (angle > TouchHandler.ROTATION_THRESHOLD)
1907         viewport.setRotation(this.gestureStartRotation_ + 1);
1908       else if (angle < -TouchHandler.ROTATION_THRESHOLD)
1909         viewport.setRotation(this.gestureStartRotation_ - 1);
1910       else
1911         viewport.setRotation(this.gestureStartRotation_);
1912       this.slideMode_.applyViewportChange();
1913       break;
1914   }
1916   // Update the last event.
1917   this.lastEvent_ = event;
1918   this.lastZoom_ = viewport.getZoom();
1922  * Handles mouse wheel events.
1923  * @param {!Event} event Wheel event.
1924  * @private
1925  */
1926 TouchHandler.prototype.onMouseWheel_ = function(event) {
1927   var event = assertInstanceof(event, MouseEvent);
1928   var viewport = this.slideMode_.getViewport();
1929   if (!this.enabled_ || !viewport.isZoomed())
1930     return;
1931   this.stopOperation();
1932   viewport.setOffset(
1933       viewport.getOffsetX() + event.wheelDeltaX,
1934       viewport.getOffsetY() + event.wheelDeltaY);
1935   this.slideMode_.applyViewportChange();