Gallery.app: Decrease toolbar hiding timeout seconds.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery.js
blob04e539f595e9376f4a799750bad230ddb549b4b9
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 'use strict';
7 /**
8  * Called from the main frame when unloading.
9  * @param {boolean=} opt_exiting True if the app is exiting.
10  */
11 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting); }
13 /**
14  * Overrided metadata worker's path.
15  * @type {string}
16  * @const
17  */
18 ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
20 /**
21  * Data model for gallery.
22  *
23  * @param {MetadataCache} metadataCache Metadata cache.
24  * @constructor
25  * @extends {cr.ui.ArrayDataModel}
26  */
27 function GalleryDataModel(metadataCache) {
28   cr.ui.ArrayDataModel.call(this, []);
30   /**
31    * Metadata cache.
32    * @type {MetadataCache}
33    * @private
34    */
35   this.metadataCache_ = metadataCache;
37   /**
38    * Directory where the image is saved if the image is located in a read-only
39    * volume.
40    * @type {DirectoryEntry}
41    */
42   this.fallbackSaveDirectory = null;
45 /**
46  * Maximum number of full size image cache.
47  * @type {number}
48  * @const
49  * @private
50  */
51 GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3;
53 /**
54  * Maximum number of screen size image cache.
55  * @type {number}
56  * @const
57  * @private
58  */
59 GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5;
61 GalleryDataModel.prototype = {
62   __proto__: cr.ui.ArrayDataModel.prototype
65 /**
66  * Saves new image.
67  *
68  * @param {VolumeManager} volumeManager Volume manager instance.
69  * @param {Gallery.Item} item Original gallery item.
70  * @param {HTMLCanvasElement} canvas Canvas containing new image.
71  * @param {boolean} overwrite Whether to overwrite the image to the item or not.
72  * @return {Promise} Promise to be fulfilled with when the operation completes.
73  */
74 GalleryDataModel.prototype.saveItem = function(
75     volumeManager, item, canvas, overwrite) {
76   var oldEntry = item.getEntry();
77   var oldMetadata = item.getMetadata();
78   var oldLocationInfo = item.getLocationInfo();
79   var metadataEncoder = ImageEncoder.encodeMetadata(
80       item.getMetadata(), canvas, 1 /* quality */);
81   var newMetadata = ContentProvider.ConvertContentMetadata(
82       metadataEncoder.getMetadata(),
83       MetadataCache.cloneMetadata(item.getMetadata()));
84   if (newMetadata.filesystem)
85     newMetadata.filesystem.modificationTime = new Date();
86   if (newMetadata.external)
87     newMetadata.external.present = true;
89   return new Promise(function(fulfill, reject) {
90     item.saveToFile(
91         volumeManager,
92         this.fallbackSaveDirectory,
93         overwrite,
94         canvas,
95         metadataEncoder,
96         function(success) {
97           if (!success) {
98             reject('Failed to save the image.');
99             return;
100           }
102           // The item's entry is updated to the latest entry. Update metadata.
103           item.setMetadata(newMetadata);
105           // Current entry is updated.
106           // Dispatch an event.
107           var event = new Event('content');
108           event.item = item;
109           event.oldEntry = oldEntry;
110           event.metadata = newMetadata;
111           this.dispatchEvent(event);
113           if (util.isSameEntry(oldEntry, item.getEntry())) {
114             // Need an update of metdataCache.
115             this.metadataCache_.set(
116                 item.getEntry(),
117                 Gallery.METADATA_TYPE,
118                 newMetadata);
119           } else {
120             // New entry is added and the item now tracks it.
121             // Add another item for the old entry.
122             var anotherItem = new Gallery.Item(
123                 oldEntry,
124                 oldLocationInfo,
125                 oldMetadata,
126                 this.metadataCache_,
127                 item.isOriginal());
128             // The item must be added behind the existing item so that it does
129             // not change the index of the existing item.
130             // TODO(hirono): Update the item index of the selection model
131             // correctly.
132             this.splice(this.indexOf(item) + 1, 0, anotherItem);
133           }
135           fulfill();
136         }.bind(this));
137   }.bind(this));
141  * Evicts image caches in the items.
142  * @param {Gallery.Item} currentSelectedItem Current selected item.
143  */
144 GalleryDataModel.prototype.evictCache = function(currentSelectedItem) {
145   // Sort the item by the last accessed date.
146   var sorted = this.slice().sort(function(a, b) {
147     return b.getLastAccessedDate() - a.getLastAccessedDate();
148   });
150   // Evict caches.
151   var contentCacheCount = 0;
152   var screenCacheCount = 0;
153   for (var i = 0; i < sorted.length; i++) {
154     if (sorted[i].contentImage) {
155       if (++contentCacheCount > GalleryDataModel.MAX_FULL_IMAGE_CACHE_) {
156         if (sorted[i].contentImage.parentNode) {
157           console.error('The content image has a parent node.');
158         } else {
159           // Force to free the buffer of the canvas by assigning zero size.
160           sorted[i].contentImage.width = 0;
161           sorted[i].contentImage.height = 0;
162           sorted[i].contentImage = null;
163         }
164       }
165     }
166     if (sorted[i].screenImage) {
167       if (++screenCacheCount > GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_) {
168         if (sorted[i].screenImage.parentNode) {
169           console.error('The screen image has a parent node.');
170         } else {
171           // Force to free the buffer of the canvas by assigning zero size.
172           sorted[i].screenImage.width = 0;
173           sorted[i].screenImage.height = 0;
174           sorted[i].screenImage = null;
175         }
176       }
177     }
178   }
182  * Gallery for viewing and editing image files.
184  * @param {!VolumeManager} volumeManager The VolumeManager instance of the
185  *     system.
186  * @constructor
187  */
188 function Gallery(volumeManager) {
189   this.context_ = {
190     appWindow: chrome.app.window.current(),
191     onClose: function() { close(); },
192     onMaximize: function() {
193       var appWindow = chrome.app.window.current();
194       if (appWindow.isMaximized())
195         appWindow.restore();
196       else
197         appWindow.maximize();
198     },
199     onMinimize: function() { chrome.app.window.current().minimize(); },
200     onAppRegionChanged: function() {},
201     metadataCache: MetadataCache.createFull(volumeManager),
202     readonlyDirName: '',
203     displayStringFunction: function() { return ''; },
204     loadTimeData: {}
205   };
206   this.container_ = document.querySelector('.gallery');
207   this.document_ = document;
208   this.metadataCache_ = this.context_.metadataCache;
209   this.volumeManager_ = volumeManager;
210   this.selectedEntry_ = null;
211   this.metadataCacheObserverId_ = null;
212   this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
214   this.dataModel_ = new GalleryDataModel(
215       this.context_.metadataCache);
216   var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
217       VolumeManagerCommon.VolumeType.DOWNLOADS);
218   downloadVolumeInfo.resolveDisplayRoot().then(function(entry) {
219     this.dataModel_.fallbackSaveDirectory = entry;
220   }.bind(this)).catch(function(error) {
221     console.error(
222         'Failed to obtain the fallback directory: ' + (error.stack || error));
223   });
224   this.selectionModel_ = new cr.ui.ListSelectionModel();
226   this.initDom_();
227   this.initListeners_();
231  * Gallery extends cr.EventTarget.
232  */
233 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
236  * Tools fade-out timeout in milliseconds.
237  * @const
238  * @type {number}
239  */
240 Gallery.FADE_TIMEOUT = 2000;
243  * First time tools fade-out timeout in milliseconds.
244  * @const
245  * @type {number}
246  */
247 Gallery.FIRST_FADE_TIMEOUT = 1000;
250  * Time until mosaic is initialized in the background. Used to make gallery
251  * in the slide mode load faster. In milliseconds.
252  * @const
253  * @type {number}
254  */
255 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
258  * Types of metadata Gallery uses (to query the metadata cache).
259  * @const
260  * @type {string}
261  */
262 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|external';
265  * Initializes listeners.
266  * @private
267  */
268 Gallery.prototype.initListeners_ = function() {
269   this.keyDownBound_ = this.onKeyDown_.bind(this);
270   this.document_.body.addEventListener('keydown', this.keyDownBound_);
272   this.inactivityWatcher_ = new MouseInactivityWatcher(
273       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
275   // Search results may contain files from different subdirectories so
276   // the observer is not going to work.
277   if (!this.context_.searchResults && this.context_.curDirEntry) {
278     this.metadataCacheObserverId_ = this.metadataCache_.addObserver(
279         this.context_.curDirEntry,
280         MetadataCache.CHILDREN,
281         'thumbnail',
282         this.updateThumbnails_.bind(this));
283   }
284   this.volumeManager_.addEventListener(
285       'externally-unmounted', this.onExternallyUnmountedBound_);
289  * Closes gallery when a volume containing the selected item is unmounted.
290  * @param {!Event} event The unmount event.
291  * @private
292  */
293 Gallery.prototype.onExternallyUnmounted_ = function(event) {
294   if (!this.selectedEntry_)
295     return;
297   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
298       event.volumeInfo) {
299     close();
300   }
304  * Unloads the Gallery.
305  * @param {boolean} exiting True if the app is exiting.
306  */
307 Gallery.prototype.onUnload = function(exiting) {
308   if (this.metadataCacheObserverId_ !== null)
309     this.metadataCache_.removeObserver(this.metadataCacheObserverId_);
310   this.volumeManager_.removeEventListener(
311       'externally-unmounted', this.onExternallyUnmountedBound_);
312   this.slideMode_.onUnload(exiting);
316  * Initializes DOM UI
317  * @private
318  */
319 Gallery.prototype.initDom_ = function() {
320   // Initialize the dialog label.
321   cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
322   cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
324   var content = document.querySelector('#content');
325   content.addEventListener('click', this.onContentClick_.bind(this));
327   this.header_ = document.querySelector('#header');
328   this.toolbar_ = document.querySelector('#toolbar');
330   var preventDefault = function(event) { event.preventDefault(); };
332   var minimizeButton = util.createChild(this.header_,
333                                         'minimize-button tool dimmable',
334                                         'button');
335   minimizeButton.tabIndex = -1;
336   minimizeButton.addEventListener('click', this.onMinimize_.bind(this));
337   minimizeButton.addEventListener('mousedown', preventDefault);
339   var maximizeButton = util.createChild(this.header_,
340                                         'maximize-button tool dimmable',
341                                         'button');
342   maximizeButton.tabIndex = -1;
343   maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
344   maximizeButton.addEventListener('mousedown', preventDefault);
346   var closeButton = util.createChild(this.header_,
347                                      'close-button tool dimmable',
348                                      'button');
349   closeButton.tabIndex = -1;
350   closeButton.addEventListener('click', this.onClose_.bind(this));
351   closeButton.addEventListener('mousedown', preventDefault);
353   this.filenameSpacer_ = this.toolbar_.querySelector('.filename-spacer');
354   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
355                                         'namebox', 'input');
357   this.filenameEdit_.setAttribute('type', 'text');
358   this.filenameEdit_.addEventListener('blur',
359       this.onFilenameEditBlur_.bind(this));
361   this.filenameEdit_.addEventListener('focus',
362       this.onFilenameFocus_.bind(this));
364   this.filenameEdit_.addEventListener('keydown',
365       this.onFilenameEditKeydown_.bind(this));
367   var middleSpacer = this.filenameSpacer_ =
368       this.toolbar_.querySelector('.middle-spacer');
369   var buttonSpacer = this.toolbar_.querySelector('button-spacer');
371   this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
373   this.errorBanner_ = new ErrorBanner(this.container_);
375   this.modeButton_ = this.toolbar_.querySelector('button.mode');
376   this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null));
378   this.mosaicMode_ = new MosaicMode(content,
379                                     this.errorBanner_,
380                                     this.dataModel_,
381                                     this.selectionModel_,
382                                     this.volumeManager_,
383                                     this.toggleMode_.bind(this, null));
385   this.slideMode_ = new SlideMode(this.container_,
386                                   content,
387                                   this.toolbar_,
388                                   this.prompt_,
389                                   this.errorBanner_,
390                                   this.dataModel_,
391                                   this.selectionModel_,
392                                   this.context_,
393                                   this.volumeManager_,
394                                   this.toggleMode_.bind(this),
395                                   str);
397   this.slideMode_.addEventListener('image-displayed', function() {
398     cr.dispatchSimpleEvent(this, 'image-displayed');
399   }.bind(this));
400   this.slideMode_.addEventListener('image-saved', function() {
401     cr.dispatchSimpleEvent(this, 'image-saved');
402   }.bind(this));
404   this.deleteButton_ = this.initToolbarButton_('delete', 'GALLERY_DELETE');
405   this.deleteButton_.addEventListener('click', this.delete_.bind(this));
407   this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
408   this.shareButton_.addEventListener(
409       'click', this.onShareButtonClick_.bind(this));
411   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
412   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
414   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
415   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
417   this.shareDialog_ = new ShareDialog(this.container_);
421  * Initializes a toolbar button.
423  * @param {string} className Class to add.
424  * @param {string} title Button title.
425  * @return {!HTMLElement} Newly created button.
426  * @private
427  */
428 Gallery.prototype.initToolbarButton_ = function(className, title) {
429   var button = this.toolbar_.querySelector('button.' + className);
430   button.title = str(title);
431   return button;
435  * Loads the content.
437  * @param {!Array.<Entry>} entries Array of entries.
438  * @param {!Array.<Entry>} selectedEntries Array of selected entries.
439  */
440 Gallery.prototype.load = function(entries, selectedEntries) {
441   // Obtains max chank size.
442   var maxChunkSize = 20;
443   var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
444   if (volumeInfo &&
445       volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) {
446     maxChunkSize = 1;
447   }
448   if (volumeInfo.isReadOnly)
449     this.context_.readonlyDirName = volumeInfo.label;
451   // Make loading list.
452   var entrySet = {};
453   for (var i = 0; i < entries.length; i++) {
454     var entry = entries[i];
455     entrySet[entry.toURL()] = {
456       entry: entry,
457       selected: false,
458       index: i
459     };
460   }
461   for (var i = 0; i < selectedEntries.length; i++) {
462     var entry = selectedEntries[i];
463     entrySet[entry.toURL()] = {
464       entry: entry,
465       selected: true,
466       index: i
467     };
468   }
469   var loadingList = [];
470   for (var url in entrySet) {
471     loadingList.push(entrySet[url]);
472   }
473   loadingList = loadingList.sort(function(a, b) {
474     if (a.selected && !b.selected)
475       return -1;
476     else if (!a.selected && b.selected)
477       return 1;
478     else
479       return a.index - b.index;
480   });
482   // Load entries.
483   // Use the self variable capture-by-closure because it is faster than bind.
484   var self = this;
485   var loadChunk = function(firstChunk) {
486     // Extract chunk.
487     var chunk = loadingList.splice(0, maxChunkSize);
488     if (!chunk.length)
489       return;
491     return new Promise(function(fulfill) {
492       // Obtains metadata for chunk.
493       var entries = chunk.map(function(chunkItem) {
494         return chunkItem.entry;
495       });
496       self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill);
497     }).then(function(metadataList) {
498       if (chunk.length !== metadataList.length)
499         return Promise.reject('Failed to load metadata.');
501       // Add items to the model.
502       var items = [];
503       chunk.forEach(function(chunkItem, index) {
504         var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
505         if (!locationInfo)  // Skip the item, since gone.
506           return;
507         var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]);
508         items.push(new Gallery.Item(
509             chunkItem.entry,
510             locationInfo,
511             clonedMetadata,
512             self.metadataCache_,
513             /* original */ true));
514       });
515       self.dataModel_.push.apply(self.dataModel_, items);
517       // Apply the selection.
518       var selectionUpdated = false;
519       for (var i = 0; i < chunk.length; i++) {
520         if (!chunk[i].selected)
521           continue;
522         var index = self.dataModel_.indexOf(items[i]);
523         if (index < 0)
524           continue;
525         self.selectionModel_.setIndexSelected(index, true);
526         selectionUpdated = true;
527       }
528       if (selectionUpdated)
529         self.onSelection_();
531       // Init modes after the first chunk is loaded.
532       if (firstChunk) {
533         // Determine the initial mode.
534         var shouldShowMosaic = selectedEntries.length > 1 ||
535             (self.context_.pageState &&
536              self.context_.pageState.gallery === 'mosaic');
537         self.setCurrentMode_(
538             shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
540         // Init mosaic mode.
541         var mosaic = self.mosaicMode_.getMosaic();
542         mosaic.init();
544         // Do the initialization for each mode.
545         if (shouldShowMosaic) {
546           mosaic.show();
547           self.inactivityWatcher_.check();  // Show the toolbar.
548           cr.dispatchSimpleEvent(self, 'loaded');
549         } else {
550           self.slideMode_.enter(
551               null,
552               function() {
553                 // Flash the toolbar briefly to show it is there.
554                 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
555               },
556               function() {
557                 cr.dispatchSimpleEvent(self, 'loaded');
558               });
559         }
560       }
562       // Continue to load chunks.
563       return loadChunk(/* firstChunk */ false);
564     });
565   };
566   loadChunk(/* firstChunk */ true).catch(function(error) {
567     console.error(error.stack || error);
568   });
572  * Handles user's 'Close' action.
573  * @private
574  */
575 Gallery.prototype.onClose_ = function() {
576   this.executeWhenReady(this.context_.onClose);
580  * Handles user's 'Maximize' action (Escape or a click on the X icon).
581  * @private
582  */
583 Gallery.prototype.onMaximize_ = function() {
584   this.executeWhenReady(this.context_.onMaximize);
588  * Handles user's 'Maximize' action (Escape or a click on the X icon).
589  * @private
590  */
591 Gallery.prototype.onMinimize_ = function() {
592   this.executeWhenReady(this.context_.onMinimize);
596  * Executes a function when the editor is done with the modifications.
597  * @param {function()} callback Function to execute.
598  */
599 Gallery.prototype.executeWhenReady = function(callback) {
600   this.currentMode_.executeWhenReady(callback);
604  * @return {Object} File manager private API.
605  */
606 Gallery.getFileManagerPrivate = function() {
607   return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
611  * @return {boolean} True if some tool is currently active.
612  */
613 Gallery.prototype.hasActiveTool = function() {
614   return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
615       this.isRenaming_();
619 * External user action event handler.
620 * @private
622 Gallery.prototype.onUserAction_ = function() {
623   // Show the toolbar and hide it after the default timeout.
624   this.inactivityWatcher_.kick();
628  * Sets the current mode, update the UI.
629  * @param {Object} mode Current mode.
630  * @private
631  */
632 Gallery.prototype.setCurrentMode_ = function(mode) {
633   if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
634     console.error('Invalid Gallery mode');
636   this.currentMode_ = mode;
637   this.container_.setAttribute('mode', this.currentMode_.getName());
638   this.updateSelectionAndState_();
639   this.updateButtons_();
643  * Mode toggle event handler.
644  * @param {function()=} opt_callback Callback.
645  * @param {Event=} opt_event Event that caused this call.
646  * @private
647  */
648 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
649   if (!this.modeButton_)
650     return;
652   if (this.changingMode_) // Do not re-enter while changing the mode.
653     return;
655   if (opt_event)
656     this.onUserAction_();
658   this.changingMode_ = true;
660   var onModeChanged = function() {
661     this.changingMode_ = false;
662     if (opt_callback) opt_callback();
663   }.bind(this);
665   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
667   var mosaic = this.mosaicMode_.getMosaic();
668   var tileRect = mosaic.getTileRect(tileIndex);
670   if (this.currentMode_ === this.slideMode_) {
671     this.setCurrentMode_(this.mosaicMode_);
672     mosaic.transform(
673         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
674     this.slideMode_.leave(
675         tileRect,
676         function() {
677           // Animate back to normal position.
678           mosaic.transform();
679           mosaic.show();
680           onModeChanged();
681         }.bind(this));
682   } else {
683     this.setCurrentMode_(this.slideMode_);
684     this.slideMode_.enter(
685         tileRect,
686         function() {
687           // Animate to zoomed position.
688           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
689           mosaic.hide();
690         }.bind(this),
691         onModeChanged);
692   }
696  * Deletes the selected items.
697  * @private
698  */
699 Gallery.prototype.delete_ = function() {
700   this.onUserAction_();
702   // Clone the sorted selected indexes array.
703   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
704   if (!indexesToRemove.length)
705     return;
707   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
709   var itemsToRemove = this.getSelectedItems();
710   var plural = itemsToRemove.length > 1;
711   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
713   function deleteNext() {
714     if (!itemsToRemove.length)
715       return;  // All deleted.
717     var entry = itemsToRemove.pop().getEntry();
718     entry.remove(deleteNext, function() {
719       console.error('Error deleting: ' + entry.name);
720       deleteNext();
721     });
722   }
724   // Prevent the Gallery from handling Esc and Enter.
725   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
726   var restoreListener = function() {
727     this.document_.body.addEventListener('keydown', this.keyDownBound_);
728   }.bind(this);
731   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
732   confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
733   confirm.show(strf(plural ?
734       'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
735       function() {
736         restoreListener();
737         this.selectionModel_.unselectAll();
738         this.selectionModel_.leadIndex = -1;
739         // Remove items from the data model, starting from the highest index.
740         while (indexesToRemove.length)
741           this.dataModel_.splice(indexesToRemove.pop(), 1);
742         // Delete actual files.
743         deleteNext();
744       }.bind(this),
745       function() {
746         // Restore the listener after a timeout so that ESC is processed.
747         setTimeout(restoreListener, 0);
748       });
752  * @return {Array.<Gallery.Item>} Current selection.
753  */
754 Gallery.prototype.getSelectedItems = function() {
755   return this.selectionModel_.selectedIndexes.map(
756       this.dataModel_.item.bind(this.dataModel_));
760  * @return {Array.<Entry>} Array of currently selected entries.
761  */
762 Gallery.prototype.getSelectedEntries = function() {
763   return this.selectionModel_.selectedIndexes.map(function(index) {
764     return this.dataModel_.item(index).getEntry();
765   }.bind(this));
769  * @return {?Gallery.Item} Current single selection.
770  */
771 Gallery.prototype.getSingleSelectedItem = function() {
772   var items = this.getSelectedItems();
773   if (items.length > 1) {
774     console.error('Unexpected multiple selection');
775     return null;
776   }
777   return items[0];
781   * Selection change event handler.
782   * @private
783   */
784 Gallery.prototype.onSelection_ = function() {
785   this.updateSelectionAndState_();
789   * Data model splice event handler.
790   * @private
791   */
792 Gallery.prototype.onSplice_ = function() {
793   this.selectionModel_.adjustLength(this.dataModel_.length);
797  * Content change event handler.
798  * @param {Event} event Event.
799  * @private
801 Gallery.prototype.onContentChange_ = function(event) {
802   var index = this.dataModel_.indexOf(event.item);
803   if (index !== this.selectionModel_.selectedIndex)
804     console.error('Content changed for unselected item');
805   this.updateSelectionAndState_();
809  * Keydown handler.
811  * @param {Event} event Event.
812  * @private
813  */
814 Gallery.prototype.onKeyDown_ = function(event) {
815   if (this.currentMode_.onKeyDown(event))
816     return;
818   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
819     case 'U+0008': // Backspace.
820       // The default handler would call history.back and close the Gallery.
821       event.preventDefault();
822       break;
824     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
825       this.toggleMode_(null, event);
826       break;
828     case 'U+0056':  // 'v'
829     case 'MediaPlayPause':
830       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
831       break;
833     case 'U+007F':  // Delete
834     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
835     case 'U+0044':  // 'd'
836       this.delete_();
837       break;
838   }
841 // Name box and rename support.
844  * Updates the UI related to the selected item and the persistent state.
846  * @private
847  */
848 Gallery.prototype.updateSelectionAndState_ = function() {
849   var numSelectedItems = this.selectionModel_.selectedIndexes.length;
850   var selectedEntryURL = null;
852   // If it's selecting something, update the variable values.
853   if (numSelectedItems) {
854     // Delete button is available when all images are NOT readOnly.
855     this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes
856         .every(function(i) {
857           return !this.dataModel_.item(i).getLocationInfo().isReadOnly;
858         }, this);
860     // Obtains selected item.
861     var selectedItem =
862         this.dataModel_.item(this.selectionModel_.selectedIndex);
863     this.selectedEntry_ = selectedItem.getEntry();
864     selectedEntryURL = this.selectedEntry_.toURL();
866     // Update cache.
867     selectedItem.touch();
868     this.dataModel_.evictCache();
870     // Update the title and the display name.
871     if (numSelectedItems === 1) {
872       document.title = this.selectedEntry_.name;
873       this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
874       this.filenameEdit_.value =
875           ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
876       this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
877     } else {
878       if (this.context_.curDirEntry) {
879         // If the Gallery was opened on search results the search query will not
880         // be recorded in the app state and the relaunch will just open the
881         // gallery in the curDirEntry directory.
882         document.title = this.context_.curDirEntry.name;
883       } else {
884         document.title = '';
885       }
886       this.filenameEdit_.disabled = true;
887       this.filenameEdit_.value =
888           strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
889       this.shareButton_.hidden = true;
890     }
891   } else {
892     document.title = '';
893     this.filenameEdit_.disabled = true;
894     this.deleteButton_.disabled = true;
895     this.filenameEdit_.value = '';
896     this.shareButton_.hidden = true;
897   }
899   util.updateAppState(
900       null,  // Keep the current directory.
901       selectedEntryURL,  // Update the selection.
902       {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
906  * Click event handler on filename edit box
907  * @private
908  */
909 Gallery.prototype.onFilenameFocus_ = function() {
910   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
911   this.filenameEdit_.originalValue = this.filenameEdit_.value;
912   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
913   this.onUserAction_();
917  * Blur event handler on filename edit box.
919  * @param {Event} event Blur event.
920  * @return {Promise} Promise fulfilled on renaming completed.
921  * @private
922  */
923 Gallery.prototype.onFilenameEditBlur_ = function(event) {
924   var item = this.getSingleSelectedItem();
925   if (item) {
926     var oldEntry = item.getEntry();
928     item.rename(this.filenameEdit_.value).then(function() {
929       var event = new Event('content');
930       event.item = item;
931       event.oldEntry = oldEntry;
932       event.metadata = null;  // Metadata unchanged.
933       this.dataModel_.dispatchEvent(event);
934     }.bind(this), function(error) {
935       if (error === 'NOT_CHANGED')
936         return Promise.resolve();
937       this.filenameEdit_.value =
938           ImageUtil.getDisplayNameFromName(item.getEntry().name);
939       this.filenameEdit_.focus();
940       if (typeof error === 'string')
941         this.prompt_.showStringAt('center', error, 5000);
942       else
943         return Promise.reject(error);
944     }.bind(this)).catch(function(error) {
945       console.error(error.stack || error);
946     });
947   }
949   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
950   this.onUserAction_();
951   return Promise.resolve();
955  * Keydown event handler on filename edit box
956  * @private
957  */
958 Gallery.prototype.onFilenameEditKeydown_ = function() {
959   switch (event.keyCode) {
960     case 27:  // Escape
961       this.filenameEdit_.value = this.filenameEdit_.originalValue;
962       this.filenameEdit_.blur();
963       break;
965     case 13:  // Enter
966       this.filenameEdit_.blur();
967       break;
968   }
969   event.stopPropagation();
973  * @return {boolean} True if file renaming is currently in progress.
974  * @private
975  */
976 Gallery.prototype.isRenaming_ = function() {
977   return this.filenameSpacer_.hasAttribute('renaming');
981  * Content area click handler.
982  * @private
983  */
984 Gallery.prototype.onContentClick_ = function() {
985   this.filenameEdit_.blur();
989  * Share button handler.
990  * @private
991  */
992 Gallery.prototype.onShareButtonClick_ = function() {
993   var item = this.getSingleSelectedItem();
994   if (!item)
995     return;
996   this.shareDialog_.show(item.getEntry(), function() {});
1000  * Updates thumbnails.
1001  * @private
1002  */
1003 Gallery.prototype.updateThumbnails_ = function() {
1004   if (this.currentMode_ === this.slideMode_)
1005     this.slideMode_.updateThumbnails();
1007   if (this.mosaicMode_) {
1008     var mosaic = this.mosaicMode_.getMosaic();
1009     if (mosaic.isInitialized())
1010       mosaic.reload();
1011   }
1015  * Updates buttons.
1016  * @private
1017  */
1018 Gallery.prototype.updateButtons_ = function() {
1019   if (this.modeButton_) {
1020     var oppositeMode =
1021         this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
1022                                                 this.slideMode_;
1023     this.modeButton_.title = str(oppositeMode.getTitle());
1024   }
1028  * Singleton gallery.
1029  * @type {Gallery}
1030  */
1031 var gallery = null;
1034  * Initialize the window.
1035  * @param {Object} backgroundComponents Background components.
1036  */
1037 window.initialize = function(backgroundComponents) {
1038   window.loadTimeData.data = backgroundComponents.stringData;
1039   gallery = new Gallery(backgroundComponents.volumeManager);
1043  * Loads entries.
1044  * @param {!Array.<Entry>} entries Array of entries.
1045  * @param {!Array.<Entry>} selectedEntries Array of selected entries.
1046  */
1047 window.loadEntries = function(entries, selectedEntries) {
1048   gallery.load(entries, selectedEntries);