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