Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / gallery.js
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * 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); }
12
13 /**
14  * Overrided metadata worker's path.
15  * @type {string}
16  * @const
17  */
18 ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
19
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, []);
29
30   /**
31    * Metadata cache.
32    * @type {MetadataCache}
33    * @private
34    */
35   this.metadataCache_ = metadataCache;
36
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;
43 }
44
45 /**
46  * Maximum number of full size image cache.
47  * @type {number}
48  * @const
49  * @private
50  */
51 GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3;
52
53 /**
54  * Maximum number of screen size image cache.
55  * @type {number}
56  * @const
57  * @private
58  */
59 GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5;
60
61 GalleryDataModel.prototype = {
62   __proto__: cr.ui.ArrayDataModel.prototype
63 };
64
65 /**
66  * Saves new image.
67  *
68  * @param {VolumeManager} volumeManager Volume manager instance.
69  * @param {Gallery.Item} item Original gallery item.
70  * @param {Canvas} 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;
88
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           }
101
102           // The item's entry is updated to the latest entry. Update metadata.
103           item.setMetadata(newMetadata);
104
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);
112
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           }
134
135           fulfill();
136         }.bind(this));
137   }.bind(this));
138 };
139
140 /**
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   });
149
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   }
179 };
180
181 /**
182  * Gallery for viewing and editing image files.
183  *
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);
213
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();
225
226   this.initDom_();
227   this.initListeners_();
228 }
229
230 /**
231  * Gallery extends cr.EventTarget.
232  */
233 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
234
235 /**
236  * Tools fade-out timeout in milliseconds.
237  * @const
238  * @type {number}
239  */
240 Gallery.FADE_TIMEOUT = 3000;
241
242 /**
243  * First time tools fade-out timeout in milliseconds.
244  * @const
245  * @type {number}
246  */
247 Gallery.FIRST_FADE_TIMEOUT = 1000;
248
249 /**
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;
256
257 /**
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';
263
264 /**
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_);
271
272   this.inactivityWatcher_ = new MouseInactivityWatcher(
273       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
274
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_);
286 };
287
288 /**
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;
296
297   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
298       event.volumeInfo) {
299     close();
300   }
301 };
302
303 /**
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);
313 };
314
315 /**
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');
323
324   var content = document.querySelector('#content');
325   content.addEventListener('click', this.onContentClick_.bind(this));
326
327   this.header_ = document.querySelector('#header');
328   this.toolbar_ = document.querySelector('#toolbar');
329
330   var preventDefault = function(event) { event.preventDefault(); };
331
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);
338
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);
345
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);
352
353   this.filenameSpacer_ = this.toolbar_.querySelector('.filename-spacer');
354   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
355                                         'namebox', 'input');
356
357   this.filenameEdit_.setAttribute('type', 'text');
358   this.filenameEdit_.addEventListener('blur',
359       this.onFilenameEditBlur_.bind(this));
360
361   this.filenameEdit_.addEventListener('focus',
362       this.onFilenameFocus_.bind(this));
363
364   this.filenameEdit_.addEventListener('keydown',
365       this.onFilenameEditKeydown_.bind(this));
366
367   var middleSpacer = this.filenameSpacer_ =
368       this.toolbar_.querySelector('.middle-spacer');
369   var buttonSpacer = this.toolbar_.querySelector('button-spacer');
370
371   this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
372
373   this.modeButton_ = this.toolbar_.querySelector('button.mode');
374   this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null));
375
376   this.mosaicMode_ = new MosaicMode(content,
377                                     this.dataModel_,
378                                     this.selectionModel_,
379                                     this.volumeManager_,
380                                     this.toggleMode_.bind(this, null));
381
382   this.slideMode_ = new SlideMode(this.container_,
383                                   content,
384                                   this.toolbar_,
385                                   this.prompt_,
386                                   this.dataModel_,
387                                   this.selectionModel_,
388                                   this.context_,
389                                   this.volumeManager_,
390                                   this.toggleMode_.bind(this),
391                                   str);
392
393   this.slideMode_.addEventListener('image-displayed', function() {
394     cr.dispatchSimpleEvent(this, 'image-displayed');
395   }.bind(this));
396   this.slideMode_.addEventListener('image-saved', function() {
397     cr.dispatchSimpleEvent(this, 'image-saved');
398   }.bind(this));
399
400   var deleteButton = this.initToolbarButton_('delete', 'GALLERY_DELETE');
401   deleteButton.addEventListener('click', this.delete_.bind(this));
402
403   this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
404   this.shareButton_.addEventListener(
405       'click', this.onShareButtonClick_.bind(this));
406
407   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
408   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
409
410   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
411   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
412
413   this.shareDialog_ = new ShareDialog(this.container_);
414 };
415
416 /**
417  * Initializes a toolbar button.
418  *
419  * @param {string} className Class to add.
420  * @param {string} title Button title.
421  * @return {!HTMLElement} Newly created button.
422  * @private
423  */
424 Gallery.prototype.initToolbarButton_ = function(className, title) {
425   var button = this.toolbar_.querySelector('button.' + className);
426   button.title = str(title);
427   return button;
428 };
429
430 /**
431  * Loads the content.
432  *
433  * @param {!Array.<Entry>} entries Array of entries.
434  * @param {!Array.<Entry>} selectedEntries Array of selected entries.
435  */
436 Gallery.prototype.load = function(entries, selectedEntries) {
437   // Obtains max chank size.
438   var maxChunkSize = 20;
439   var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
440   if (volumeInfo &&
441       volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) {
442     maxChunkSize = 1;
443   }
444   if (volumeInfo.isReadOnly)
445     this.context_.readonlyDirName = volumeInfo.label;
446
447   // Make loading list.
448   var entrySet = {};
449   for (var i = 0; i < entries.length; i++) {
450     var entry = entries[i];
451     entrySet[entry.toURL()] = {
452       entry: entry,
453       selected: false,
454       index: i
455     };
456   }
457   for (var i = 0; i < selectedEntries.length; i++) {
458     var entry = selectedEntries[i];
459     entrySet[entry.toURL()] = {
460       entry: entry,
461       selected: true,
462       index: i
463     };
464   }
465   var loadingList = [];
466   for (var url in entrySet) {
467     loadingList.push(entrySet[url]);
468   }
469   loadingList = loadingList.sort(function(a, b) {
470     if (a.selected && !b.selected)
471       return -1;
472     else if (!a.selected && b.selected)
473       return 1;
474     else
475       return a.index - b.index;
476   });
477
478   // Load entries.
479   // Use the self variable capture-by-closure because it is faster than bind.
480   var self = this;
481   var loadChunk = function(firstChunk) {
482     // Extract chunk.
483     var chunk = loadingList.splice(0, maxChunkSize);
484     if (!chunk.length)
485       return;
486
487     return new Promise(function(fulfill) {
488       // Obtains metadata for chunk.
489       var entries = chunk.map(function(chunkItem) {
490         return chunkItem.entry;
491       });
492       self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill);
493     }).then(function(metadataList) {
494       if (chunk.length !== metadataList.length)
495         return Promise.reject('Failed to load metadata.');
496
497       // Add items to the model.
498       var items = [];
499       chunk.forEach(function(chunkItem, index) {
500         var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
501         if (!locationInfo)  // Skip the item, since gone.
502           return;
503         var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]);
504         items.push(new Gallery.Item(
505             chunkItem.entry,
506             locationInfo,
507             clonedMetadata,
508             self.metadataCache_,
509             /* original */ true));
510       });
511       self.dataModel_.push.apply(self.dataModel_, items);
512
513       // Apply the selection.
514       var selectionUpdated = false;
515       for (var i = 0; i < chunk.length; i++) {
516         if (!chunk[i].selected)
517           continue;
518         var index = self.dataModel_.indexOf(items[i]);
519         if (index < 0)
520           continue;
521         self.selectionModel_.setIndexSelected(index, true);
522         selectionUpdated = true;
523       }
524       if (selectionUpdated)
525         self.onSelection_();
526
527       // Init modes after the first chunk is loaded.
528       if (firstChunk) {
529         // Determine the initial mode.
530         var shouldShowMosaic = selectedEntries.length > 1 ||
531             (self.context_.pageState &&
532              self.context_.pageState.gallery === 'mosaic');
533         self.setCurrentMode_(
534             shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
535
536         // Init mosaic mode.
537         var mosaic = self.mosaicMode_.getMosaic();
538         mosaic.init();
539
540         // Do the initialization for each mode.
541         if (shouldShowMosaic) {
542           mosaic.show();
543           self.inactivityWatcher_.check();  // Show the toolbar.
544           cr.dispatchSimpleEvent(self, 'loaded');
545         } else {
546           self.slideMode_.enter(
547               null,
548               function() {
549                 // Flash the toolbar briefly to show it is there.
550                 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
551               },
552               function() {
553                 cr.dispatchSimpleEvent(self, 'loaded');
554               });
555         }
556       }
557
558       // Continue to load chunks.
559       return loadChunk(/* firstChunk */ false);
560     });
561   };
562   loadChunk(/* firstChunk */ true).catch(function(error) {
563     console.error(error.stack || error);
564   });
565 };
566
567 /**
568  * Handles user's 'Close' action.
569  * @private
570  */
571 Gallery.prototype.onClose_ = function() {
572   this.executeWhenReady(this.context_.onClose);
573 };
574
575 /**
576  * Handles user's 'Maximize' action (Escape or a click on the X icon).
577  * @private
578  */
579 Gallery.prototype.onMaximize_ = function() {
580   this.executeWhenReady(this.context_.onMaximize);
581 };
582
583 /**
584  * Handles user's 'Maximize' action (Escape or a click on the X icon).
585  * @private
586  */
587 Gallery.prototype.onMinimize_ = function() {
588   this.executeWhenReady(this.context_.onMinimize);
589 };
590
591 /**
592  * Executes a function when the editor is done with the modifications.
593  * @param {function} callback Function to execute.
594  */
595 Gallery.prototype.executeWhenReady = function(callback) {
596   this.currentMode_.executeWhenReady(callback);
597 };
598
599 /**
600  * @return {Object} File manager private API.
601  */
602 Gallery.getFileManagerPrivate = function() {
603   return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
604 };
605
606 /**
607  * @return {boolean} True if some tool is currently active.
608  */
609 Gallery.prototype.hasActiveTool = function() {
610   return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
611       this.isRenaming_();
612 };
613
614 /**
615 * External user action event handler.
616 * @private
617 */
618 Gallery.prototype.onUserAction_ = function() {
619   // Show the toolbar and hide it after the default timeout.
620   this.inactivityWatcher_.kick();
621 };
622
623 /**
624  * Sets the current mode, update the UI.
625  * @param {Object} mode Current mode.
626  * @private
627  */
628 Gallery.prototype.setCurrentMode_ = function(mode) {
629   if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
630     console.error('Invalid Gallery mode');
631
632   this.currentMode_ = mode;
633   this.container_.setAttribute('mode', this.currentMode_.getName());
634   this.updateSelectionAndState_();
635   this.updateButtons_();
636 };
637
638 /**
639  * Mode toggle event handler.
640  * @param {function=} opt_callback Callback.
641  * @param {Event=} opt_event Event that caused this call.
642  * @private
643  */
644 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
645   if (!this.modeButton_)
646     return;
647
648   if (this.changingMode_) // Do not re-enter while changing the mode.
649     return;
650
651   if (opt_event)
652     this.onUserAction_();
653
654   this.changingMode_ = true;
655
656   var onModeChanged = function() {
657     this.changingMode_ = false;
658     if (opt_callback) opt_callback();
659   }.bind(this);
660
661   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
662
663   var mosaic = this.mosaicMode_.getMosaic();
664   var tileRect = mosaic.getTileRect(tileIndex);
665
666   if (this.currentMode_ === this.slideMode_) {
667     this.setCurrentMode_(this.mosaicMode_);
668     mosaic.transform(
669         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
670     this.slideMode_.leave(
671         tileRect,
672         function() {
673           // Animate back to normal position.
674           mosaic.transform();
675           mosaic.show();
676           onModeChanged();
677         }.bind(this));
678   } else {
679     this.setCurrentMode_(this.slideMode_);
680     this.slideMode_.enter(
681         tileRect,
682         function() {
683           // Animate to zoomed position.
684           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
685           mosaic.hide();
686         }.bind(this),
687         onModeChanged);
688   }
689 };
690
691 /**
692  * Deletes the selected items.
693  * @private
694  */
695 Gallery.prototype.delete_ = function() {
696   this.onUserAction_();
697
698   // Clone the sorted selected indexes array.
699   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
700   if (!indexesToRemove.length)
701     return;
702
703   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
704
705   var itemsToRemove = this.getSelectedItems();
706   var plural = itemsToRemove.length > 1;
707   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
708
709   function deleteNext() {
710     if (!itemsToRemove.length)
711       return;  // All deleted.
712
713     var entry = itemsToRemove.pop().getEntry();
714     entry.remove(deleteNext, function() {
715       util.flog('Error deleting: ' + entry.name, deleteNext);
716     });
717   }
718
719   // Prevent the Gallery from handling Esc and Enter.
720   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
721   var restoreListener = function() {
722     this.document_.body.addEventListener('keydown', this.keyDownBound_);
723   }.bind(this);
724
725
726   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
727   confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
728   confirm.show(strf(plural ?
729       'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
730       function() {
731         restoreListener();
732         this.selectionModel_.unselectAll();
733         this.selectionModel_.leadIndex = -1;
734         // Remove items from the data model, starting from the highest index.
735         while (indexesToRemove.length)
736           this.dataModel_.splice(indexesToRemove.pop(), 1);
737         // Delete actual files.
738         deleteNext();
739       }.bind(this),
740       function() {
741         // Restore the listener after a timeout so that ESC is processed.
742         setTimeout(restoreListener, 0);
743       });
744 };
745
746 /**
747  * @return {Array.<Gallery.Item>} Current selection.
748  */
749 Gallery.prototype.getSelectedItems = function() {
750   return this.selectionModel_.selectedIndexes.map(
751       this.dataModel_.item.bind(this.dataModel_));
752 };
753
754 /**
755  * @return {Array.<Entry>} Array of currently selected entries.
756  */
757 Gallery.prototype.getSelectedEntries = function() {
758   return this.selectionModel_.selectedIndexes.map(function(index) {
759     return this.dataModel_.item(index).getEntry();
760   }.bind(this));
761 };
762
763 /**
764  * @return {?Gallery.Item} Current single selection.
765  */
766 Gallery.prototype.getSingleSelectedItem = function() {
767   var items = this.getSelectedItems();
768   if (items.length > 1) {
769     console.error('Unexpected multiple selection');
770     return null;
771   }
772   return items[0];
773 };
774
775 /**
776   * Selection change event handler.
777   * @private
778   */
779 Gallery.prototype.onSelection_ = function() {
780   this.updateSelectionAndState_();
781 };
782
783 /**
784   * Data model splice event handler.
785   * @private
786   */
787 Gallery.prototype.onSplice_ = function() {
788   this.selectionModel_.adjustLength(this.dataModel_.length);
789 };
790
791 /**
792  * Content change event handler.
793  * @param {Event} event Event.
794  * @private
795 */
796 Gallery.prototype.onContentChange_ = function(event) {
797   var index = this.dataModel_.indexOf(event.item);
798   if (index !== this.selectionModel_.selectedIndex)
799     console.error('Content changed for unselected item');
800   this.updateSelectionAndState_();
801 };
802
803 /**
804  * Keydown handler.
805  *
806  * @param {Event} event Event.
807  * @private
808  */
809 Gallery.prototype.onKeyDown_ = function(event) {
810   if (this.currentMode_.onKeyDown(event))
811     return;
812
813   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
814     case 'U+0008': // Backspace.
815       // The default handler would call history.back and close the Gallery.
816       event.preventDefault();
817       break;
818
819     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
820       this.toggleMode_(null, event);
821       break;
822
823     case 'U+0056':  // 'v'
824     case 'MediaPlayPause':
825       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
826       break;
827
828     case 'U+007F':  // Delete
829     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
830     case 'U+0044':  // 'd'
831       this.delete_();
832       break;
833   }
834 };
835
836 // Name box and rename support.
837
838 /**
839  * Updates the UI related to the selected item and the persistent state.
840  *
841  * @private
842  */
843 Gallery.prototype.updateSelectionAndState_ = function() {
844   var numSelectedItems = this.selectionModel_.selectedIndexes.length;
845   var selectedEntryURL = null;
846
847   // If it's selecting something, update the variable values.
848   if (numSelectedItems) {
849     // Obtains selected item.
850     var selectedItem =
851         this.dataModel_.item(this.selectionModel_.selectedIndex);
852     this.selectedEntry_ = selectedItem.getEntry();
853     selectedEntryURL = this.selectedEntry_.toURL();
854
855     // Update cache.
856     selectedItem.touch();
857     this.dataModel_.evictCache();
858
859     // Update the title and the display name.
860     if (numSelectedItems === 1) {
861       document.title = this.selectedEntry_.name;
862       this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
863       this.filenameEdit_.value =
864           ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
865       this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
866     } else {
867       if (this.context_.curDirEntry) {
868         // If the Gallery was opened on search results the search query will not
869         // be recorded in the app state and the relaunch will just open the
870         // gallery in the curDirEntry directory.
871         document.title = this.context_.curDirEntry.name;
872       } else {
873         document.title = '';
874       }
875       this.filenameEdit_.disabled = true;
876       this.filenameEdit_.value =
877           strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
878       this.shareButton_.hidden = true;
879     }
880   } else {
881     document.title = '';
882     this.filenameEdit_.disabled = true;
883     this.filenameEdit_.value = '';
884     this.shareButton_.hidden = true;
885   }
886
887   util.updateAppState(
888       null,  // Keep the current directory.
889       selectedEntryURL,  // Update the selection.
890       {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
891 };
892
893 /**
894  * Click event handler on filename edit box
895  * @private
896  */
897 Gallery.prototype.onFilenameFocus_ = function() {
898   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
899   this.filenameEdit_.originalValue = this.filenameEdit_.value;
900   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
901   this.onUserAction_();
902 };
903
904 /**
905  * Blur event handler on filename edit box.
906  *
907  * @param {Event} event Blur event.
908  * @return {Promise} Promise fulfilled on renaming completed.
909  * @private
910  */
911 Gallery.prototype.onFilenameEditBlur_ = function(event) {
912   var item = this.getSingleSelectedItem();
913   if (item) {
914     var oldEntry = item.getEntry();
915
916     item.rename(this.filenameEdit_.value).then(function() {
917       var event = new Event('content');
918       event.item = item;
919       event.oldEntry = oldEntry;
920       event.metadata = null;  // Metadata unchanged.
921       this.dataModel_.dispatchEvent(event);
922     }.bind(this), function(error) {
923       if (error === 'NOT_CHANGED')
924         return Promise.resolve();
925       this.filenameEdit_.value =
926           ImageUtil.getDisplayNameFromName(item.getEntry().name);
927       this.filenameEdit_.focus();
928       if (typeof error === 'string')
929         this.prompt_.showStringAt('center', error, 5000);
930       else
931         return Promise.reject(error);
932     }.bind(this)).catch(function(error) {
933       console.error(error.stack || error);
934     });
935   }
936
937   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
938   this.onUserAction_();
939   return Promise.resolve();
940 };
941
942 /**
943  * Keydown event handler on filename edit box
944  * @private
945  */
946 Gallery.prototype.onFilenameEditKeydown_ = function() {
947   switch (event.keyCode) {
948     case 27:  // Escape
949       this.filenameEdit_.value = this.filenameEdit_.originalValue;
950       this.filenameEdit_.blur();
951       break;
952
953     case 13:  // Enter
954       this.filenameEdit_.blur();
955       break;
956   }
957   event.stopPropagation();
958 };
959
960 /**
961  * @return {boolean} True if file renaming is currently in progress.
962  * @private
963  */
964 Gallery.prototype.isRenaming_ = function() {
965   return this.filenameSpacer_.hasAttribute('renaming');
966 };
967
968 /**
969  * Content area click handler.
970  * @private
971  */
972 Gallery.prototype.onContentClick_ = function() {
973   this.filenameEdit_.blur();
974 };
975
976 /**
977  * Share button handler.
978  * @private
979  */
980 Gallery.prototype.onShareButtonClick_ = function() {
981   var item = this.getSingleSelectedItem();
982   if (!item)
983     return;
984   this.shareDialog_.show(item.getEntry(), function() {});
985 };
986
987 /**
988  * Updates thumbnails.
989  * @private
990  */
991 Gallery.prototype.updateThumbnails_ = function() {
992   if (this.currentMode_ === this.slideMode_)
993     this.slideMode_.updateThumbnails();
994
995   if (this.mosaicMode_) {
996     var mosaic = this.mosaicMode_.getMosaic();
997     if (mosaic.isInitialized())
998       mosaic.reload();
999   }
1000 };
1001
1002 /**
1003  * Updates buttons.
1004  * @private
1005  */
1006 Gallery.prototype.updateButtons_ = function() {
1007   if (this.modeButton_) {
1008     var oppositeMode =
1009         this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
1010                                                 this.slideMode_;
1011     this.modeButton_.title = str(oppositeMode.getTitle());
1012   }
1013 };
1014
1015 /**
1016  * Singleton gallery.
1017  * @type {Gallery}
1018  */
1019 var gallery = null;
1020
1021 /**
1022  * Initialize the window.
1023  * @param {Object} backgroundComponents Background components.
1024  */
1025 window.initialize = function(backgroundComponents) {
1026   window.loadTimeData.data = backgroundComponents.stringData;
1027   gallery = new Gallery(backgroundComponents.volumeManager);
1028 };
1029
1030 /**
1031  * Loads entries.
1032  * @param {!Array.<Entry>} entries Array of entries.
1033  * @param {!Array.<Entry>} selectedEntries Array of selected entries.
1034  */
1035 window.loadEntries = function(entries, selectedEntries) {
1036   gallery.load(entries, selectedEntries);
1037 };