Home | History | Annotate | Download | only in js
      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.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Slide mode displays a single image and has a set of controls to navigate
      9  * between the images and to edit an image.
     10  *
     11  * TODO(kaznacheev): Introduce a parameter object.
     12  *
     13  * @param {Element} container Main container element.
     14  * @param {Element} content Content container element.
     15  * @param {Element} toolbar Toolbar element.
     16  * @param {ImageEditor.Prompt} prompt Prompt.
     17  * @param {cr.ui.ArrayDataModel} dataModel Data model.
     18  * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
     19  * @param {Object} context Context.
     20  * @param {function(function())} toggleMode Function to toggle the Gallery mode.
     21  * @param {function(string):string} displayStringFunction String formatting
     22  *     function.
     23  * @constructor
     24  */
     25 function SlideMode(container, content, toolbar, prompt,
     26                    dataModel, selectionModel, context,
     27                    toggleMode, displayStringFunction) {
     28   this.container_ = container;
     29   this.document_ = container.ownerDocument;
     30   this.content = content;
     31   this.toolbar_ = toolbar;
     32   this.prompt_ = prompt;
     33   this.dataModel_ = dataModel;
     34   this.selectionModel_ = selectionModel;
     35   this.context_ = context;
     36   this.metadataCache_ = context.metadataCache;
     37   this.toggleMode_ = toggleMode;
     38   this.displayStringFunction_ = displayStringFunction;
     39 
     40   this.onSelectionBound_ = this.onSelection_.bind(this);
     41   this.onSpliceBound_ = this.onSplice_.bind(this);
     42   this.onContentBound_ = this.onContentChange_.bind(this);
     43 
     44   // Unique numeric key, incremented per each load attempt used to discard
     45   // old attempts. This can happen especially when changing selection fast or
     46   // Internet connection is slow.
     47   this.currentUniqueKey_ = 0;
     48 
     49   this.initListeners_();
     50   this.initDom_();
     51 }
     52 
     53 /**
     54  * SlideMode extends cr.EventTarget.
     55  */
     56 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
     57 
     58 /**
     59  * List of available editor modes.
     60  * @type {Array.<ImageEditor.Mode>}
     61  */
     62 SlideMode.editorModes = [
     63   new ImageEditor.Mode.InstantAutofix(),
     64   new ImageEditor.Mode.Crop(),
     65   new ImageEditor.Mode.Exposure(),
     66   new ImageEditor.Mode.OneClick(
     67       'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
     68   new ImageEditor.Mode.OneClick(
     69       'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
     70 ];
     71 
     72 /**
     73  * @return {string} Mode name.
     74  */
     75 SlideMode.prototype.getName = function() { return 'slide' };
     76 
     77 /**
     78  * @return {string} Mode title.
     79  */
     80 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' };
     81 
     82 /**
     83  * Initialize the listeners.
     84  * @private
     85  */
     86 SlideMode.prototype.initListeners_ = function() {
     87   window.addEventListener('resize', this.onResize_.bind(this), false);
     88 };
     89 
     90 /**
     91  * Initialize the UI.
     92  * @private
     93  */
     94 SlideMode.prototype.initDom_ = function() {
     95   // Container for displayed image or video.
     96   this.imageContainer_ = util.createChild(
     97       this.document_.querySelector('.content'), 'image-container');
     98   this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
     99 
    100   this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
    101 
    102   // Overwrite options and info bubble.
    103   this.options_ = util.createChild(
    104       this.toolbar_.querySelector('.filename-spacer'), 'options');
    105 
    106   this.savedLabel_ = util.createChild(this.options_, 'saved');
    107   this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
    108 
    109   var overwriteOriginalBox =
    110       util.createChild(this.options_, 'overwrite-original');
    111 
    112   this.overwriteOriginal_ = util.createChild(
    113       overwriteOriginalBox, 'common white', 'input');
    114   this.overwriteOriginal_.type = 'checkbox';
    115   this.overwriteOriginal_.id = 'overwrite-checkbox';
    116   util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) {
    117     // Out-of-the box default is 'true'
    118     this.overwriteOriginal_.checked =
    119         (typeof value !== 'string' || value === 'true');
    120   }.bind(this));
    121   this.overwriteOriginal_.addEventListener('click',
    122       this.onOverwriteOriginalClick_.bind(this));
    123 
    124   var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
    125   overwriteLabel.textContent =
    126       this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
    127   overwriteLabel.setAttribute('for', 'overwrite-checkbox');
    128 
    129   this.bubble_ = util.createChild(this.toolbar_, 'bubble');
    130   this.bubble_.hidden = true;
    131 
    132   var bubbleContent = util.createChild(this.bubble_);
    133   bubbleContent.innerHTML = this.displayStringFunction_(
    134       'GALLERY_OVERWRITE_BUBBLE');
    135 
    136   util.createChild(this.bubble_, 'pointer bottom', 'span');
    137 
    138   var bubbleClose = util.createChild(this.bubble_, 'close-x');
    139   bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
    140 
    141   // Video player controls.
    142   this.mediaSpacer_ =
    143       util.createChild(this.container_, 'video-controls-spacer');
    144   this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool');
    145   this.mediaControls_ = new VideoControls(
    146       this.mediaToolbar_,
    147       this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'),
    148       this.displayStringFunction_.bind(this),
    149       this.toggleFullScreen_.bind(this),
    150       this.container_);
    151 
    152   // Ribbon and related controls.
    153   this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
    154 
    155   this.arrowLeft_ =
    156       util.createChild(this.arrowBox_, 'arrow left tool dimmable');
    157   this.arrowLeft_.addEventListener('click',
    158       this.advanceManually.bind(this, -1));
    159   util.createChild(this.arrowLeft_);
    160 
    161   util.createChild(this.arrowBox_, 'arrow-spacer');
    162 
    163   this.arrowRight_ =
    164       util.createChild(this.arrowBox_, 'arrow right tool dimmable');
    165   this.arrowRight_.addEventListener('click',
    166       this.advanceManually.bind(this, 1));
    167   util.createChild(this.arrowRight_);
    168 
    169   this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer');
    170   this.ribbon_ = new Ribbon(this.document_,
    171       this.metadataCache_, this.dataModel_, this.selectionModel_);
    172   this.ribbonSpacer_.appendChild(this.ribbon_);
    173 
    174   // Error indicator.
    175   var errorWrapper = util.createChild(this.container_, 'prompt-wrapper');
    176   errorWrapper.setAttribute('pos', 'center');
    177 
    178   this.errorBanner_ = util.createChild(errorWrapper, 'error-banner');
    179 
    180   util.createChild(this.container_, 'spinner');
    181 
    182   var slideShowButton = util.createChild(this.toolbar_,
    183       'button slideshow', 'button');
    184   slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
    185   slideShowButton.addEventListener('click',
    186       this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
    187 
    188   var slideShowToolbar =
    189       util.createChild(this.container_, 'tool slideshow-toolbar');
    190   util.createChild(slideShowToolbar, 'slideshow-play').
    191       addEventListener('click', this.toggleSlideshowPause_.bind(this));
    192   util.createChild(slideShowToolbar, 'slideshow-end').
    193       addEventListener('click', this.stopSlideshow_.bind(this));
    194 
    195   // Editor.
    196 
    197   this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button');
    198   this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
    199   this.editButton_.setAttribute('disabled', '');  // Disabled by default.
    200   this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
    201 
    202   this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button');
    203   this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
    204   this.printButton_.setAttribute('disabled', '');  // Disabled by default.
    205   this.printButton_.addEventListener('click', this.print_.bind(this));
    206 
    207   this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer');
    208   this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
    209 
    210   this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
    211   this.editBarModeWrapper_ = util.createChild(
    212       this.editBarMode_, 'edit-modal-wrapper');
    213   this.editBarModeWrapper_.hidden = true;
    214 
    215   // Objects supporting image display and editing.
    216   this.viewport_ = new Viewport();
    217 
    218   this.imageView_ = new ImageView(
    219       this.imageContainer_,
    220       this.viewport_,
    221       this.metadataCache_);
    222 
    223   this.editor_ = new ImageEditor(
    224       this.viewport_,
    225       this.imageView_,
    226       this.prompt_,
    227       {
    228         root: this.container_,
    229         image: this.imageContainer_,
    230         toolbar: this.editBarMain_,
    231         mode: this.editBarModeWrapper_
    232       },
    233       SlideMode.editorModes,
    234       this.displayStringFunction_,
    235       this.onToolsVisibilityChanged_.bind(this));
    236 
    237   this.editor_.getBuffer().addOverlay(
    238       new SwipeOverlay(this.advanceManually.bind(this)));
    239 };
    240 
    241 /**
    242  * Load items, display the selected item.
    243  * @param {Rect} zoomFromRect Rectangle for zoom effect.
    244  * @param {function} displayCallback Called when the image is displayed.
    245  * @param {function} loadCallback Called when the image is displayed.
    246  */
    247 SlideMode.prototype.enter = function(
    248     zoomFromRect, displayCallback, loadCallback) {
    249   this.sequenceDirection_ = 0;
    250   this.sequenceLength_ = 0;
    251 
    252   var loadDone = function(loadType, delay) {
    253     this.active_ = true;
    254 
    255     this.selectionModel_.addEventListener('change', this.onSelectionBound_);
    256     this.dataModel_.addEventListener('splice', this.onSpliceBound_);
    257     this.dataModel_.addEventListener('content', this.onContentBound_);
    258 
    259     ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
    260     this.ribbon_.enable();
    261 
    262     // Wait 1000ms after the animation is done, then prefetch the next image.
    263     this.requestPrefetch(1, delay + 1000);
    264 
    265     if (loadCallback) loadCallback();
    266   }.bind(this);
    267 
    268   // The latest |leave| call might have left the image animating. Remove it.
    269   this.unloadImage_();
    270 
    271   if (this.getItemCount_() === 0) {
    272     this.displayedIndex_ = -1;
    273     //TODO(kaznacheev) Show this message in the grid mode too.
    274     this.showErrorBanner_('GALLERY_NO_IMAGES');
    275     loadDone();
    276   } else {
    277     // Remember the selection if it is empty or multiple. It will be restored
    278     // in |leave| if the user did not changing the selection manually.
    279     var currentSelection = this.selectionModel_.selectedIndexes;
    280     if (currentSelection.length === 1)
    281       this.savedSelection_ = null;
    282     else
    283       this.savedSelection_ = currentSelection;
    284 
    285     // Ensure valid single selection.
    286     // Note that the SlideMode object is not listening to selection change yet.
    287     this.select(Math.max(0, this.getSelectedIndex()));
    288     this.displayedIndex_ = this.getSelectedIndex();
    289 
    290     var selectedItem = this.getSelectedItem();
    291     // Show the selected item ASAP, then complete the initialization
    292     // (loading the ribbon thumbnails can take some time).
    293     this.metadataCache_.getOne(selectedItem.getEntry(), Gallery.METADATA_TYPE,
    294         function(metadata) {
    295           this.loadItem_(selectedItem.getEntry(), metadata,
    296               zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect),
    297               displayCallback, loadDone);
    298         }.bind(this));
    299 
    300   }
    301 };
    302 
    303 /**
    304  * Leave the mode.
    305  * @param {Rect} zoomToRect Rectangle for zoom effect.
    306  * @param {function} callback Called when the image is committed and
    307  *   the zoom-out animation has started.
    308  */
    309 SlideMode.prototype.leave = function(zoomToRect, callback) {
    310   var commitDone = function() {
    311       this.stopEditing_();
    312       this.stopSlideshow_();
    313       ImageUtil.setAttribute(this.arrowBox_, 'active', false);
    314       this.selectionModel_.removeEventListener(
    315           'change', this.onSelectionBound_);
    316       this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
    317       this.dataModel_.removeEventListener('content', this.onContentBound_);
    318       this.ribbon_.disable();
    319       this.active_ = false;
    320       if (this.savedSelection_)
    321         this.selectionModel_.selectedIndexes = this.savedSelection_;
    322       this.unloadImage_(zoomToRect);
    323       callback();
    324     }.bind(this);
    325 
    326   if (this.getItemCount_() === 0) {
    327     this.showErrorBanner_(false);
    328     commitDone();
    329   } else {
    330     this.commitItem_(commitDone);
    331   }
    332 
    333   // Disable the slide-mode only buttons when leaving.
    334   this.editButton_.setAttribute('disabled', '');
    335   this.printButton_.setAttribute('disabled', '');
    336 };
    337 
    338 
    339 /**
    340  * Execute an action when the editor is not busy.
    341  *
    342  * @param {function} action Function to execute.
    343  */
    344 SlideMode.prototype.executeWhenReady = function(action) {
    345   this.editor_.executeWhenReady(action);
    346 };
    347 
    348 /**
    349  * @return {boolean} True if the mode has active tools (that should not fade).
    350  */
    351 SlideMode.prototype.hasActiveTool = function() {
    352   return this.isEditing();
    353 };
    354 
    355 /**
    356  * @return {number} Item count.
    357  * @private
    358  */
    359 SlideMode.prototype.getItemCount_ = function() {
    360   return this.dataModel_.length;
    361 };
    362 
    363 /**
    364  * @param {number} index Index.
    365  * @return {Gallery.Item} Item.
    366  */
    367 SlideMode.prototype.getItem = function(index) {
    368   return this.dataModel_.item(index);
    369 };
    370 
    371 /**
    372  * @return {Gallery.Item} Selected index.
    373  */
    374 SlideMode.prototype.getSelectedIndex = function() {
    375   return this.selectionModel_.selectedIndex;
    376 };
    377 
    378 /**
    379  * @return {Rect} Screen rectangle of the selected image.
    380  */
    381 SlideMode.prototype.getSelectedImageRect = function() {
    382   if (this.getSelectedIndex() < 0)
    383     return null;
    384   else
    385     return this.viewport_.getScreenClipped();
    386 };
    387 
    388 /**
    389  * @return {Gallery.Item} Selected item.
    390  */
    391 SlideMode.prototype.getSelectedItem = function() {
    392   return this.getItem(this.getSelectedIndex());
    393 };
    394 
    395 /**
    396  * Toggles the full screen mode.
    397  * @private
    398  */
    399 SlideMode.prototype.toggleFullScreen_ = function() {
    400   util.toggleFullScreen(this.context_.appWindow,
    401                         !util.isFullScreen(this.context_.appWindow));
    402 };
    403 
    404 /**
    405  * Selection change handler.
    406  *
    407  * Commits the current image and displays the newly selected image.
    408  * @private
    409  */
    410 SlideMode.prototype.onSelection_ = function() {
    411   if (this.selectionModel_.selectedIndexes.length === 0)
    412     return;  // Temporary empty selection.
    413 
    414   // Forget the saved selection if the user changed the selection manually.
    415   if (!this.isSlideshowOn_())
    416     this.savedSelection_ = null;
    417 
    418   if (this.getSelectedIndex() === this.displayedIndex_)
    419     return;  // Do not reselect.
    420 
    421   this.commitItem_(this.loadSelectedItem_.bind(this));
    422 };
    423 
    424 /**
    425  * Handles changes in tools visibility, and if the header is dimmed, then
    426  * requests disabling the draggable app region.
    427  *
    428  * @private
    429  */
    430 SlideMode.prototype.onToolsVisibilityChanged_ = function() {
    431   var headerDimmed =
    432       this.document_.querySelector('.header').hasAttribute('dimmed');
    433   this.context_.onAppRegionChanged(!headerDimmed);
    434 };
    435 
    436 /**
    437  * Change the selection.
    438  *
    439  * @param {number} index New selected index.
    440  * @param {number=} opt_slideHint Slide animation direction (-1|1).
    441  */
    442 SlideMode.prototype.select = function(index, opt_slideHint) {
    443   this.slideHint_ = opt_slideHint;
    444   this.selectionModel_.selectedIndex = index;
    445   this.selectionModel_.leadIndex = index;
    446 };
    447 
    448 /**
    449  * Load the selected item.
    450  *
    451  * @private
    452  */
    453 SlideMode.prototype.loadSelectedItem_ = function() {
    454   var slideHint = this.slideHint_;
    455   this.slideHint_ = undefined;
    456 
    457   var index = this.getSelectedIndex();
    458   if (index === this.displayedIndex_)
    459     return;  // Do not reselect.
    460 
    461   var step = slideHint || (index - this.displayedIndex_);
    462 
    463   if (Math.abs(step) != 1) {
    464     // Long leap, the sequence is broken, we have no good prefetch candidate.
    465     this.sequenceDirection_ = 0;
    466     this.sequenceLength_ = 0;
    467   } else if (this.sequenceDirection_ === step) {
    468     // Keeping going in sequence.
    469     this.sequenceLength_++;
    470   } else {
    471     // Reversed the direction. Reset the counter.
    472     this.sequenceDirection_ = step;
    473     this.sequenceLength_ = 1;
    474   }
    475 
    476   if (this.sequenceLength_ <= 1) {
    477     // We have just broke the sequence. Touch the current image so that it stays
    478     // in the cache longer.
    479     this.imageView_.prefetch(this.imageView_.contentEntry_);
    480   }
    481 
    482   this.displayedIndex_ = index;
    483 
    484   function shouldPrefetch(loadType, step, sequenceLength) {
    485     // Never prefetch when selecting out of sequence.
    486     if (Math.abs(step) != 1)
    487       return false;
    488 
    489     // Never prefetch after a video load (decoding the next image can freeze
    490     // the UI for a second or two).
    491     if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE)
    492       return false;
    493 
    494     // Always prefetch if the previous load was from cache.
    495     if (loadType === ImageView.LOAD_TYPE_CACHED_FULL)
    496       return true;
    497 
    498     // Prefetch if we have been going in the same direction for long enough.
    499     return sequenceLength >= 3;
    500   }
    501 
    502   var selectedItem = this.getSelectedItem();
    503   this.currentUniqueKey_++;
    504   var selectedUniqueKey = this.currentUniqueKey_;
    505   var onMetadata = function(metadata) {
    506     // Discard, since another load has been invoked after this one.
    507     if (selectedUniqueKey != this.currentUniqueKey_) return;
    508     this.loadItem_(selectedItem.getEntry(), metadata,
    509         new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
    510         function() {} /* no displayCallback */,
    511         function(loadType, delay) {
    512           // Discard, since another load has been invoked after this one.
    513           if (selectedUniqueKey != this.currentUniqueKey_) return;
    514           if (shouldPrefetch(loadType, step, this.sequenceLength_)) {
    515             this.requestPrefetch(step, delay);
    516           }
    517           if (this.isSlideshowPlaying_())
    518             this.scheduleNextSlide_();
    519         }.bind(this));
    520   }.bind(this);
    521   this.metadataCache_.getOne(
    522       selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata);
    523 };
    524 
    525 /**
    526  * Unload the current image.
    527  *
    528  * @param {Rect} zoomToRect Rectangle for zoom effect.
    529  * @private
    530  */
    531 SlideMode.prototype.unloadImage_ = function(zoomToRect) {
    532   this.imageView_.unload(zoomToRect);
    533   this.container_.removeAttribute('video');
    534 };
    535 
    536 /**
    537  * Data model 'splice' event handler.
    538  * @param {Event} event Event.
    539  * @private
    540  */
    541 SlideMode.prototype.onSplice_ = function(event) {
    542   ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
    543 
    544   // Splice invalidates saved indices, drop the saved selection.
    545   this.savedSelection_ = null;
    546 
    547   if (event.removed.length != 1)
    548     return;
    549 
    550   // Delay the selection to let the ribbon splice handler work first.
    551   setTimeout(function() {
    552     if (event.index < this.dataModel_.length) {
    553       // There is the next item, select it.
    554       // The next item is now at the same index as the removed one, so we need
    555       // to correct displayIndex_ so that loadSelectedItem_ does not think
    556       // we are re-selecting the same item (and does right-to-left slide-in
    557       // animation).
    558       this.displayedIndex_ = event.index - 1;
    559       this.select(event.index);
    560     } else if (this.dataModel_.length) {
    561       // Removed item is the rightmost, but there are more items.
    562       this.select(event.index - 1);  // Select the new last index.
    563     } else {
    564       // No items left. Unload the image and show the banner.
    565       this.commitItem_(function() {
    566         this.unloadImage_();
    567         this.showErrorBanner_('GALLERY_NO_IMAGES');
    568       }.bind(this));
    569     }
    570   }.bind(this), 0);
    571 };
    572 
    573 /**
    574  * @param {number} direction -1 for left, 1 for right.
    575  * @return {number} Next index in the given direction, with wrapping.
    576  * @private
    577  */
    578 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
    579   function advance(index, limit) {
    580     index += (direction > 0 ? 1 : -1);
    581     if (index < 0)
    582       return limit - 1;
    583     if (index === limit)
    584       return 0;
    585     return index;
    586   }
    587 
    588   // If the saved selection is multiple the Slideshow should cycle through
    589   // the saved selection.
    590   if (this.isSlideshowOn_() &&
    591       this.savedSelection_ && this.savedSelection_.length > 1) {
    592     var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
    593         this.savedSelection_.length);
    594     return this.savedSelection_[pos];
    595   } else {
    596     return advance(this.getSelectedIndex(), this.getItemCount_());
    597   }
    598 };
    599 
    600 /**
    601  * Advance the selection based on the pressed key ID.
    602  * @param {string} keyID Key identifier.
    603  */
    604 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
    605   this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1);
    606 };
    607 
    608 /**
    609  * Advance the selection as a result of a user action (as opposed to an
    610  * automatic change in the slideshow mode).
    611  * @param {number} direction -1 for left, 1 for right.
    612  */
    613 SlideMode.prototype.advanceManually = function(direction) {
    614   if (this.isSlideshowPlaying_()) {
    615     this.pauseSlideshow_();
    616     cr.dispatchSimpleEvent(this, 'useraction');
    617   }
    618   this.selectNext(direction);
    619 };
    620 
    621 /**
    622  * Select the next item.
    623  * @param {number} direction -1 for left, 1 for right.
    624  */
    625 SlideMode.prototype.selectNext = function(direction) {
    626   this.select(this.getNextSelectedIndex_(direction), direction);
    627 };
    628 
    629 /**
    630  * Select the first item.
    631  */
    632 SlideMode.prototype.selectFirst = function() {
    633   this.select(0);
    634 };
    635 
    636 /**
    637  * Select the last item.
    638  */
    639 SlideMode.prototype.selectLast = function() {
    640   this.select(this.getItemCount_() - 1);
    641 };
    642 
    643 // Loading/unloading
    644 
    645 /**
    646  * Load and display an item.
    647  *
    648  * @param {FileEntry} entry Item entry to be loaded.
    649  * @param {Object} metadata Item metadata.
    650  * @param {Object} effect Transition effect object.
    651  * @param {function} displayCallback Called when the image is displayed
    652  *     (which can happen before the image load due to caching).
    653  * @param {function} loadCallback Called when the image is fully loaded.
    654  * @private
    655  */
    656 SlideMode.prototype.loadItem_ = function(
    657     entry, metadata, effect, displayCallback, loadCallback) {
    658   this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata);
    659 
    660   this.showSpinner_(true);
    661 
    662   var loadDone = function(loadType, delay, error) {
    663     var video = this.isShowingVideo_();
    664     ImageUtil.setAttribute(this.container_, 'video', video);
    665 
    666     this.showSpinner_(false);
    667     if (loadType === ImageView.LOAD_TYPE_ERROR) {
    668       // if we have a specific error, then display it
    669       if (error) {
    670         this.showErrorBanner_(error);
    671       } else {
    672         // otherwise try to infer general error
    673         this.showErrorBanner_(
    674             video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR');
    675       }
    676     } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) {
    677       this.showErrorBanner_(
    678           video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE');
    679     }
    680 
    681     if (video) {
    682       // The editor toolbar does not make sense for video, hide it.
    683       this.stopEditing_();
    684       this.mediaControls_.attachMedia(this.imageView_.getVideo());
    685 
    686       // TODO(kaznacheev): Add metrics for video playback.
    687     } else {
    688       ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
    689 
    690       var toMillions = function(number) {
    691         return Math.round(number / (1000 * 1000));
    692       };
    693 
    694       ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
    695           toMillions(metadata.filesystem.size));
    696 
    697       var canvas = this.imageView_.getCanvas();
    698       ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
    699           toMillions(canvas.width * canvas.height));
    700 
    701       var extIndex = entry.name.lastIndexOf('.');
    702       var ext = extIndex < 0 ? '' :
    703           entry.name.substr(extIndex + 1).toLowerCase();
    704       if (ext === 'jpeg') ext = 'jpg';
    705       ImageUtil.metrics.recordEnum(
    706           ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
    707     }
    708 
    709     // Enable or disable buttons for editing and printing.
    710     if (video || error) {
    711       this.editButton_.setAttribute('disabled', '');
    712       this.printButton_.setAttribute('disabled', '');
    713     } else {
    714       this.editButton_.removeAttribute('disabled');
    715       this.printButton_.removeAttribute('disabled');
    716     }
    717 
    718     // For once edited image, disallow the 'overwrite' setting change.
    719     ImageUtil.setAttribute(this.options_, 'saved',
    720         !this.getSelectedItem().isOriginal());
    721 
    722     util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
    723         function(value) {
    724           var times = typeof value === 'string' ? parseInt(value, 10) : 0;
    725           if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
    726             this.bubble_.hidden = false;
    727             if (this.isEditing()) {
    728               util.platform.setPreference(
    729                   SlideMode.OVERWRITE_BUBBLE_KEY, times + 1);
    730             }
    731           }
    732         }.bind(this));
    733 
    734     loadCallback(loadType, delay);
    735   }.bind(this);
    736 
    737   var displayDone = function() {
    738     cr.dispatchSimpleEvent(this, 'image-displayed');
    739     displayCallback();
    740   }.bind(this);
    741 
    742   this.editor_.openSession(entry, metadata, effect,
    743       this.saveCurrentImage_.bind(this), displayDone, loadDone);
    744 };
    745 
    746 /**
    747  * Commit changes to the current item and reset all messages/indicators.
    748  *
    749  * @param {function} callback Callback.
    750  * @private
    751  */
    752 SlideMode.prototype.commitItem_ = function(callback) {
    753   this.showSpinner_(false);
    754   this.showErrorBanner_(false);
    755   this.editor_.getPrompt().hide();
    756 
    757   // Detach any media attached to the controls.
    758   if (this.mediaControls_.getMedia())
    759     this.mediaControls_.detachMedia();
    760 
    761   // If showing the video, then pause it. Note, that it may not be attached
    762   // to the media controls yet.
    763   if (this.isShowingVideo_()) {
    764     this.imageView_.getVideo().pause();
    765     // Force stop downloading, if uncached on Drive.
    766     this.imageView_.getVideo().src = '';
    767     this.imageView_.getVideo().load();
    768   }
    769 
    770   this.editor_.closeSession(callback);
    771 };
    772 
    773 /**
    774  * Request a prefetch for the next image.
    775  *
    776  * @param {number} direction -1 or 1.
    777  * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
    778  *   loading from disrupting the animation that might be still in progress.
    779  */
    780 SlideMode.prototype.requestPrefetch = function(direction, delay) {
    781   if (this.getItemCount_() <= 1) return;
    782 
    783   var index = this.getNextSelectedIndex_(direction);
    784   var nextItemEntry = this.getItem(index).getEntry();
    785   this.imageView_.prefetch(nextItemEntry, delay);
    786 };
    787 
    788 // Event handlers.
    789 
    790 /**
    791  * Unload handler, to be called from the top frame.
    792  * @param {boolean} exiting True if the app is exiting.
    793  */
    794 SlideMode.prototype.onUnload = function(exiting) {
    795   if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) {
    796     this.mediaControls_.savePosition(exiting);
    797   }
    798 };
    799 
    800 /**
    801  * Click handler for the image container.
    802  *
    803  * @param {Event} event Mouse click event.
    804  * @private
    805  */
    806 SlideMode.prototype.onClick_ = function(event) {
    807   if (!this.isShowingVideo_() || !this.mediaControls_.getMedia())
    808     return;
    809   if (event.ctrlKey) {
    810     this.mediaControls_.toggleLoopedModeWithFeedback(true);
    811     if (!this.mediaControls_.isPlaying())
    812       this.mediaControls_.togglePlayStateWithFeedback();
    813   } else {
    814     this.mediaControls_.togglePlayStateWithFeedback();
    815   }
    816 };
    817 
    818 /**
    819  * Click handler for the entire document.
    820  * @param {Event} e Mouse click event.
    821  * @private
    822  */
    823 SlideMode.prototype.onDocumentClick_ = function(e) {
    824   // Close the bubble if clicked outside of it and if it is visible.
    825   if (!this.bubble_.contains(e.target) &&
    826       !this.editButton_.contains(e.target) &&
    827       !this.arrowLeft_.contains(e.target) &&
    828       !this.arrowRight_.contains(e.target) &&
    829       !this.bubble_.hidden) {
    830     this.bubble_.hidden = true;
    831   }
    832 };
    833 
    834 /**
    835  * Keydown handler.
    836  *
    837  * @param {Event} event Event.
    838  * @return {boolean} True if handled.
    839  */
    840 SlideMode.prototype.onKeyDown = function(event) {
    841   var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
    842 
    843   if (this.isSlideshowOn_()) {
    844     switch (keyID) {
    845       case 'U+001B':  // Escape exits the slideshow.
    846         this.stopSlideshow_(event);
    847         break;
    848 
    849       case 'U+0020':  // Space pauses/resumes the slideshow.
    850         this.toggleSlideshowPause_();
    851         break;
    852 
    853       case 'Up':
    854       case 'Down':
    855       case 'Left':
    856       case 'Right':
    857         this.advanceWithKeyboard(keyID);
    858         break;
    859     }
    860     return true;  // Consume all keystrokes in the slideshow mode.
    861   }
    862 
    863   if (this.isEditing() && this.editor_.onKeyDown(event))
    864     return true;
    865 
    866   switch (keyID) {
    867     case 'U+0020':  // Space toggles the video playback.
    868       if (this.isShowingVideo_() && this.mediaControls_.getMedia())
    869         this.mediaControls_.togglePlayStateWithFeedback();
    870       break;
    871 
    872     case 'Ctrl-U+0050':  // Ctrl+'p' prints the current image.
    873       if (!this.printButton_.hasAttribute('disabled'))
    874         this.print_();
    875       break;
    876 
    877     case 'U+0045':  // 'e' toggles the editor.
    878       if (!this.editButton_.hasAttribute('disabled'))
    879         this.toggleEditor(event);
    880       break;
    881 
    882     case 'U+001B':  // Escape
    883       if (!this.isEditing())
    884         return false;  // Not handled.
    885       this.toggleEditor(event);
    886       break;
    887 
    888     case 'Home':
    889       this.selectFirst();
    890       break;
    891     case 'End':
    892       this.selectLast();
    893       break;
    894     case 'Up':
    895     case 'Down':
    896     case 'Left':
    897     case 'Right':
    898       this.advanceWithKeyboard(keyID);
    899       break;
    900 
    901     default: return false;
    902   }
    903 
    904   return true;
    905 };
    906 
    907 /**
    908  * Resize handler.
    909  * @private
    910  */
    911 SlideMode.prototype.onResize_ = function() {
    912   this.viewport_.sizeByFrameAndFit(this.container_);
    913   this.viewport_.repaint();
    914 };
    915 
    916 /**
    917  * Update thumbnails.
    918  */
    919 SlideMode.prototype.updateThumbnails = function() {
    920   this.ribbon_.reset();
    921   if (this.active_)
    922     this.ribbon_.redraw();
    923 };
    924 
    925 // Saving
    926 
    927 /**
    928  * Save the current image to a file.
    929  *
    930  * @param {function} callback Callback.
    931  * @private
    932  */
    933 SlideMode.prototype.saveCurrentImage_ = function(callback) {
    934   var item = this.getSelectedItem();
    935   var oldEntry = item.getEntry();
    936   var canvas = this.imageView_.getCanvas();
    937 
    938   this.showSpinner_(true);
    939   var metadataEncoder = ImageEncoder.encodeMetadata(
    940       this.selectedImageMetadata_.media, canvas, 1 /* quality */);
    941   var selectedImageMetadata = ContentProvider.ConvertContentMetadata(
    942       metadataEncoder.getMetadata(), this.selectedImageMetadata_);
    943   if (selectedImageMetadata.filesystem)
    944     selectedImageMetadata.filesystem.modificationTime = new Date();
    945   this.selectedImageMetadata_ = selectedImageMetadata;
    946   this.metadataCache_.set(oldEntry,
    947                           Gallery.METADATA_TYPE,
    948                           selectedImageMetadata);
    949 
    950   item.saveToFile(
    951       this.context_.saveDirEntry,
    952       this.shouldOverwriteOriginal_(),
    953       canvas,
    954       metadataEncoder,
    955       function(success) {
    956         // TODO(kaznacheev): Implement write error handling.
    957         // Until then pretend that the save succeeded.
    958         this.showSpinner_(false);
    959         this.flashSavedLabel_();
    960 
    961         var event = new Event('content');
    962         event.item = item;
    963         event.oldEntry = oldEntry;
    964         event.metadata = selectedImageMetadata;
    965         this.dataModel_.dispatchEvent(event);
    966 
    967         // Allow changing the 'Overwrite original' setting only if the user
    968         // used Undo to restore the original image AND it is not a copy.
    969         // Otherwise lock the setting in its current state.
    970         var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
    971         ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
    972 
    973         if (this.imageView_.getContentRevision() === 1) {  // First edit.
    974           ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
    975         }
    976 
    977         if (!util.isSameEntry(oldEntry, item.getEntry())) {
    978           this.dataModel_.splice(
    979               this.getSelectedIndex(), 0, new Gallery.Item(oldEntry));
    980           // The ribbon will ignore the splice above and redraw after the
    981           // select call below (while being obscured by the Editor toolbar,
    982           // so there is no need for nice animation here).
    983           // SlideMode will ignore the selection change as the displayed item
    984           // index has not changed.
    985           this.select(++this.displayedIndex_);
    986         }
    987         callback();
    988         cr.dispatchSimpleEvent(this, 'image-saved');
    989       }.bind(this));
    990 };
    991 
    992 /**
    993  * Update caches when the selected item has been renamed.
    994  * @param {Event} event Event.
    995  * @private
    996  */
    997 SlideMode.prototype.onContentChange_ = function(event) {
    998   var newEntry = event.item.getEntry();
    999   if (util.isSameEntry(newEntry, event.oldEntry))
   1000     this.imageView_.changeEntry(newEntry);
   1001   this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE);
   1002 };
   1003 
   1004 /**
   1005  * Flash 'Saved' label briefly to indicate that the image has been saved.
   1006  * @private
   1007  */
   1008 SlideMode.prototype.flashSavedLabel_ = function() {
   1009   var setLabelHighlighted =
   1010       ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
   1011   setTimeout(setLabelHighlighted.bind(null, true), 0);
   1012   setTimeout(setLabelHighlighted.bind(null, false), 300);
   1013 };
   1014 
   1015 /**
   1016  * Local storage key for the 'Overwrite original' setting.
   1017  * @type {string}
   1018  */
   1019 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
   1020 
   1021 /**
   1022  * Local storage key for the number of times that
   1023  * the overwrite info bubble has been displayed.
   1024  * @type {string}
   1025  */
   1026 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
   1027 
   1028 /**
   1029  * Max number that the overwrite info bubble is shown.
   1030  * @type {number}
   1031  */
   1032 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
   1033 
   1034 /**
   1035  * @return {boolean} True if 'Overwrite original' is set.
   1036  * @private
   1037  */
   1038 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
   1039    return this.overwriteOriginal_.checked;
   1040 };
   1041 
   1042 /**
   1043  * 'Overwrite original' checkbox handler.
   1044  * @param {Event} event Event.
   1045  * @private
   1046  */
   1047 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
   1048   util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked);
   1049 };
   1050 
   1051 /**
   1052  * Overwrite info bubble close handler.
   1053  * @private
   1054  */
   1055 SlideMode.prototype.onCloseBubble_ = function() {
   1056   this.bubble_.hidden = true;
   1057   util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
   1058       SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
   1059 };
   1060 
   1061 // Slideshow
   1062 
   1063 /**
   1064  * Slideshow interval in ms.
   1065  */
   1066 SlideMode.SLIDESHOW_INTERVAL = 5000;
   1067 
   1068 /**
   1069  * First slideshow interval in ms. It should be shorter so that the user
   1070  * is not guessing whether the button worked.
   1071  */
   1072 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
   1073 
   1074 /**
   1075  * Empirically determined duration of the fullscreen toggle animation.
   1076  */
   1077 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
   1078 
   1079 /**
   1080  * @return {boolean} True if the slideshow is on.
   1081  * @private
   1082  */
   1083 SlideMode.prototype.isSlideshowOn_ = function() {
   1084   return this.container_.hasAttribute('slideshow');
   1085 };
   1086 
   1087 /**
   1088  * Start the slideshow.
   1089  * @param {number=} opt_interval First interval in ms.
   1090  * @param {Event=} opt_event Event.
   1091  */
   1092 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
   1093   // Set the attribute early to prevent the toolbar from flashing when
   1094   // the slideshow is being started from the mosaic view.
   1095   this.container_.setAttribute('slideshow', 'playing');
   1096 
   1097   if (this.active_) {
   1098     this.stopEditing_();
   1099   } else {
   1100     // We are in the Mosaic mode. Toggle the mode but remember to return.
   1101     this.leaveAfterSlideshow_ = true;
   1102     this.toggleMode_(this.startSlideshow.bind(
   1103         this, SlideMode.SLIDESHOW_INTERVAL, opt_event));
   1104     return;
   1105   }
   1106 
   1107   if (opt_event)  // Caused by user action, notify the Gallery.
   1108     cr.dispatchSimpleEvent(this, 'useraction');
   1109 
   1110   this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
   1111   if (!this.fullscreenBeforeSlideshow_) {
   1112     // Wait until the zoom animation from the mosaic mode is done.
   1113     setTimeout(this.toggleFullScreen_.bind(this),
   1114                ImageView.ZOOM_ANIMATION_DURATION);
   1115     opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
   1116         SlideMode.FULLSCREEN_TOGGLE_DELAY;
   1117   }
   1118 
   1119   this.resumeSlideshow_(opt_interval);
   1120 };
   1121 
   1122 /**
   1123  * Stop the slideshow.
   1124  * @param {Event=} opt_event Event.
   1125  * @private
   1126  */
   1127 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
   1128   if (!this.isSlideshowOn_())
   1129     return;
   1130 
   1131   if (opt_event)  // Caused by user action, notify the Gallery.
   1132     cr.dispatchSimpleEvent(this, 'useraction');
   1133 
   1134   this.pauseSlideshow_();
   1135   this.container_.removeAttribute('slideshow');
   1136 
   1137   // Do not restore fullscreen if we exited fullscreen while in slideshow.
   1138   var fullscreen = util.isFullScreen(this.context_.appWindow);
   1139   var toggleModeDelay = 0;
   1140   if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
   1141     this.toggleFullScreen_();
   1142     toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
   1143   }
   1144   if (this.leaveAfterSlideshow_) {
   1145     this.leaveAfterSlideshow_ = false;
   1146     setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
   1147   }
   1148 };
   1149 
   1150 /**
   1151  * @return {boolean} True if the slideshow is playing (not paused).
   1152  * @private
   1153  */
   1154 SlideMode.prototype.isSlideshowPlaying_ = function() {
   1155   return this.container_.getAttribute('slideshow') === 'playing';
   1156 };
   1157 
   1158 /**
   1159  * Pause/resume the slideshow.
   1160  * @private
   1161  */
   1162 SlideMode.prototype.toggleSlideshowPause_ = function() {
   1163   cr.dispatchSimpleEvent(this, 'useraction');  // Show the tools.
   1164   if (this.isSlideshowPlaying_()) {
   1165     this.pauseSlideshow_();
   1166   } else {
   1167     this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
   1168   }
   1169 };
   1170 
   1171 /**
   1172  * @param {number=} opt_interval Slideshow interval in ms.
   1173  * @private
   1174  */
   1175 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
   1176   console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
   1177 
   1178   if (this.slideShowTimeout_)
   1179     clearTimeout(this.slideShowTimeout_);
   1180 
   1181   this.slideShowTimeout_ = setTimeout(function() {
   1182         this.slideShowTimeout_ = null;
   1183         this.selectNext(1);
   1184       }.bind(this),
   1185       opt_interval || SlideMode.SLIDESHOW_INTERVAL);
   1186 };
   1187 
   1188 /**
   1189  * Resume the slideshow.
   1190  * @param {number=} opt_interval Slideshow interval in ms.
   1191  * @private
   1192  */
   1193 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
   1194   this.container_.setAttribute('slideshow', 'playing');
   1195   this.scheduleNextSlide_(opt_interval);
   1196 };
   1197 
   1198 /**
   1199  * Pause the slideshow.
   1200  * @private
   1201  */
   1202 SlideMode.prototype.pauseSlideshow_ = function() {
   1203   this.container_.setAttribute('slideshow', 'paused');
   1204   if (this.slideShowTimeout_) {
   1205     clearTimeout(this.slideShowTimeout_);
   1206     this.slideShowTimeout_ = null;
   1207   }
   1208 };
   1209 
   1210 /**
   1211  * @return {boolean} True if the editor is active.
   1212  */
   1213 SlideMode.prototype.isEditing = function() {
   1214   return this.container_.hasAttribute('editing');
   1215 };
   1216 
   1217 /**
   1218  * Stop editing.
   1219  * @private
   1220  */
   1221 SlideMode.prototype.stopEditing_ = function() {
   1222   if (this.isEditing())
   1223     this.toggleEditor();
   1224 };
   1225 
   1226 /**
   1227  * Activate/deactivate editor.
   1228  * @param {Event=} opt_event Event.
   1229  */
   1230 SlideMode.prototype.toggleEditor = function(opt_event) {
   1231   if (opt_event)  // Caused by user action, notify the Gallery.
   1232     cr.dispatchSimpleEvent(this, 'useraction');
   1233 
   1234   if (!this.active_) {
   1235     this.toggleMode_(this.toggleEditor.bind(this));
   1236     return;
   1237   }
   1238 
   1239   this.stopSlideshow_();
   1240   if (!this.isEditing() && this.isShowingVideo_())
   1241     return;  // No editing for videos.
   1242 
   1243   ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
   1244 
   1245   if (this.isEditing()) { // isEditing has just been flipped to a new value.
   1246     if (this.context_.readonlyDirName) {
   1247       this.editor_.getPrompt().showAt(
   1248           'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
   1249     }
   1250   } else {
   1251     this.editor_.getPrompt().hide();
   1252     this.editor_.leaveModeGently();
   1253   }
   1254 };
   1255 
   1256 /**
   1257  * Prints the current item.
   1258  * @private
   1259  */
   1260 SlideMode.prototype.print_ = function() {
   1261   cr.dispatchSimpleEvent(this, 'useraction');
   1262   window.print();
   1263 };
   1264 
   1265 /**
   1266  * Display the error banner.
   1267  * @param {string} message Message.
   1268  * @private
   1269  */
   1270 SlideMode.prototype.showErrorBanner_ = function(message) {
   1271   if (message) {
   1272     this.errorBanner_.textContent = this.displayStringFunction_(message);
   1273   }
   1274   ImageUtil.setAttribute(this.container_, 'error', !!message);
   1275 };
   1276 
   1277 /**
   1278  * Show/hide the busy spinner.
   1279  *
   1280  * @param {boolean} on True if show, false if hide.
   1281  * @private
   1282  */
   1283 SlideMode.prototype.showSpinner_ = function(on) {
   1284   if (this.spinnerTimer_) {
   1285     clearTimeout(this.spinnerTimer_);
   1286     this.spinnerTimer_ = null;
   1287   }
   1288 
   1289   if (on) {
   1290     this.spinnerTimer_ = setTimeout(function() {
   1291       this.spinnerTimer_ = null;
   1292       ImageUtil.setAttribute(this.container_, 'spinner', true);
   1293     }.bind(this), 1000);
   1294   } else {
   1295     ImageUtil.setAttribute(this.container_, 'spinner', false);
   1296   }
   1297 };
   1298 
   1299 /**
   1300  * @return {boolean} True if the current item is a video.
   1301  * @private
   1302  */
   1303 SlideMode.prototype.isShowingVideo_ = function() {
   1304   return !!this.imageView_.getVideo();
   1305 };
   1306 
   1307 /**
   1308  * Overlay that handles swipe gestures. Changes to the next or previous file.
   1309  * @param {function(number)} callback A callback accepting the swipe direction
   1310  *    (1 means left, -1 right).
   1311  * @constructor
   1312  * @implements {ImageBuffer.Overlay}
   1313  */
   1314 function SwipeOverlay(callback) {
   1315   this.callback_ = callback;
   1316 }
   1317 
   1318 /**
   1319  * Inherit ImageBuffer.Overlay.
   1320  */
   1321 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype;
   1322 
   1323 /**
   1324  * @param {number} x X pointer position.
   1325  * @param {number} y Y pointer position.
   1326  * @param {boolean} touch True if dragging caused by touch.
   1327  * @return {function} The closure to call on drag.
   1328  */
   1329 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) {
   1330   if (!touch)
   1331     return null;
   1332   var origin = x;
   1333   var done = false;
   1334   return function(x, y) {
   1335     if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) {
   1336       this.callback_(1);
   1337       done = true;
   1338     } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) {
   1339       this.callback_(-1);
   1340       done = true;
   1341     }
   1342   }.bind(this);
   1343 };
   1344 
   1345 /**
   1346  * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
   1347  * horizontally it's considered as a swipe gesture (change the current image).
   1348  */
   1349 SwipeOverlay.SWIPE_THRESHOLD = 100;
   1350