Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery.js
blob5d995924551d004d4543ddb3feb8c452904c9edb
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 /**
6  * Overrided metadata worker's path.
7  * @type {string}
8  */
9 ContentMetadataProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
11 /**
12  * Gallery for viewing and editing image files.
13  *
14  * @param {!VolumeManagerWrapper} volumeManager
15  * @constructor
16  * @struct
17  */
18 function Gallery(volumeManager) {
19   /**
20    * @type {{appWindow: chrome.app.window.AppWindow, readonlyDirName: string,
21    *     displayStringFunction: function(), loadTimeData: Object}}
22    * @private
23    */
24   this.context_ = {
25     appWindow: chrome.app.window.current(),
26     readonlyDirName: '',
27     displayStringFunction: function() { return ''; },
28     loadTimeData: {},
29   };
30   this.container_ = queryRequiredElement('.gallery');
31   this.document_ = document;
32   this.volumeManager_ = volumeManager;
33   /**
34    * @private {!MetadataModel}
35    * @const
36    */
37   this.metadataModel_ = MetadataModel.create(volumeManager);
38   /**
39    * @private {!ThumbnailModel}
40    * @const
41    */
42   this.thumbnailModel_ = new ThumbnailModel(this.metadataModel_);
43   this.selectedEntry_ = null;
44   this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
45   this.initialized_ = false;
47   this.dataModel_ = new GalleryDataModel(this.metadataModel_);
48   var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
49       VolumeManagerCommon.VolumeType.DOWNLOADS);
50   downloadVolumeInfo.resolveDisplayRoot().then(function(entry) {
51     this.dataModel_.fallbackSaveDirectory = entry;
52   }.bind(this)).catch(function(error) {
53     console.error(
54         'Failed to obtain the fallback directory: ' + (error.stack || error));
55   });
56   this.selectionModel_ = new cr.ui.ListSelectionModel();
58   /**
59    * @type {(SlideMode|ThumbnailMode)}
60    * @private
61    */
62   this.currentMode_ = null;
64   /**
65    * @type {boolean}
66    * @private
67    */
68   this.changingMode_ = false;
70   // -----------------------------------------------------------------
71   // Initializes the UI.
73   // Initialize the dialog label.
74   cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
75   cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
77   var content = getRequiredElement('content');
78   content.addEventListener('click', this.onContentClick_.bind(this));
80   this.topToolbar_ = getRequiredElement('top-toolbar');
81   this.bottomToolbar_ = getRequiredElement('bottom-toolbar');
83   this.filenameSpacer_ = queryRequiredElement('.filename-spacer',
84       this.topToolbar_);
86   /**
87    * @private {HTMLInputElement}
88    * @const
89    */
90   this.filenameEdit_ = /** @type {HTMLInputElement} */
91       (queryRequiredElement('input', this.filenameSpacer_));
93   this.filenameCanvas_ = document.createElement('canvas');
94   this.filenameCanvasContext_ = this.filenameCanvas_.getContext('2d');
96   // Set font style of canvas context to same font style with rename field.
97   var filenameEditComputedStyle = window.getComputedStyle(this.filenameEdit_);
98   this.filenameCanvasContext_.font = filenameEditComputedStyle.font;
100   this.filenameEdit_.addEventListener('blur',
101       this.onFilenameEditBlur_.bind(this));
102   this.filenameEdit_.addEventListener('focus',
103       this.onFilenameFocus_.bind(this));
104   this.filenameEdit_.addEventListener('input',
105       this.resizeRenameField_.bind(this));
106   this.filenameEdit_.addEventListener('keydown',
107       this.onFilenameEditKeydown_.bind(this));
109   var buttonSpacer = queryRequiredElement('.button-spacer', this.topToolbar_);
111   this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
113   this.errorBanner_ = new ErrorBanner(this.container_);
115   /**
116    * @private {!HTMLElement}
117    * @const
118    */
119   this.modeSwitchButton_ = queryRequiredElement('button.mode',
120       this.topToolbar_);
121   GalleryUtil.decorateMouseFocusHandling(this.modeSwitchButton_);
122   this.modeSwitchButton_.addEventListener('click',
123       this.onModeSwitchButtonClicked_.bind(this));
125   /**
126    * @private {!PaperRipple}
127    */
128   this.modeSwitchButtonRipple_ = /** @type {!PaperRipple} */
129       (queryRequiredElement('paper-ripple', this.modeSwitchButton_));
131   /**
132    * @private {!DimmableUIController}
133    * @const
134    */
135   this.dimmableUIController_ = new DimmableUIController(this.container_);
137   this.thumbnailMode_ = new ThumbnailMode(
138       assertInstanceof(document.querySelector('.thumbnail-view'), HTMLElement),
139       this.errorBanner_,
140       this.dataModel_,
141       this.selectionModel_,
142       this.onChangeToSlideMode_.bind(this));
143   this.thumbnailMode_.hide();
145   this.slideMode_ = new SlideMode(this.container_,
146                                   content,
147                                   this.topToolbar_,
148                                   this.bottomToolbar_,
149                                   this.prompt_,
150                                   this.errorBanner_,
151                                   this.dataModel_,
152                                   this.selectionModel_,
153                                   this.metadataModel_,
154                                   this.thumbnailModel_,
155                                   this.context_,
156                                   this.volumeManager_,
157                                   this.toggleMode_.bind(this),
158                                   str,
159                                   this.dimmableUIController_);
160   this.slideMode_.addEventListener('image-displayed', function() {
161     cr.dispatchSimpleEvent(this, 'image-displayed');
162   }.bind(this));
164   /**
165    * @private {!HTMLElement}
166    * @const
167    */
168   this.deleteButton_ = queryRequiredElement(
169       'paper-button.delete', this.topToolbar_);
170   this.deleteButton_.addEventListener('click', this.delete_.bind(this));
172   /**
173    * @private {!HTMLElement}
174    * @const
175    */
176   this.slideshowButton_ = queryRequiredElement('paper-button.slideshow',
177       this.topToolbar_);
179   /**
180    * @private {!HTMLElement}
181    * @const
182    */
183   this.shareButton_ = queryRequiredElement(
184       'paper-button.share', this.topToolbar_);
185   this.shareButton_.addEventListener(
186       'click', this.onShareButtonClick_.bind(this));
188   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
189   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
191   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
192   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
194   this.shareDialog_ = new ShareDialog(this.container_);
196   // -----------------------------------------------------------------
197   // Initialize listeners.
199   this.keyDownBound_ = this.onKeyDown_.bind(this);
200   this.document_.body.addEventListener('keydown', this.keyDownBound_);
202   // TODO(hirono): Add observer to handle thumbnail update.
203   this.volumeManager_.addEventListener(
204       'externally-unmounted', this.onExternallyUnmountedBound_);
205   // The 'pagehide' event is called when the app window is closed.
206   window.addEventListener('pagehide', this.onPageHide_.bind(this));
208   window.addEventListener('resize', this.resizeRenameField_.bind(this));
210   // We must call this method after elements of all tools have been attached to
211   // the DOM.
212   this.dimmableUIController_.setTools(document.querySelectorAll('.tool'));
216  * Gallery extends cr.EventTarget.
217  */
218 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
221  * Tools fade-out timeout in milliseconds.
222  * @const
223  * @type {number}
224  */
225 Gallery.FADE_TIMEOUT = 2000;
228  * First time tools fade-out timeout in milliseconds.
229  * @const
230  * @type {number}
231  */
232 Gallery.FIRST_FADE_TIMEOUT = 1000;
235  * Time until mosaic is initialized in the background. Used to make gallery
236  * in the slide mode load faster. In milliseconds.
237  * @const
238  * @type {number}
239  */
240 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
243  * Types of metadata Gallery uses (to query the metadata cache).
244  * @const
245  * @type {!Array<string>}
246  */
247 Gallery.PREFETCH_PROPERTY_NAMES =
248     ['imageWidth', 'imageHeight', 'imageRotation', 'size', 'present'];
251  * Closes gallery when a volume containing the selected item is unmounted.
252  * @param {!Event} event The unmount event.
253  * @private
254  */
255 Gallery.prototype.onExternallyUnmounted_ = function(event) {
256   if (!this.selectedEntry_)
257     return;
259   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
260       event.volumeInfo) {
261     window.close();
262   }
266  * Unloads the Gallery.
267  * @private
268  */
269 Gallery.prototype.onPageHide_ = function() {
270   this.volumeManager_.removeEventListener(
271       'externally-unmounted', this.onExternallyUnmountedBound_);
272   this.volumeManager_.dispose();
276  * Loads the content.
278  * @param {!Array<!Entry>} selectedEntries Array of selected entries.
279  */
280 Gallery.prototype.load = function(selectedEntries) {
281   GalleryUtil.createEntrySet(selectedEntries).then(function(allEntries) {
282     this.loadInternal_(allEntries, selectedEntries);
283   }.bind(this));
287  * Loads the content.
289  * @param {!Array<!FileEntry>} entries Array of entries.
290  * @param {!Array<!FileEntry>} selectedEntries Array of selected entries.
291  * @private
292  */
293 Gallery.prototype.loadInternal_ = function(entries, selectedEntries) {
294   // Add the entries to data model.
295   var items = [];
296   for (var i = 0; i < entries.length; i++) {
297     var locationInfo = this.volumeManager_.getLocationInfo(entries[i]);
298     if (!locationInfo)  // Skip the item, since gone.
299       return;
300     items.push(new Gallery.Item(
301         entries[i],
302         locationInfo,
303         null,
304         null,
305         true));
306   }
307   this.dataModel_.splice(0, this.dataModel_.length);
308   this.updateThumbnails_();  // Remove the caches.
310   GalleryDataModel.prototype.splice.apply(
311       this.dataModel_, [0, 0].concat(items));
313   // Apply the selection.
314   var selectedSet = {};
315   for (var i = 0; i < selectedEntries.length; i++) {
316     selectedSet[selectedEntries[i].toURL()] = true;
317   }
318   for (var i = 0; i < items.length; i++) {
319     if (!selectedSet[items[i].getEntry().toURL()])
320       continue;
321     this.selectionModel_.setIndexSelected(i, true);
322   }
323   this.onSelection_();
325   // Obtains max chank size.
326   var maxChunkSize = 20;
327   var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
328   if (volumeInfo) {
329     if (GalleryUtil.isOnMTPVolume(entries[0], this.volumeManager_))
330       maxChunkSize = 1;
332     if (volumeInfo.isReadOnly ||
333         GalleryUtil.isOnMTPVolume(entries[0], this.volumeManager_)) {
334       this.context_.readonlyDirName = volumeInfo.label;
335     }
336   }
338   // If items are empty, stop initialization.
339   if (items.length === 0) {
340     this.dataModel_.splice(0, this.dataModel_.length);
341     return;
342   }
344   // Load entries.
345   // Use the self variable capture-by-closure because it is faster than bind.
346   var self = this;
347   var thumbnailModel = new ThumbnailModel(this.metadataModel_);
348   var loadChunk = function(firstChunk) {
349     // Extract chunk.
350     var chunk = items.splice(0, maxChunkSize);
351     if (!chunk.length)
352       return;
353     var entries = chunk.map(function(chunkItem) {
354       return chunkItem.getEntry();
355     });
356     var metadataPromise = self.metadataModel_.get(
357         entries, Gallery.PREFETCH_PROPERTY_NAMES);
358     var thumbnailPromise = thumbnailModel.get(entries);
359     return Promise.all([metadataPromise, thumbnailPromise]).then(
360         function(metadataLists) {
361       // Add items to the model.
362       chunk.forEach(function(chunkItem, index) {
363         chunkItem.setMetadataItem(metadataLists[0][index]);
364         chunkItem.setThumbnailMetadataItem(metadataLists[1][index]);
366         var event = new Event('content');
367         event.item = chunkItem;
368         event.oldEntry = chunkItem.getEntry();
369         event.thumbnailChanged = true;
370         self.dataModel_.dispatchEvent(event);
371       });
373       // Init modes after the first chunk is loaded.
374       if (firstChunk && !self.initialized_) {
375         // Determine the initial mode.
376         var shouldShowThumbnail = selectedEntries.length > 1 ||
377             (self.context_.pageState &&
378              self.context_.pageState.gallery === 'thumbnail');
379         self.setCurrentMode_(
380             shouldShowThumbnail ? self.thumbnailMode_ : self.slideMode_);
382         // Do the initialization for each mode.
383         if (shouldShowThumbnail) {
384           self.thumbnailMode_.show();
385           cr.dispatchSimpleEvent(self, 'loaded');
386         } else {
387           self.slideMode_.enter(
388               null,
389               function() {
390                 // Flash the toolbar briefly to show it is there.
391                 self.dimmableUIController_.kick(Gallery.FIRST_FADE_TIMEOUT);
392               },
393               function() {
394                 cr.dispatchSimpleEvent(self, 'loaded');
395               });
396         }
397         self.initialized_ = true;
398       }
400       // Continue to load chunks.
401       return loadChunk(/* firstChunk */ false);
402     });
403   };
404   loadChunk(/* firstChunk */ true).catch(function(error) {
405     console.error(error.stack || error);
406   });
410  * @return {boolean} True if some tool is currently active.
411  */
412 Gallery.prototype.hasActiveTool = function() {
413   return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
414       this.isRenaming_();
418 * External user action event handler.
419 * @private
421 Gallery.prototype.onUserAction_ = function() {
422   // Show the toolbar and hide it after the default timeout.
423   this.dimmableUIController_.kick();
427  * Sets the current mode, update the UI.
428  * @param {!(SlideMode|ThumbnailMode)} mode Current mode.
429  * @private
431  * TODO(yawano): Since this method is confusing with changeCurrentMode_. Rename
432  *     or remove this method.
433  */
434 Gallery.prototype.setCurrentMode_ = function(mode) {
435   if (mode !== this.slideMode_ && mode !== this.thumbnailMode_)
436     console.error('Invalid Gallery mode');
438   this.currentMode_ = mode;
439   this.container_.setAttribute('mode', this.currentMode_.getName());
440   this.dimmableUIController_.setDisabled(this.currentMode_ !== this.slideMode_);
441   this.updateSelectionAndState_();
445  * Handles click event of mode switch button.
446  * @param {!Event} event An event.
447  * @private
448  */
449 Gallery.prototype.onModeSwitchButtonClicked_ = function(event) {
450   this.modeSwitchButtonRipple_.simulatedRipple();
451   this.toggleMode_(undefined /* callback */, event);
455  * Change to slide mode.
456  * @private
457  */
458 Gallery.prototype.onChangeToSlideMode_ = function() {
459   if (this.modeSwitchButton_.disabled)
460     return;
462   this.changeCurrentMode_(this.slideMode_);
466  * Change current mode.
467  * @param {!(SlideMode|ThumbnailMode)} mode Target mode.
468  * @param {Event=} opt_event Event that caused this call.
469  * @private
470  */
471 Gallery.prototype.changeCurrentMode_ = function(mode, opt_event) {
472   return new Promise(function(fulfill, reject) {
473     // Do not re-enter while changing the mode.
474     if (this.currentMode_ === mode || this.changingMode_) {
475       fulfill();
476       return;
477     }
479     if (opt_event)
480       this.onUserAction_();
482     this.changingMode_ = true;
484     var onModeChanged = function() {
485       this.changingMode_ = false;
486       fulfill();
487     }.bind(this);
489     var thumbnailIndex = Math.max(0, this.selectionModel_.selectedIndex);
490     var thumbnailRect = ImageRect.createFromBounds(
491         this.thumbnailMode_.getThumbnailRect(thumbnailIndex));
493     if (mode === this.thumbnailMode_) {
494       this.setCurrentMode_(this.thumbnailMode_);
495       this.slideMode_.leave(
496           thumbnailRect,
497           function() {
498             // Show thumbnail mode and perform animation.
499             this.thumbnailMode_.show();
500             var fromRect = this.slideMode_.getSelectedImageRect();
501             if (fromRect) {
502               this.thumbnailMode_.performEnterAnimation(
503                   thumbnailIndex, fromRect);
504             }
505             this.thumbnailMode_.focus();
507             onModeChanged();
508           }.bind(this));
509       this.bottomToolbar_.hidden = true;
510     } else {
511       // TODO(yawano): Make animation smooth. With this implementation,
512       //     animation starts after the image is fully loaded.
513       this.setCurrentMode_(this.slideMode_);
514       this.slideMode_.enter(
515           thumbnailRect,
516           function() {
517             // Animate to zoomed position.
518             this.thumbnailMode_.hide();
519           }.bind(this),
520           onModeChanged);
521       this.bottomToolbar_.hidden = false;
522     }
523   }.bind(this));
527  * Mode toggle event handler.
528  * @param {function()=} opt_callback Callback.
529  * @param {Event=} opt_event Event that caused this call.
530  * @private
531  */
532 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
533   // If it's in editing, leave edit mode.
534   if (this.slideMode_.isEditing())
535     this.slideMode_.toggleEditor();
537   var targetMode = this.currentMode_ === this.slideMode_ ?
538       this.thumbnailMode_ : this.slideMode_;
540   this.changeCurrentMode_(targetMode, opt_event).then(function() {
541     if (opt_callback)
542       opt_callback();
543   });
547  * Deletes the selected items.
548  * @private
549  */
550 Gallery.prototype.delete_ = function() {
551   this.onUserAction_();
553   // Clone the sorted selected indexes array.
554   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
555   if (!indexesToRemove.length)
556     return;
558   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
560   var itemsToRemove = this.getSelectedItems();
561   var plural = itemsToRemove.length > 1;
562   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
564   function deleteNext() {
565     if (!itemsToRemove.length)
566       return;  // All deleted.
568     var entry = itemsToRemove.pop().getEntry();
569     entry.remove(deleteNext, function() {
570       console.error('Error deleting: ' + entry.name);
571       deleteNext();
572     });
573   }
575   // Prevent the Gallery from handling Esc and Enter.
576   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
577   var restoreListener = function() {
578     this.document_.body.addEventListener('keydown', this.keyDownBound_);
579   }.bind(this);
582   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
583   confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
584   confirm.show(strf(plural ?
585       'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
586       function() {
587         restoreListener();
588         this.selectionModel_.unselectAll();
589         this.selectionModel_.leadIndex = -1;
590         // Remove items from the data model, starting from the highest index.
591         while (indexesToRemove.length)
592           this.dataModel_.splice(indexesToRemove.pop(), 1);
593         // Delete actual files.
594         deleteNext();
595       }.bind(this),
596       function() {
597         // Restore the listener after a timeout so that ESC is processed.
598         setTimeout(restoreListener, 0);
599       },
600       null);
604  * @return {!Array<Gallery.Item>} Current selection.
605  */
606 Gallery.prototype.getSelectedItems = function() {
607   return this.selectionModel_.selectedIndexes.map(
608       this.dataModel_.item.bind(this.dataModel_));
612  * @return {!Array<Entry>} Array of currently selected entries.
613  */
614 Gallery.prototype.getSelectedEntries = function() {
615   return this.selectionModel_.selectedIndexes.map(function(index) {
616     return this.dataModel_.item(index).getEntry();
617   }.bind(this));
621  * @return {?Gallery.Item} Current single selection.
622  */
623 Gallery.prototype.getSingleSelectedItem = function() {
624   var items = this.getSelectedItems();
625   if (items.length > 1) {
626     console.error('Unexpected multiple selection');
627     return null;
628   }
629   return items[0];
633   * Selection change event handler.
634   * @private
635   */
636 Gallery.prototype.onSelection_ = function() {
637   this.updateSelectionAndState_();
641   * Data model splice event handler.
642   * @private
643   */
644 Gallery.prototype.onSplice_ = function() {
645   this.selectionModel_.adjustLength(this.dataModel_.length);
646   this.selectionModel_.selectedIndexes =
647       this.selectionModel_.selectedIndexes.filter(function(index) {
648     return 0 <= index && index < this.dataModel_.length;
649   }.bind(this));
651   // Disable mode switch button if there is no image.
652   this.modeSwitchButton_.disabled = this.dataModel_.length === 0;
656  * Content change event handler.
657  * @param {!Event} event Event.
658  * @private
660 Gallery.prototype.onContentChange_ = function(event) {
661   this.updateSelectionAndState_();
665  * Keydown handler.
667  * @param {!Event} event
668  * @private
669  */
670 Gallery.prototype.onKeyDown_ = function(event) {
671   var keyString = util.getKeyModifiers(event) + event.keyIdentifier;
673   // Handle debug shortcut keys.
674   switch (keyString) {
675     case 'Ctrl-Shift-U+0049': // Ctrl+Shift+I
676       chrome.fileManagerPrivate.openInspector('normal');
677       break;
678     case 'Ctrl-Shift-U+004A': // Ctrl+Shift+J
679       chrome.fileManagerPrivate.openInspector('console');
680       break;
681     case 'Ctrl-Shift-U+0043': // Ctrl+Shift+C
682       chrome.fileManagerPrivate.openInspector('element');
683       break;
684     case 'Ctrl-Shift-U+0042': // Ctrl+Shift+B
685       chrome.fileManagerPrivate.openInspector('background');
686       break;
687   }
689   // Do not capture keys when share dialog is shown.
690   if (this.shareDialog_.isShowing())
691     return;
693   // Show UIs when user types any key.
694   this.dimmableUIController_.kick();
696   // Handle mode specific shortcut keys.
697   if (this.currentMode_.onKeyDown(event)) {
698     event.preventDefault();
699     return;
700   }
702   // Handle application wide shortcut keys.
703   switch (keyString) {
704     case 'U+0008': // Backspace.
705       // The default handler would call history.back and close the Gallery.
706       event.preventDefault();
707       break;
709     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
710       if (!this.modeSwitchButton_.disabled)
711         this.toggleMode_(undefined, event);
712       break;
714     case 'U+0056':  // 'v'
715     case 'MediaPlayPause':
716       if (!this.slideshowButton_.disabled) {
717         this.slideMode_.startSlideshow(
718             SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
719       }
720       break;
722     case 'U+007F':  // Delete
723     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
724     case 'U+0044':  // 'd'
725       if (!this.deleteButton_.disabled)
726         this.delete_();
727       break;
729     case 'U+001B':  // Escape
730       window.close();
731       break;
732   }
735 // Name box and rename support.
738  * Updates the UI related to the selected item and the persistent state.
740  * @private
741  */
742 Gallery.prototype.updateSelectionAndState_ = function() {
743   var numSelectedItems = this.selectionModel_.selectedIndexes.length;
744   var selectedEntryURL = null;
746   // If it's selecting something, update the variable values.
747   if (numSelectedItems) {
748     // Enable slideshow button.
749     this.slideshowButton_.disabled = false;
751     // Delete button is available when all images are NOT readOnly.
752     this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes
753         .every(function(i) {
754           return !this.dataModel_.item(i).getLocationInfo().isReadOnly;
755         }, this);
757     // Obtains selected item.
758     var selectedItem =
759         this.dataModel_.item(this.selectionModel_.selectedIndex);
760     this.selectedEntry_ = selectedItem.getEntry();
761     selectedEntryURL = this.selectedEntry_.toURL();
763     // Update cache.
764     selectedItem.touch();
765     this.dataModel_.evictCache();
767     // Update the title and the display name.
768     if (numSelectedItems === 1) {
769       document.title = this.selectedEntry_.name;
770       this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
771       this.filenameEdit_.value =
772           ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
773       this.resizeRenameField_();
775       this.shareButton_.disabled = !selectedItem.getLocationInfo().isDriveBased;
776     } else {
777       if (this.context_.curDirEntry) {
778         // If the Gallery was opened on search results the search query will not
779         // be recorded in the app state and the relaunch will just open the
780         // gallery in the curDirEntry directory.
781         document.title = this.context_.curDirEntry.name;
782       } else {
783         document.title = '';
784       }
785       this.filenameEdit_.disabled = true;
786       this.filenameEdit_.value =
787           strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
788       this.resizeRenameField_();
790       this.shareButton_.disabled = true;
791     }
792   } else {
793     document.title = '';
794     this.filenameEdit_.disabled = true;
795     this.filenameEdit_.value = '';
796     this.resizeRenameField_();
798     this.deleteButton_.disabled = true;
799     this.slideshowButton_.disabled = true;
800     this.shareButton_.disabled = true;
801   }
803   util.updateAppState(
804       null,  // Keep the current directory.
805       selectedEntryURL,  // Update the selection.
806       {
807         gallery: (this.currentMode_ === this.thumbnailMode_ ?
808                   'thumbnail' : 'slide')
809       });
813  * Click event handler on filename edit box
814  * @private
815  */
816 Gallery.prototype.onFilenameFocus_ = function() {
817   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
818   this.filenameEdit_.originalValue = this.filenameEdit_.value;
819   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
820   this.onUserAction_();
824  * Blur event handler on filename edit box.
826  * @param {!Event} event Blur event.
827  * @private
828  */
829 Gallery.prototype.onFilenameEditBlur_ = function(event) {
830   var item = this.getSingleSelectedItem();
831   if (item) {
832     var oldEntry = item.getEntry();
834     item.rename(this.filenameEdit_.value).then(function() {
835       var event = new Event('content');
836       event.item = item;
837       event.oldEntry = oldEntry;
838       event.thumbnailChanged = false;
839       this.dataModel_.dispatchEvent(event);
840     }.bind(this), function(error) {
841       if (error === 'NOT_CHANGED')
842         return Promise.resolve();
843       this.filenameEdit_.value =
844           ImageUtil.getDisplayNameFromName(item.getEntry().name);
845       this.resizeRenameField_();
846       this.filenameEdit_.focus();
847       if (typeof error === 'string')
848         this.prompt_.showStringAt('center', error, 5000);
849       else
850         return Promise.reject(error);
851     }.bind(this)).catch(function(error) {
852       console.error(error.stack || error);
853     });
854   }
856   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
857   this.onUserAction_();
861  * Minimum width of rename field.
862  * @const {number}
863  */
864 Gallery.MIN_WIDTH_RENAME_FIELD = 160; // px
867  * End padding for rename field.
868  * @const {number}
869  */
870 Gallery.END_PADDING_RENAME_FIELD = 20; // px
873  * Resize rename field depending on its content.
874  * @private
875  */
876 Gallery.prototype.resizeRenameField_ = function() {
877   var size = this.filenameCanvasContext_.measureText(this.filenameEdit_.value);
879   var width = Math.min(Math.max(
880       size.width + Gallery.END_PADDING_RENAME_FIELD,
881       Gallery.MIN_WIDTH_RENAME_FIELD), window.innerWidth / 2);
883   this.filenameEdit_.style.width = width + 'px';
887  * Keydown event handler on filename edit box
888  * @param {!Event} event A keyboard event.
889  * @private
890  */
891 Gallery.prototype.onFilenameEditKeydown_ = function(event) {
892   event = assertInstanceof(event, KeyboardEvent);
893   switch (event.keyCode) {
894     case 27:  // Escape
895       this.filenameEdit_.value = this.filenameEdit_.originalValue;
896       this.resizeRenameField_();
897       this.filenameEdit_.blur();
898       break;
900     case 13:  // Enter
901       this.filenameEdit_.blur();
902       break;
903   }
904   event.stopPropagation();
908  * @return {boolean} True if file renaming is currently in progress.
909  * @private
910  */
911 Gallery.prototype.isRenaming_ = function() {
912   return this.filenameSpacer_.hasAttribute('renaming');
916  * Content area click handler.
917  * @private
918  */
919 Gallery.prototype.onContentClick_ = function() {
920   this.filenameEdit_.blur();
924  * Share button handler.
925  * @private
926  */
927 Gallery.prototype.onShareButtonClick_ = function() {
928   var item = this.getSingleSelectedItem();
929   if (!item)
930     return;
931   this.shareDialog_.showEntry(item.getEntry(), function() {});
935  * Updates thumbnails.
936  * @private
937  */
938 Gallery.prototype.updateThumbnails_ = function() {
939   if (this.currentMode_ === this.slideMode_)
940     this.slideMode_.updateThumbnails();
944  * Singleton gallery.
945  * @type {Gallery}
946  */
947 var gallery = null;
950  * (Re-)loads entries.
951  */
952 function reload() {
953   initializePromise.then(function() {
954     util.URLsToEntries(window.appState.urls, function(entries) {
955       gallery.load(entries);
956     });
957   });
961  * Promise to initialize the load time data.
962  * @type {!Promise}
963  */
964 var loadTimeDataPromise = new Promise(function(fulfill, reject) {
965   chrome.fileManagerPrivate.getStrings(function(strings) {
966     window.loadTimeData.data = strings;
967     i18nTemplate.process(document, loadTimeData);
968     fulfill(true);
969   });
973  * Promise to initialize volume manager.
974  * @type {!Promise}
975  */
976 var volumeManagerPromise = new Promise(function(fulfill, reject) {
977   var volumeManager = new VolumeManagerWrapper(
978       VolumeManagerWrapper.NonNativeVolumeStatus.ENABLED);
979   volumeManager.ensureInitialized(fulfill.bind(null, volumeManager));
983  * Promise to initialize both the volume manager and the load time data.
984  * @type {!Promise}
985  */
986 var initializePromise =
987     Promise.all([loadTimeDataPromise, volumeManagerPromise]).
988     then(function(args) {
989       var volumeManager = args[1];
990       gallery = new Gallery(volumeManager);
991     });
993 // Loads entries.
994 initializePromise.then(reload);