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