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 util.addPageLoadHandler(function() {
      8   if (!location.hash)
      9     return;
     10 
     11   var pageState;
     12   if (location.search) {
     13     try {
     14       pageState = JSON.parse(decodeURIComponent(location.search.substr(1)));
     15     } catch (ignore) {}
     16   }
     17   Gallery.openStandalone(decodeURI(location.hash.substr(1)), pageState);
     18 });
     19 
     20 /**
     21  * Called from the main frame when unloading.
     22  * @return {string?} User-visible message on null if it is OK to close.
     23  */
     24 function beforeunload() { return Gallery.instance.onBeforeUnload() }
     25 
     26 /**
     27  * Called from the main frame when unloading.
     28  * @param {boolean=} opt_exiting True if the app is exiting.
     29  */
     30 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting) }
     31 
     32 /**
     33  * Gallery for viewing and editing image files.
     34  *
     35  * @param {Object} context Object containing the following:
     36  *     {function(string)} onNameChange Called every time a selected
     37  *         item name changes (on rename and on selection change).
     38  *     {AppWindow} appWindow
     39  *     {function(string)} onBack
     40  *     {function()} onClose
     41  *     {function()} onMaximize
     42  *     {MetadataCache} metadataCache
     43  *     {Array.<Object>} shareActions
     44  *     {string} readonlyDirName Directory name for readonly warning or null.
     45  *     {DirEntry} saveDirEntry Directory to save to.
     46  *     {function(string)} displayStringFunction.
     47  * @class
     48  * @constructor
     49  */
     50 function Gallery(context) {
     51   this.container_ = document.querySelector('.gallery');
     52   this.document_ = document;
     53   this.context_ = context;
     54   this.metadataCache_ = context.metadataCache;
     55   this.volumeManager_ = VolumeManager.getInstance();
     56 
     57   this.dataModel_ = new cr.ui.ArrayDataModel([]);
     58   this.selectionModel_ = new cr.ui.ListSelectionModel();
     59   this.displayStringFunction_ = context.displayStringFunction;
     60 
     61   this.initDom_();
     62   this.initListeners_();
     63 }
     64 
     65 /**
     66  * Gallery extends cr.EventTarget.
     67  */
     68 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
     69 
     70 /**
     71  * Create and initialize a Gallery object based on a context.
     72  *
     73  * @param {Object} context Gallery context.
     74  * @param {Array.<string>} urls Array of urls.
     75  * @param {Array.<string>} selectedUrls Array of selected urls.
     76  */
     77 Gallery.open = function(context, urls, selectedUrls) {
     78   Gallery.instance = new Gallery(context);
     79   Gallery.instance.load(urls, selectedUrls);
     80 };
     81 
     82 /**
     83  * Create a Gallery object in a tab.
     84  * TODO(mtomasz): Remove it after dropping support for Files.app V1.
     85  *
     86  * @param {string} path File system path to a selected file.
     87  * @param {Object} pageState Page state object.
     88  * @param {function=} opt_callback Called when gallery object is constructed.
     89  */
     90 Gallery.openStandalone = function(path, pageState, opt_callback) {
     91   ImageUtil.metrics = metrics;
     92 
     93   var currentDir;
     94   var urls = [];
     95   var selectedUrls = [];
     96   var appWindow = chrome.app.window.current();
     97 
     98   Gallery.getFileBrowserPrivate().requestFileSystem(function(filesystem) {
     99     // If the path points to the directory scan it.
    100     filesystem.root.getDirectory(path, {create: false}, scanDirectory,
    101         function() {
    102           // Try to scan the parent directory.
    103           var pathParts = path.split('/');
    104           pathParts.pop();
    105           var parentPath = pathParts.join('/');
    106           filesystem.root.getDirectory(parentPath, {create: false},
    107               scanDirectory, open /* no data, just display an error */);
    108         });
    109   });
    110 
    111   var scanDirectory = function(dirEntry) {
    112     currentDir = dirEntry;
    113     util.forEachDirEntry(currentDir, function(entry) {
    114       if (entry == null) {
    115         open();
    116       } else if (FileType.isImageOrVideo(entry)) {
    117         var url = entry.toURL();
    118         urls.push(url);
    119         if (entry.fullPath == path)
    120           selectedUrls = [url];
    121       }
    122     });
    123   };
    124 
    125   var onBack = function() {
    126     // Exiting to the Files app seems arbitrary. Consider closing the tab.
    127     document.location = 'main.html?' +
    128         JSON.stringify({defaultPath: document.location.hash.substr(1)});
    129   };
    130 
    131   var onClose = function() {
    132     window.close();
    133   };
    134 
    135   var onMaximize = function() {
    136     var appWindow = chrome.app.window.current();
    137     if (appWindow.isMaximized())
    138       appWindow.restore();
    139     else
    140       appWindow.maximize();
    141   };
    142 
    143   function open() {
    144     urls.sort();
    145     Gallery.getFileBrowserPrivate().getStrings(function(strings) {
    146       loadTimeData.data = strings;
    147       var context = {
    148         readonlyDirName: null,
    149         curDirEntry: currentDir,
    150         saveDirEntry: null,
    151         metadataCache: MetadataCache.createFull(),
    152         pageState: pageState,
    153         appWindow: appWindow,
    154         onBack: onBack,
    155         onClose: onClose,
    156         onMaximize: onMaximize,
    157         displayStringFunction: strf
    158       };
    159       Gallery.open(context, urls, selectedUrls);
    160       if (opt_callback) opt_callback();
    161     });
    162   }
    163 };
    164 
    165 /**
    166  * Tools fade-out timeout im milliseconds.
    167  * @const
    168  * @type {number}
    169  */
    170 Gallery.FADE_TIMEOUT = 3000;
    171 
    172 /**
    173  * First time tools fade-out timeout im milliseconds.
    174  * @const
    175  * @type {number}
    176  */
    177 Gallery.FIRST_FADE_TIMEOUT = 1000;
    178 
    179 /**
    180  * Time until mosaic is initialized in the background. Used to make gallery
    181  * in the slide mode load faster. In miiliseconds.
    182  * @const
    183  * @type {number}
    184  */
    185 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
    186 
    187 /**
    188  * Types of metadata Gallery uses (to query the metadata cache).
    189  * @const
    190  * @type {string}
    191  */
    192 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming';
    193 
    194 /**
    195  * Initialize listeners.
    196  * @private
    197  */
    198 Gallery.prototype.initListeners_ = function() {
    199   this.document_.oncontextmenu = function(e) { e.preventDefault(); };
    200   this.keyDownBound_ = this.onKeyDown_.bind(this);
    201   this.document_.body.addEventListener('keydown', this.keyDownBound_);
    202 
    203   this.inactivityWatcher_ = new MouseInactivityWatcher(
    204       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
    205 
    206   // Search results may contain files from different subdirectories so
    207   // the observer is not going to work.
    208   if (!this.context_.searchResults) {
    209     this.thumbnailObserverId_ = this.metadataCache_.addObserver(
    210         this.context_.curDirEntry,
    211         MetadataCache.CHILDREN,
    212         'thumbnail',
    213         this.updateThumbnails_.bind(this));
    214   }
    215 
    216   this.volumeManager_.addEventListener('externally-unmounted',
    217       this.onExternallyUnmounted_.bind(this));
    218 };
    219 
    220 /**
    221  * Closes gallery when a volume containing the selected item is unmounted.
    222  * @param {Event} event The unmount event.
    223  * @private
    224  */
    225 Gallery.prototype.onExternallyUnmounted_ = function(event) {
    226   if (!this.selectedItemFilesystemPath_)
    227     return;
    228   if (this.selectedItemFilesystemPath_.indexOf(event.mountPath) == 0)
    229     this.onBack_();
    230 };
    231 
    232 /**
    233  * Beforeunload handler.
    234  * @return {string?} User-visible message on null if it is OK to close.
    235  */
    236 Gallery.prototype.onBeforeUnload = function() {
    237   return this.slideMode_.onBeforeUnload();
    238 };
    239 
    240 /**
    241  * Unload the Gallery.
    242  * @param {boolean} exiting True if the app is exiting.
    243  */
    244 Gallery.prototype.onUnload = function(exiting) {
    245   if (!this.context_.searchResults) {
    246     this.metadataCache_.removeObserver(this.thumbnailObserverId_);
    247   }
    248   this.slideMode_.onUnload(exiting);
    249 };
    250 
    251 /**
    252  * Initializes DOM UI
    253  * @private
    254  */
    255 Gallery.prototype.initDom_ = function() {
    256   var content = util.createChild(this.container_, 'content');
    257   content.addEventListener('click', this.onContentClick_.bind(this));
    258 
    259   this.header_ = util.createChild(this.container_, 'header tool dimmable');
    260   this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable');
    261 
    262   var backButton = util.createChild(this.container_,
    263                                     'back-button tool dimmable');
    264   util.createChild(backButton);
    265   backButton.addEventListener('click', this.onBack_.bind(this));
    266 
    267   var maximizeButton = util.createChild(this.header_,
    268                                         'maximize-button tool dimmable',
    269                                         'button');
    270   maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
    271 
    272   var closeButton = util.createChild(this.header_,
    273                                      'close-button tool dimmable',
    274                                      'button');
    275   closeButton.addEventListener('click', this.onClose_.bind(this));
    276 
    277   this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer');
    278   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
    279                                         'namebox', 'input');
    280 
    281   this.filenameEdit_.setAttribute('type', 'text');
    282   this.filenameEdit_.addEventListener('blur',
    283       this.onFilenameEditBlur_.bind(this));
    284 
    285   this.filenameEdit_.addEventListener('focus',
    286       this.onFilenameFocus_.bind(this));
    287 
    288   this.filenameEdit_.addEventListener('keydown',
    289       this.onFilenameEditKeydown_.bind(this));
    290 
    291   util.createChild(this.toolbar_, 'button-spacer');
    292 
    293   this.prompt_ = new ImageEditor.Prompt(
    294       this.container_, this.displayStringFunction_);
    295 
    296   this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button');
    297   this.modeButton_.addEventListener('click',
    298       this.toggleMode_.bind(this, null));
    299 
    300   this.mosaicMode_ = new MosaicMode(content,
    301                                     this.dataModel_,
    302                                     this.selectionModel_,
    303                                     this.metadataCache_,
    304                                     this.toggleMode_.bind(this, null));
    305 
    306   this.slideMode_ = new SlideMode(this.container_,
    307                                   content,
    308                                   this.toolbar_,
    309                                   this.prompt_,
    310                                   this.dataModel_,
    311                                   this.selectionModel_,
    312                                   this.context_,
    313                                   this.toggleMode_.bind(this),
    314                                   this.displayStringFunction_);
    315 
    316   this.slideMode_.addEventListener('image-displayed', function() {
    317     cr.dispatchSimpleEvent(this, 'image-displayed');
    318   }.bind(this));
    319   this.slideMode_.addEventListener('image-saved', function() {
    320     cr.dispatchSimpleEvent(this, 'image-saved');
    321   }.bind(this));
    322 
    323   this.printButton_ = this.createToolbarButton_('print', 'GALLERY_PRINT');
    324   this.printButton_.setAttribute('disabled', '');
    325   this.printButton_.addEventListener('click', this.print_.bind(this));
    326 
    327   var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE');
    328   deleteButton.addEventListener('click', this.delete_.bind(this));
    329 
    330   this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE');
    331   this.shareButton_.setAttribute('disabled', '');
    332   this.shareButton_.addEventListener('click', this.toggleShare_.bind(this));
    333 
    334   this.shareMenu_ = util.createChild(this.container_, 'share-menu');
    335   this.shareMenu_.hidden = true;
    336   util.createChild(this.shareMenu_, 'bubble-point');
    337 
    338   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
    339   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
    340 
    341   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
    342   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
    343 };
    344 
    345 /**
    346  * Creates toolbar button.
    347  *
    348  * @param {string} className Class to add.
    349  * @param {string} title Button title.
    350  * @return {HTMLElement} Newly created button.
    351  * @private
    352  */
    353 Gallery.prototype.createToolbarButton_ = function(className, title) {
    354   var button = util.createChild(this.toolbar_, className, 'button');
    355   button.title = this.displayStringFunction_(title);
    356   return button;
    357 };
    358 
    359 /**
    360  * Load the content.
    361  *
    362  * @param {Array.<string>} urls Array of urls.
    363  * @param {Array.<string>} selectedUrls Array of selected urls.
    364  */
    365 Gallery.prototype.load = function(urls, selectedUrls) {
    366   var items = [];
    367   for (var index = 0; index < urls.length; ++index) {
    368     items.push(new Gallery.Item(urls[index]));
    369   }
    370   this.dataModel_.push.apply(this.dataModel_, items);
    371 
    372   this.selectionModel_.adjustLength(this.dataModel_.length);
    373 
    374   for (var i = 0; i != selectedUrls.length; i++) {
    375     var selectedIndex = urls.indexOf(selectedUrls[i]);
    376     if (selectedIndex >= 0)
    377       this.selectionModel_.setIndexSelected(selectedIndex, true);
    378     else
    379       console.error('Cannot select ' + selectedUrls[i]);
    380   }
    381 
    382   if (this.selectionModel_.selectedIndexes.length == 0)
    383     this.onSelection_();
    384 
    385   var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic();
    386 
    387   // Mosaic view should show up if most of the selected files are images.
    388   var imagesCount = 0;
    389   for (var i = 0; i != selectedUrls.length; i++) {
    390     if (FileType.getMediaType(selectedUrls[i]) == 'image')
    391       imagesCount++;
    392   }
    393   var mostlyImages = imagesCount > (selectedUrls.length / 2.0);
    394 
    395   var forcedMosaic = (this.context_.pageState &&
    396        this.context_.pageState.gallery == 'mosaic');
    397 
    398   var showMosaic = (mostlyImages && selectedUrls.length > 1) || forcedMosaic;
    399   if (mosaic && showMosaic) {
    400     this.setCurrentMode_(this.mosaicMode_);
    401     mosaic.init();
    402     mosaic.show();
    403     this.inactivityWatcher_.check();  // Show the toolbar.
    404     cr.dispatchSimpleEvent(this, 'loaded');
    405   } else {
    406     this.setCurrentMode_(this.slideMode_);
    407     var maybeLoadMosaic = function() {
    408       if (mosaic)
    409         mosaic.init();
    410       cr.dispatchSimpleEvent(this, 'loaded');
    411     }.bind(this);
    412     /* TODO: consider nice blow-up animation for the first image */
    413     this.slideMode_.enter(null, function() {
    414         // Flash the toolbar briefly to show it is there.
    415         this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
    416       }.bind(this),
    417       maybeLoadMosaic);
    418   }
    419 };
    420 
    421 /**
    422  * Close the Gallery and go to Files.app.
    423  * @private
    424  */
    425 Gallery.prototype.back_ = function() {
    426   if (util.isFullScreen(this.context_.appWindow)) {
    427     util.toggleFullScreen(this.context_.appWindow,
    428                           false);  // Leave the full screen mode.
    429   }
    430   this.context_.onBack(this.getSelectedUrls());
    431 };
    432 
    433 /**
    434  * Handle user's 'Back' action (Escape or a click on the X icon).
    435  * @private
    436  */
    437 Gallery.prototype.onBack_ = function() {
    438   this.executeWhenReady(this.back_.bind(this));
    439 };
    440 
    441 /**
    442  * Handle user's 'Close' action.
    443  * @private
    444  */
    445 Gallery.prototype.onClose_ = function() {
    446   this.executeWhenReady(this.context_.onClose);
    447 };
    448 
    449 /**
    450  * Handle user's 'Maximize' action (Escape or a click on the X icon).
    451  * @private
    452  */
    453 Gallery.prototype.onMaximize_ = function() {
    454   this.executeWhenReady(this.context_.onMaximize);
    455 };
    456 
    457 /**
    458  * Execute a function when the editor is done with the modifications.
    459  * @param {function} callback Function to execute.
    460  */
    461 Gallery.prototype.executeWhenReady = function(callback) {
    462   this.currentMode_.executeWhenReady(callback);
    463 };
    464 
    465 /**
    466  * @return {Object} File browser private API.
    467  */
    468 Gallery.getFileBrowserPrivate = function() {
    469   return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate;
    470 };
    471 
    472 /**
    473  * @return {boolean} True if some tool is currently active.
    474  */
    475 Gallery.prototype.hasActiveTool = function() {
    476   return this.currentMode_.hasActiveTool() ||
    477       this.isSharing_() || this.isRenaming_();
    478 };
    479 
    480 /**
    481 * External user action event handler.
    482 * @private
    483 */
    484 Gallery.prototype.onUserAction_ = function() {
    485   this.closeShareMenu_();
    486   // Show the toolbar and hide it after the default timeout.
    487   this.inactivityWatcher_.kick();
    488 };
    489 
    490 /**
    491  * Set the current mode, update the UI.
    492  * @param {Object} mode Current mode.
    493  * @private
    494  */
    495 Gallery.prototype.setCurrentMode_ = function(mode) {
    496   if (mode != this.slideMode_ && mode != this.mosaicMode_)
    497     console.error('Invalid Gallery mode');
    498 
    499   this.currentMode_ = mode;
    500   if (this.modeButton_) {
    501     var oppositeMode =
    502         mode == this.slideMode_ ? this.mosaicMode_ : this.slideMode_;
    503     this.modeButton_.title =
    504         this.displayStringFunction_(oppositeMode.getTitle());
    505   }
    506 
    507   // Printing is available only in the slide view.
    508   if (mode == this.slideMode_)
    509     this.printButton_.removeAttribute('disabled');
    510   else
    511     this.printButton_.setAttribute('disabled', '');
    512 
    513   this.container_.setAttribute('mode', this.currentMode_.getName());
    514   this.updateSelectionAndState_();
    515 };
    516 
    517 /**
    518  * Mode toggle event handler.
    519  * @param {function=} opt_callback Callback.
    520  * @param {Event=} opt_event Event that caused this call.
    521  * @private
    522  */
    523 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
    524   if (!this.modeButton_)
    525     return;
    526 
    527   if (this.changingMode_) // Do not re-enter while changing the mode.
    528     return;
    529 
    530   if (opt_event)
    531     this.onUserAction_();
    532 
    533   this.changingMode_ = true;
    534 
    535   var onModeChanged = function() {
    536     this.changingMode_ = false;
    537     if (opt_callback) opt_callback();
    538   }.bind(this);
    539 
    540   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
    541 
    542   var mosaic = this.mosaicMode_.getMosaic();
    543   var tileRect = mosaic.getTileRect(tileIndex);
    544 
    545   if (this.currentMode_ == this.slideMode_) {
    546     this.setCurrentMode_(this.mosaicMode_);
    547     mosaic.transform(
    548         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
    549     this.slideMode_.leave(tileRect,
    550         function() {
    551           // Animate back to normal position.
    552           mosaic.transform();
    553           mosaic.show();
    554           onModeChanged();
    555         }.bind(this));
    556   } else {
    557     this.setCurrentMode_(this.slideMode_);
    558     this.slideMode_.enter(tileRect,
    559         function() {
    560           // Animate to zoomed position.
    561           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
    562           mosaic.hide();
    563         }.bind(this),
    564         onModeChanged);
    565   }
    566 };
    567 
    568 /**
    569  * Deletes the selected items.
    570  * @private
    571  */
    572 Gallery.prototype.delete_ = function() {
    573   this.onUserAction_();
    574 
    575   // Clone the sorted selected indexes array.
    576   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
    577   if (!indexesToRemove.length)
    578     return;
    579 
    580   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
    581 
    582   var itemsToRemove = this.getSelectedItems();
    583   var plural = itemsToRemove.length > 1;
    584   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
    585 
    586   function deleteNext() {
    587     if (!itemsToRemove.length)
    588       return;  // All deleted.
    589 
    590     var url = itemsToRemove.pop().getUrl();
    591     webkitResolveLocalFileSystemURL(url,
    592         function(entry) {
    593           entry.remove(deleteNext,
    594               util.flog('Error deleting ' + url, deleteNext));
    595         },
    596         util.flog('Error resolving ' + url, deleteNext));
    597   }
    598 
    599   // Prevent the Gallery from handling Esc and Enter.
    600   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
    601   var restoreListener = function() {
    602     this.document_.body.addEventListener('keydown', this.keyDownBound_);
    603   }.bind(this);
    604 
    605   cr.ui.dialogs.BaseDialog.OK_LABEL = this.displayStringFunction_(
    606       'GALLERY_OK_LABEL');
    607   cr.ui.dialogs.BaseDialog.CANCEL_LABEL =
    608       this.displayStringFunction_('GALLERY_CANCEL_LABEL');
    609   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
    610   confirm.show(
    611       this.displayStringFunction_(plural ? 'GALLERY_CONFIRM_DELETE_SOME' :
    612           'GALLERY_CONFIRM_DELETE_ONE', param),
    613       function() {
    614         restoreListener();
    615         this.selectionModel_.unselectAll();
    616         this.selectionModel_.leadIndex = -1;
    617         // Remove items from the data model, starting from the highest index.
    618         while (indexesToRemove.length)
    619           this.dataModel_.splice(indexesToRemove.pop(), 1);
    620         // Delete actual files.
    621         deleteNext();
    622       }.bind(this),
    623       function() {
    624         // Restore the listener after a timeout so that ESC is processed.
    625         setTimeout(restoreListener, 0);
    626       });
    627 };
    628 
    629 /**
    630  * Prints the current item.
    631  * @private
    632  */
    633 Gallery.prototype.print_ = function() {
    634   this.onUserAction_();
    635   window.print();
    636 };
    637 
    638 /**
    639  * @return {Array.<Gallery.Item>} Current selection.
    640  */
    641 Gallery.prototype.getSelectedItems = function() {
    642   return this.selectionModel_.selectedIndexes.map(
    643       this.dataModel_.item.bind(this.dataModel_));
    644 };
    645 
    646 /**
    647  * @return {Array.<string>} Array of currently selected urls.
    648  */
    649 Gallery.prototype.getSelectedUrls = function() {
    650   return this.selectionModel_.selectedIndexes.map(function(index) {
    651     return this.dataModel_.item(index).getUrl();
    652   }.bind(this));
    653 };
    654 
    655 /**
    656  * @return {Gallery.Item} Current single selection.
    657  */
    658 Gallery.prototype.getSingleSelectedItem = function() {
    659   var items = this.getSelectedItems();
    660   if (items.length > 1)
    661     throw new Error('Unexpected multiple selection');
    662   return items[0];
    663 };
    664 
    665 /**
    666   * Selection change event handler.
    667   * @private
    668   */
    669 Gallery.prototype.onSelection_ = function() {
    670   this.updateSelectionAndState_();
    671   this.updateShareMenu_();
    672 };
    673 
    674 /**
    675   * Data model splice event handler.
    676   * @private
    677   */
    678 Gallery.prototype.onSplice_ = function() {
    679   this.selectionModel_.adjustLength(this.dataModel_.length);
    680 };
    681 
    682 /**
    683  * Content change event handler.
    684  * @param {Event} event Event.
    685  * @private
    686 */
    687 Gallery.prototype.onContentChange_ = function(event) {
    688   var index = this.dataModel_.indexOf(event.item);
    689   if (index != this.selectionModel_.selectedIndex)
    690     console.error('Content changed for unselected item');
    691   this.updateSelectionAndState_();
    692 };
    693 
    694 /**
    695  * Keydown handler.
    696  *
    697  * @param {Event} event Event.
    698  * @private
    699  */
    700 Gallery.prototype.onKeyDown_ = function(event) {
    701   var wasSharing = this.isSharing_();
    702   this.closeShareMenu_();
    703 
    704   if (this.currentMode_.onKeyDown(event))
    705     return;
    706 
    707   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
    708     case 'U+0008': // Backspace.
    709       // The default handler would call history.back and close the Gallery.
    710       event.preventDefault();
    711       break;
    712 
    713     case 'U+001B':  // Escape
    714       // Swallow Esc if it closed the Share menu, otherwise close the Gallery.
    715       if (!wasSharing)
    716         this.onBack_();
    717       break;
    718 
    719     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
    720       this.toggleMode_(null, event);
    721       break;
    722 
    723     case 'U+0056':  // 'v'
    724       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
    725       break;
    726 
    727     case 'Ctrl-U+0050':  // Ctrl+'p' prints the current image.
    728       if (this.currentMode_ == this.slideMode_)
    729         this.print_();
    730       break;
    731 
    732     case 'U+007F':  // Delete
    733     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
    734       this.delete_();
    735       break;
    736   }
    737 };
    738 
    739 // Name box and rename support.
    740 
    741 /**
    742  * Update the UI related to the selected item and the persistent state.
    743  *
    744  * @private
    745  */
    746 Gallery.prototype.updateSelectionAndState_ = function() {
    747   var path;
    748   var displayName = '';
    749 
    750   var selectedItems = this.getSelectedItems();
    751   if (selectedItems.length == 1) {
    752     var item = selectedItems[0];
    753     path = util.extractFilePath(item.getUrl());
    754     var fullName = item.getFileName();
    755     window.top.document.title = fullName;
    756     displayName = ImageUtil.getFileNameFromFullName(fullName);
    757   } else if (selectedItems.length > 1) {
    758     // If the Gallery was opened on search results the search query will not be
    759     // recorded in the app state and the relaunch will just open the gallery
    760     // in the curDirEntry directory.
    761     path = this.context_.curDirEntry.fullPath;
    762     window.top.document.title = this.context_.curDirEntry.name;
    763     displayName =
    764         this.displayStringFunction_('GALLERY_ITEMS_SELECTED',
    765                                     selectedItems.length);
    766   }
    767 
    768   window.top.util.updateAppState(path,
    769       {gallery: (this.currentMode_ == this.mosaicMode_ ? 'mosaic' : 'slide')});
    770 
    771   // We can't rename files in readonly directory.
    772   // We can only rename a single file.
    773   this.filenameEdit_.disabled = selectedItems.length != 1 ||
    774                                 this.context_.readonlyDirName;
    775 
    776   this.filenameEdit_.value = displayName;
    777 
    778   // Resolve real filesystem path of the current file.
    779   if (this.selectionModel_.selectedIndexes.length) {
    780     var selectedIndex = this.selectionModel_.selectedIndex;
    781     var selectedItem =
    782         this.dataModel_.item(this.selectionModel_.selectedIndex);
    783 
    784     this.selectedItemFilesystemPath_ = null;
    785     webkitResolveLocalFileSystemURL(selectedItem.getUrl(),
    786       function(entry) {
    787         if (this.selectionModel_.selectedIndex != selectedIndex)
    788           return;
    789         this.selectedItemFilesystemPath_ = entry.fullPath;
    790       }.bind(this));
    791   }
    792 };
    793 
    794 /**
    795  * Click event handler on filename edit box
    796  * @private
    797  */
    798 Gallery.prototype.onFilenameFocus_ = function() {
    799   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
    800   this.filenameEdit_.originalValue = this.filenameEdit_.value;
    801   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
    802   this.onUserAction_();
    803 };
    804 
    805 /**
    806  * Blur event handler on filename edit box.
    807  *
    808  * @param {Event} event Blur event.
    809  * @return {boolean} if default action should be prevented.
    810  * @private
    811  */
    812 Gallery.prototype.onFilenameEditBlur_ = function(event) {
    813   if (this.filenameEdit_.value && this.filenameEdit_.value[0] == '.') {
    814     this.prompt_.show('file_hidden_name', 5000);
    815     this.filenameEdit_.focus();
    816     event.stopPropagation();
    817     event.preventDefault();
    818     return false;
    819   }
    820 
    821   var item = this.getSingleSelectedItem();
    822   var oldUrl = item.getUrl();
    823 
    824   var onFileExists = function() {
    825     this.prompt_.show('file_exists', 3000);
    826     this.filenameEdit_.value = name;
    827     this.filenameEdit_.focus();
    828   }.bind(this);
    829 
    830   var onSuccess = function() {
    831     var e = new cr.Event('content');
    832     e.item = item;
    833     e.oldUrl = oldUrl;
    834     e.metadata = null;  // Metadata unchanged.
    835     this.dataModel_.dispatchEvent(e);
    836   }.bind(this);
    837 
    838   if (this.filenameEdit_.value) {
    839     this.getSingleSelectedItem().rename(
    840         this.filenameEdit_.value, onSuccess, onFileExists);
    841   }
    842 
    843   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
    844   this.onUserAction_();
    845 };
    846 
    847 /**
    848  * Keydown event handler on filename edit box
    849  * @private
    850  */
    851 Gallery.prototype.onFilenameEditKeydown_ = function() {
    852   switch (event.keyCode) {
    853     case 27:  // Escape
    854       this.filenameEdit_.value = this.filenameEdit_.originalValue;
    855       this.filenameEdit_.blur();
    856       break;
    857 
    858     case 13:  // Enter
    859       this.filenameEdit_.blur();
    860       break;
    861   }
    862   event.stopPropagation();
    863 };
    864 
    865 /**
    866  * @return {boolean} True if file renaming is currently in progress.
    867  * @private
    868  */
    869 Gallery.prototype.isRenaming_ = function() {
    870   return this.filenameSpacer_.hasAttribute('renaming');
    871 };
    872 
    873 /**
    874  * Content area click handler.
    875  * @private
    876  */
    877 Gallery.prototype.onContentClick_ = function() {
    878   this.closeShareMenu_();
    879   this.filenameEdit_.blur();
    880 };
    881 
    882 // Share button support.
    883 
    884 /**
    885  * @return {boolean} True if the Share menu is active.
    886  * @private
    887  */
    888 Gallery.prototype.isSharing_ = function() {
    889   return !this.shareMenu_.hidden;
    890 };
    891 
    892 /**
    893  * Close Share menu if it is open.
    894  * @private
    895  */
    896 Gallery.prototype.closeShareMenu_ = function() {
    897   if (this.isSharing_())
    898     this.toggleShare_();
    899 };
    900 
    901 /**
    902  * Share button handler.
    903  * @private
    904  */
    905 Gallery.prototype.toggleShare_ = function() {
    906   if (!this.shareButton_.hasAttribute('disabled'))
    907     this.shareMenu_.hidden = !this.shareMenu_.hidden;
    908   this.inactivityWatcher_.check();
    909 };
    910 
    911 /**
    912  * Update available actions list based on the currently selected urls.
    913  * @private.
    914  */
    915 Gallery.prototype.updateShareMenu_ = function() {
    916   var urls = this.getSelectedUrls();
    917 
    918   function isShareAction(task) {
    919     var taskParts = task.taskId.split('|');
    920     return taskParts[0] != chrome.runtime.id;
    921   }
    922 
    923   var api = Gallery.getFileBrowserPrivate();
    924   var mimeTypes = [];  // TODO(kaznacheev) Collect mime types properly.
    925 
    926   var createShareMenu = function(tasks) {
    927     var wasHidden = this.shareMenu_.hidden;
    928     this.shareMenu_.hidden = true;
    929     var items = this.shareMenu_.querySelectorAll('.item');
    930     for (var i = 0; i != items.length; i++) {
    931       items[i].parentNode.removeChild(items[i]);
    932     }
    933 
    934     for (var t = 0; t != tasks.length; t++) {
    935       var task = tasks[t];
    936       if (!isShareAction(task)) continue;
    937 
    938       var item = util.createChild(this.shareMenu_, 'item');
    939       item.textContent = task.title;
    940       item.style.backgroundImage = 'url(' + task.iconUrl + ')';
    941       item.addEventListener('click', function(taskId) {
    942         this.toggleShare_();  // Hide the menu.
    943         this.executeWhenReady(api.executeTask.bind(api, taskId, urls));
    944       }.bind(this, task.taskId));
    945     }
    946 
    947     var empty = this.shareMenu_.querySelector('.item') == null;
    948     ImageUtil.setAttribute(this.shareButton_, 'disabled', empty);
    949     this.shareMenu_.hidden = wasHidden || empty;
    950   }.bind(this);
    951 
    952   // Create or update the share menu with a list of sharing tasks and show
    953   // or hide the share button.
    954   if (!urls.length)
    955     createShareMenu([]);  // Empty list of tasks, since there is no selection.
    956   else
    957     api.getFileTasks(urls, mimeTypes, createShareMenu);
    958 };
    959 
    960 /**
    961  * Update thumbnails.
    962  * @private
    963  */
    964 Gallery.prototype.updateThumbnails_ = function() {
    965   if (this.currentMode_ == this.slideMode_)
    966     this.slideMode_.updateThumbnails();
    967 
    968   if (this.mosaicMode_) {
    969     var mosaic = this.mosaicMode_.getMosaic();
    970     if (mosaic.isInitialized())
    971       mosaic.reload();
    972   }
    973 };
    974