Move action_runner.py out of actions folder prior to moving actions to internal.
[chromium-blink-merge.git] / ui / file_manager / gallery / js / gallery.js
blob545d867943401bc1bca70427463177406e94e1d3
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, onClose: function(),
21    *     onMaximize: function(), onMinimize: function(),
22    *     onAppRegionChanged: function(), readonlyDirName: string,
23    *     displayStringFunction: function(), loadTimeData: Object,
24    *     curDirEntry: Entry, searchResults: *}}
25    * @private
26    *
27    * TODO(yawano): curDirEntry and searchResults seem not to be used.
28    *     Investigate them and remove them if possible.
29    */
30   this.context_ = {
31     appWindow: chrome.app.window.current(),
32     onClose: function() { window.close(); },
33     onMaximize: function() {
34       var appWindow = chrome.app.window.current();
35       if (appWindow.isMaximized())
36         appWindow.restore();
37       else
38         appWindow.maximize();
39     },
40     onMinimize: function() { chrome.app.window.current().minimize(); },
41     onAppRegionChanged: function() {},
42     readonlyDirName: '',
43     displayStringFunction: function() { return ''; },
44     loadTimeData: {},
45     curDirEntry: null,
46     searchResults: null
47   };
48   this.container_ = queryRequiredElement(document, '.gallery');
49   this.document_ = document;
50   this.volumeManager_ = volumeManager;
51   /**
52    * @private {!MetadataModel}
53    * @const
54    */
55   this.metadataModel_ = MetadataModel.create(volumeManager);
56   /**
57    * @private {!ThumbnailModel}
58    * @const
59    */
60   this.thumbnailModel_ = new ThumbnailModel(this.metadataModel_);
61   this.selectedEntry_ = null;
62   this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
63   this.initialized_ = false;
65   this.dataModel_ = new GalleryDataModel(this.metadataModel_);
66   var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
67       VolumeManagerCommon.VolumeType.DOWNLOADS);
68   downloadVolumeInfo.resolveDisplayRoot().then(function(entry) {
69     this.dataModel_.fallbackSaveDirectory = entry;
70   }.bind(this)).catch(function(error) {
71     console.error(
72         'Failed to obtain the fallback directory: ' + (error.stack || error));
73   });
74   this.selectionModel_ = new cr.ui.ListSelectionModel();
76   /**
77    * @type {(SlideMode|MosaicMode)}
78    * @private
79    */
80   this.currentMode_ = null;
82   /**
83    * @type {boolean}
84    * @private
85    */
86   this.changingMode_ = false;
88   // -----------------------------------------------------------------
89   // Initializes the UI.
91   // Initialize the dialog label.
92   cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
93   cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
95   var content = queryRequiredElement(document, '#content');
96   content.addEventListener('click', this.onContentClick_.bind(this));
98   this.header_ = queryRequiredElement(document, '#header');
99   this.toolbar_ = queryRequiredElement(document, '#toolbar');
101   var preventDefault = function(event) { event.preventDefault(); };
103   var minimizeButton = util.createChild(this.header_,
104                                         'minimize-button tool dimmable',
105                                         'button');
106   minimizeButton.tabIndex = -1;
107   minimizeButton.addEventListener('click', this.onMinimize_.bind(this));
108   minimizeButton.addEventListener('mousedown', preventDefault);
110   var maximizeButton = util.createChild(this.header_,
111                                         'maximize-button tool dimmable',
112                                         'button');
113   maximizeButton.tabIndex = -1;
114   maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
115   maximizeButton.addEventListener('mousedown', preventDefault);
117   var closeButton = util.createChild(this.header_,
118                                      'close-button tool dimmable',
119                                      'button');
120   closeButton.tabIndex = -1;
121   closeButton.addEventListener('click', this.onClose_.bind(this));
122   closeButton.addEventListener('mousedown', preventDefault);
124   this.filenameSpacer_ = queryRequiredElement(this.toolbar_,
125       '.filename-spacer');
126   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
127                                         'namebox', 'input');
129   this.filenameEdit_.setAttribute('type', 'text');
130   this.filenameEdit_.addEventListener('blur',
131       this.onFilenameEditBlur_.bind(this));
133   this.filenameEdit_.addEventListener('focus',
134       this.onFilenameFocus_.bind(this));
136   this.filenameEdit_.addEventListener('keydown',
137       this.onFilenameEditKeydown_.bind(this));
139   var middleSpacer = queryRequiredElement(this.toolbar_, '.middle-spacer');
140   var buttonSpacer = queryRequiredElement(this.toolbar_, '.button-spacer');
142   this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
144   this.errorBanner_ = new ErrorBanner(this.container_);
146   this.modeButton_ = queryRequiredElement(this.toolbar_, 'button.mode');
147   this.modeButton_.addEventListener('click',
148       this.toggleMode_.bind(this, undefined));
150   this.mosaicMode_ = new MosaicMode(content,
151                                     this.errorBanner_,
152                                     this.dataModel_,
153                                     this.selectionModel_,
154                                     this.volumeManager_,
155                                     this.toggleMode_.bind(this, undefined));
157   this.slideMode_ = new SlideMode(this.container_,
158                                   content,
159                                   this.toolbar_,
160                                   this.prompt_,
161                                   this.errorBanner_,
162                                   this.dataModel_,
163                                   this.selectionModel_,
164                                   this.metadataModel_,
165                                   this.thumbnailModel_,
166                                   this.context_,
167                                   this.volumeManager_,
168                                   this.toggleMode_.bind(this),
169                                   str);
171   this.slideMode_.addEventListener('image-displayed', function() {
172     cr.dispatchSimpleEvent(this, 'image-displayed');
173   }.bind(this));
175   this.deleteButton_ = this.initToolbarButton_('delete', 'GALLERY_DELETE');
176   this.deleteButton_.addEventListener('click', this.delete_.bind(this));
178   this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
179   this.shareButton_.addEventListener(
180       'click', this.onShareButtonClick_.bind(this));
182   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
183   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
185   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
186   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
188   this.shareDialog_ = new ShareDialog(this.container_);
190   // -----------------------------------------------------------------
191   // Initialize listeners.
193   this.keyDownBound_ = this.onKeyDown_.bind(this);
194   this.document_.body.addEventListener('keydown', this.keyDownBound_);
196   this.inactivityWatcher_ = new MouseInactivityWatcher(
197       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
199   // TODO(hirono): Add observer to handle thumbnail update.
200   this.volumeManager_.addEventListener(
201       'externally-unmounted', this.onExternallyUnmountedBound_);
202   // The 'pagehide' event is called when the app window is closed.
203   window.addEventListener('pagehide', this.onPageHide_.bind(this));
207  * Gallery extends cr.EventTarget.
208  */
209 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
212  * Tools fade-out timeout in milliseconds.
213  * @const
214  * @type {number}
215  */
216 Gallery.FADE_TIMEOUT = 2000;
219  * First time tools fade-out timeout in milliseconds.
220  * @const
221  * @type {number}
222  */
223 Gallery.FIRST_FADE_TIMEOUT = 1000;
226  * Time until mosaic is initialized in the background. Used to make gallery
227  * in the slide mode load faster. In milliseconds.
228  * @const
229  * @type {number}
230  */
231 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
234  * Types of metadata Gallery uses (to query the metadata cache).
235  * @const
236  * @type {!Array<string>}
237  */
238 Gallery.PREFETCH_PROPERTY_NAMES =
239     ['imageWidth', 'imageHeight', 'size', 'present'];
242  * Closes gallery when a volume containing the selected item is unmounted.
243  * @param {!Event} event The unmount event.
244  * @private
245  */
246 Gallery.prototype.onExternallyUnmounted_ = function(event) {
247   if (!this.selectedEntry_)
248     return;
250   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
251       event.volumeInfo) {
252     window.close();
253   }
257  * Unloads the Gallery.
258  * @private
259  */
260 Gallery.prototype.onPageHide_ = function() {
261   this.volumeManager_.removeEventListener(
262       'externally-unmounted', this.onExternallyUnmountedBound_);
263   this.volumeManager_.dispose();
267  * Initializes a toolbar button.
269  * @param {string} className Class to add.
270  * @param {string} title Button title.
271  * @return {!HTMLElement} Newly created button.
272  * @private
273  */
274 Gallery.prototype.initToolbarButton_ = function(className, title) {
275   var button = queryRequiredElement(this.toolbar_, 'button.' + className);
276   button.title = str(title);
277   return button;
281  * Loads the content.
283  * @param {!Array.<!Entry>} selectedEntries Array of selected entries.
284  */
285 Gallery.prototype.load = function(selectedEntries) {
286   GalleryUtil.createEntrySet(selectedEntries).then(function(allEntries) {
287     this.loadInternal_(allEntries, selectedEntries);
288   }.bind(this));
292  * Loads the content.
294  * @param {!Array.<!Entry>} entries Array of entries.
295  * @param {!Array.<!Entry>} selectedEntries Array of selected entries.
296  * @private
297  */
298 Gallery.prototype.loadInternal_ = function(entries, selectedEntries) {
299   // Obtains max chank size.
300   var maxChunkSize = 20;
301   var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
302   if (volumeInfo) {
303     if (volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP)
304       maxChunkSize = 1;
305     if (volumeInfo.isReadOnly)
306       this.context_.readonlyDirName = volumeInfo.label;
307   }
309   // Make loading list.
310   var entrySet = {};
311   for (var i = 0; i < entries.length; i++) {
312     var entry = entries[i];
313     entrySet[entry.toURL()] = {
314       entry: entry,
315       selected: false,
316       index: i
317     };
318   }
319   for (var i = 0; i < selectedEntries.length; i++) {
320     var entry = selectedEntries[i];
321     entrySet[entry.toURL()] = {
322       entry: entry,
323       selected: true,
324       index: i
325     };
326   }
327   var loadingList = [];
328   for (var url in entrySet) {
329     loadingList.push(entrySet[url]);
330   }
331   loadingList = loadingList.sort(function(a, b) {
332     if (a.selected && !b.selected)
333       return -1;
334     else if (!a.selected && b.selected)
335       return 1;
336     else
337       return a.index - b.index;
338   });
340   if (loadingList.length === 0) {
341     this.dataModel_.splice(0, this.dataModel_.length);
342     return;
343   }
345   // Load entries.
346   // Use the self variable capture-by-closure because it is faster than bind.
347   var self = this;
348   var thumbnailModel = new ThumbnailModel(this.metadataModel_);
349   var loadChunk = function(firstChunk) {
350     // Extract chunk.
351     var chunk = loadingList.splice(0, maxChunkSize);
352     if (!chunk.length)
353       return;
354     var entries = chunk.map(function(chunkItem) {
355       return chunkItem.entry;
356     });
357     var metadataPromise = self.metadataModel_.get(
358         entries, Gallery.PREFETCH_PROPERTY_NAMES);
359     var thumbnailPromise = thumbnailModel.get(entries);
360     return Promise.all([metadataPromise, thumbnailPromise]).then(
361         function(metadataLists) {
362       // Remove all the previous items if it's the first chunk.
363       // Do it here because prevent a flicker between removing all the items
364       // and adding new ones.
365       if (firstChunk) {
366         self.dataModel_.splice(0, self.dataModel_.length);
367         self.updateThumbnails_();  // Remove the caches.
368       }
370       // Add items to the model.
371       var items = [];
372       chunk.forEach(function(chunkItem, index) {
373         var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
374         if (!locationInfo)  // Skip the item, since gone.
375           return;
376         items.push(new Gallery.Item(
377             chunkItem.entry,
378             locationInfo,
379             metadataLists[0][index],
380             metadataLists[1][index],
381             /* original */ true));
382       });
383       self.dataModel_.push.apply(self.dataModel_, items);
385       // Apply the selection.
386       var selectionUpdated = false;
387       for (var i = 0; i < chunk.length; i++) {
388         if (!chunk[i].selected)
389           continue;
390         var index = self.dataModel_.indexOf(items[i]);
391         if (index < 0)
392           continue;
393         self.selectionModel_.setIndexSelected(index, true);
394         selectionUpdated = true;
395       }
396       if (selectionUpdated)
397         self.onSelection_();
399       // Init modes after the first chunk is loaded.
400       if (firstChunk && !self.initialized_) {
401         // Determine the initial mode.
402         var shouldShowMosaic = selectedEntries.length > 1 ||
403             (self.context_.pageState &&
404              self.context_.pageState.gallery === 'mosaic');
405         self.setCurrentMode_(
406             shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
408         // Init mosaic mode.
409         var mosaic = self.mosaicMode_.getMosaic();
410         mosaic.init();
412         // Do the initialization for each mode.
413         if (shouldShowMosaic) {
414           mosaic.show();
415           self.inactivityWatcher_.check();  // Show the toolbar.
416           cr.dispatchSimpleEvent(self, 'loaded');
417         } else {
418           self.slideMode_.enter(
419               null,
420               function() {
421                 // Flash the toolbar briefly to show it is there.
422                 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
423               },
424               function() {
425                 cr.dispatchSimpleEvent(self, 'loaded');
426               });
427         }
428         self.initialized_ = true;
429       }
431       // Continue to load chunks.
432       return loadChunk(/* firstChunk */ false);
433     });
434   };
435   loadChunk(/* firstChunk */ true).catch(function(error) {
436     console.error(error.stack || error);
437   });
441  * Handles user's 'Close' action.
442  * @private
443  */
444 Gallery.prototype.onClose_ = function() {
445   this.executeWhenReady(this.context_.onClose);
449  * Handles user's 'Maximize' action (Escape or a click on the X icon).
450  * @private
451  */
452 Gallery.prototype.onMaximize_ = function() {
453   this.executeWhenReady(this.context_.onMaximize);
457  * Handles user's 'Maximize' action (Escape or a click on the X icon).
458  * @private
459  */
460 Gallery.prototype.onMinimize_ = function() {
461   this.executeWhenReady(this.context_.onMinimize);
465  * Executes a function when the editor is done with the modifications.
466  * @param {function()} callback Function to execute.
467  */
468 Gallery.prototype.executeWhenReady = function(callback) {
469   this.currentMode_.executeWhenReady(callback);
473  * @return {!Object} File manager private API.
474  */
475 Gallery.getFileManagerPrivate = function() {
476   return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
480  * @return {boolean} True if some tool is currently active.
481  */
482 Gallery.prototype.hasActiveTool = function() {
483   return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
484       this.isRenaming_();
488 * External user action event handler.
489 * @private
491 Gallery.prototype.onUserAction_ = function() {
492   // Show the toolbar and hide it after the default timeout.
493   this.inactivityWatcher_.kick();
497  * Sets the current mode, update the UI.
498  * @param {!(SlideMode|MosaicMode)} mode Current mode.
499  * @private
500  */
501 Gallery.prototype.setCurrentMode_ = function(mode) {
502   if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
503     console.error('Invalid Gallery mode');
505   this.currentMode_ = mode;
506   this.container_.setAttribute('mode', this.currentMode_.getName());
507   this.updateSelectionAndState_();
508   this.updateButtons_();
512  * Mode toggle event handler.
513  * @param {function()=} opt_callback Callback.
514  * @param {Event=} opt_event Event that caused this call.
515  * @private
516  */
517 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
518   if (!this.modeButton_)
519     return;
521   if (this.changingMode_) // Do not re-enter while changing the mode.
522     return;
524   if (opt_event)
525     this.onUserAction_();
527   this.changingMode_ = true;
529   var onModeChanged = function() {
530     this.changingMode_ = false;
531     if (opt_callback) opt_callback();
532   }.bind(this);
534   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
536   var mosaic = this.mosaicMode_.getMosaic();
537   var tileRect = mosaic.getTileRect(tileIndex);
539   if (this.currentMode_ === this.slideMode_) {
540     this.setCurrentMode_(this.mosaicMode_);
541     mosaic.transform(
542         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
543     this.slideMode_.leave(
544         tileRect,
545         function() {
546           // Animate back to normal position.
547           mosaic.transform(null, null);
548           mosaic.show();
549           onModeChanged();
550         }.bind(this));
551   } else {
552     this.setCurrentMode_(this.slideMode_);
553     this.slideMode_.enter(
554         tileRect,
555         function() {
556           // Animate to zoomed position.
557           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
558           mosaic.hide();
559         }.bind(this),
560         onModeChanged);
561   }
565  * Deletes the selected items.
566  * @private
567  */
568 Gallery.prototype.delete_ = function() {
569   this.onUserAction_();
571   // Clone the sorted selected indexes array.
572   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
573   if (!indexesToRemove.length)
574     return;
576   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
578   var itemsToRemove = this.getSelectedItems();
579   var plural = itemsToRemove.length > 1;
580   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
582   function deleteNext() {
583     if (!itemsToRemove.length)
584       return;  // All deleted.
586     var entry = itemsToRemove.pop().getEntry();
587     entry.remove(deleteNext, function() {
588       console.error('Error deleting: ' + entry.name);
589       deleteNext();
590     });
591   }
593   // Prevent the Gallery from handling Esc and Enter.
594   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
595   var restoreListener = function() {
596     this.document_.body.addEventListener('keydown', this.keyDownBound_);
597   }.bind(this);
600   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
601   confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
602   confirm.show(strf(plural ?
603       'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
604       function() {
605         restoreListener();
606         this.selectionModel_.unselectAll();
607         this.selectionModel_.leadIndex = -1;
608         // Remove items from the data model, starting from the highest index.
609         while (indexesToRemove.length)
610           this.dataModel_.splice(indexesToRemove.pop(), 1);
611         // Delete actual files.
612         deleteNext();
613       }.bind(this),
614       function() {
615         // Restore the listener after a timeout so that ESC is processed.
616         setTimeout(restoreListener, 0);
617       },
618       null);
622  * @return {!Array.<Gallery.Item>} Current selection.
623  */
624 Gallery.prototype.getSelectedItems = function() {
625   return this.selectionModel_.selectedIndexes.map(
626       this.dataModel_.item.bind(this.dataModel_));
630  * @return {!Array.<Entry>} Array of currently selected entries.
631  */
632 Gallery.prototype.getSelectedEntries = function() {
633   return this.selectionModel_.selectedIndexes.map(function(index) {
634     return this.dataModel_.item(index).getEntry();
635   }.bind(this));
639  * @return {?Gallery.Item} Current single selection.
640  */
641 Gallery.prototype.getSingleSelectedItem = function() {
642   var items = this.getSelectedItems();
643   if (items.length > 1) {
644     console.error('Unexpected multiple selection');
645     return null;
646   }
647   return items[0];
651   * Selection change event handler.
652   * @private
653   */
654 Gallery.prototype.onSelection_ = function() {
655   this.updateSelectionAndState_();
659   * Data model splice event handler.
660   * @private
661   */
662 Gallery.prototype.onSplice_ = function() {
663   this.selectionModel_.adjustLength(this.dataModel_.length);
664   this.selectionModel_.selectedIndexes =
665       this.selectionModel_.selectedIndexes.filter(function(index) {
666     return 0 <= index && index < this.dataModel_.length;
667   }.bind(this));
671  * Content change event handler.
672  * @param {!Event} event Event.
673  * @private
675 Gallery.prototype.onContentChange_ = function(event) {
676   var index = this.dataModel_.indexOf(event.item);
677   if (index !== this.selectionModel_.selectedIndex)
678     console.error('Content changed for unselected item');
679   this.updateSelectionAndState_();
683  * Keydown handler.
685  * @param {!Event} event Event.
686  * @private
687  */
688 Gallery.prototype.onKeyDown_ = function(event) {
689   if (this.currentMode_.onKeyDown(event))
690     return;
692   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
693     case 'U+0008': // Backspace.
694       // The default handler would call history.back and close the Gallery.
695       event.preventDefault();
696       break;
698     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
699       this.toggleMode_(undefined, event);
700       break;
702     case 'U+0056':  // 'v'
703     case 'MediaPlayPause':
704       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
705       break;
707     case 'U+007F':  // Delete
708     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
709     case 'U+0044':  // 'd'
710       this.delete_();
711       break;
713     case 'U+001B':  // Escape
714       window.close();
715       break;
716   }
719 // Name box and rename support.
722  * Updates the UI related to the selected item and the persistent state.
724  * @private
725  */
726 Gallery.prototype.updateSelectionAndState_ = function() {
727   var numSelectedItems = this.selectionModel_.selectedIndexes.length;
728   var selectedEntryURL = null;
730   // If it's selecting something, update the variable values.
731   if (numSelectedItems) {
732     // Delete button is available when all images are NOT readOnly.
733     this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes
734         .every(function(i) {
735           return !this.dataModel_.item(i).getLocationInfo().isReadOnly;
736         }, this);
738     // Obtains selected item.
739     var selectedItem =
740         this.dataModel_.item(this.selectionModel_.selectedIndex);
741     this.selectedEntry_ = selectedItem.getEntry();
742     selectedEntryURL = this.selectedEntry_.toURL();
744     // Update cache.
745     selectedItem.touch();
746     this.dataModel_.evictCache();
748     // Update the title and the display name.
749     if (numSelectedItems === 1) {
750       document.title = this.selectedEntry_.name;
751       this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
752       this.filenameEdit_.value =
753           ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
754       this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
755     } else {
756       if (this.context_.curDirEntry) {
757         // If the Gallery was opened on search results the search query will not
758         // be recorded in the app state and the relaunch will just open the
759         // gallery in the curDirEntry directory.
760         document.title = this.context_.curDirEntry.name;
761       } else {
762         document.title = '';
763       }
764       this.filenameEdit_.disabled = true;
765       this.filenameEdit_.value =
766           strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
767       this.shareButton_.hidden = true;
768     }
769   } else {
770     document.title = '';
771     this.filenameEdit_.disabled = true;
772     this.deleteButton_.disabled = true;
773     this.filenameEdit_.value = '';
774     this.shareButton_.hidden = true;
775   }
777   util.updateAppState(
778       null,  // Keep the current directory.
779       selectedEntryURL,  // Update the selection.
780       {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
784  * Click event handler on filename edit box
785  * @private
786  */
787 Gallery.prototype.onFilenameFocus_ = function() {
788   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
789   this.filenameEdit_.originalValue = this.filenameEdit_.value;
790   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
791   this.onUserAction_();
795  * Blur event handler on filename edit box.
797  * @param {!Event} event Blur event.
798  * @private
799  */
800 Gallery.prototype.onFilenameEditBlur_ = function(event) {
801   var item = this.getSingleSelectedItem();
802   if (item) {
803     var oldEntry = item.getEntry();
805     item.rename(this.filenameEdit_.value).then(function() {
806       var event = new Event('content');
807       event.item = item;
808       event.oldEntry = oldEntry;
809       event.thumbnailChanged = false;
810       this.dataModel_.dispatchEvent(event);
811     }.bind(this), function(error) {
812       if (error === 'NOT_CHANGED')
813         return Promise.resolve();
814       this.filenameEdit_.value =
815           ImageUtil.getDisplayNameFromName(item.getEntry().name);
816       this.filenameEdit_.focus();
817       if (typeof error === 'string')
818         this.prompt_.showStringAt('center', error, 5000);
819       else
820         return Promise.reject(error);
821     }.bind(this)).catch(function(error) {
822       console.error(error.stack || error);
823     });
824   }
826   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
827   this.onUserAction_();
831  * Keydown event handler on filename edit box
832  * @param {!Event} event A keyboard event.
833  * @private
834  */
835 Gallery.prototype.onFilenameEditKeydown_ = function(event) {
836   event = assertInstanceof(event, KeyboardEvent);
837   switch (event.keyCode) {
838     case 27:  // Escape
839       this.filenameEdit_.value = this.filenameEdit_.originalValue;
840       this.filenameEdit_.blur();
841       break;
843     case 13:  // Enter
844       this.filenameEdit_.blur();
845       break;
846   }
847   event.stopPropagation();
851  * @return {boolean} True if file renaming is currently in progress.
852  * @private
853  */
854 Gallery.prototype.isRenaming_ = function() {
855   return this.filenameSpacer_.hasAttribute('renaming');
859  * Content area click handler.
860  * @private
861  */
862 Gallery.prototype.onContentClick_ = function() {
863   this.filenameEdit_.blur();
867  * Share button handler.
868  * @private
869  */
870 Gallery.prototype.onShareButtonClick_ = function() {
871   var item = this.getSingleSelectedItem();
872   if (!item)
873     return;
874   this.shareDialog_.showEntry(item.getEntry(), function() {});
878  * Updates thumbnails.
879  * @private
880  */
881 Gallery.prototype.updateThumbnails_ = function() {
882   if (this.currentMode_ === this.slideMode_)
883     this.slideMode_.updateThumbnails();
885   if (this.mosaicMode_) {
886     var mosaic = this.mosaicMode_.getMosaic();
887     if (mosaic.isInitialized())
888       mosaic.reload();
889   }
893  * Updates buttons.
894  * @private
895  */
896 Gallery.prototype.updateButtons_ = function() {
897   if (this.modeButton_) {
898     var oppositeMode =
899         this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
900                                                 this.slideMode_;
901     this.modeButton_.title = str(oppositeMode.getTitle());
902   }
906  * Enters the debug mode.
907  */
908 Gallery.prototype.debugMe = function() {
909   this.mosaicMode_.debugMe();
913  * Singleton gallery.
914  * @type {Gallery}
915  */
916 var gallery = null;
919  * (Re-)loads entries.
920  */
921 function reload() {
922   initializePromise.then(function() {
923     util.URLsToEntries(window.appState.urls, function(entries) {
924       gallery.load(entries);
925     });
926   });
930  * Promise to initialize the load time data.
931  * @type {!Promise}
932  */
933 var loadTimeDataPromise = new Promise(function(fulfill, reject) {
934   chrome.fileManagerPrivate.getStrings(function(strings) {
935     window.loadTimeData.data = strings;
936     i18nTemplate.process(document, loadTimeData);
937     fulfill(true);
938   });
942  * Promise to initialize volume manager.
943  * @type {!Promise}
944  */
945 var volumeManagerPromise = new Promise(function(fulfill, reject) {
946   var volumeManager = new VolumeManagerWrapper(
947       VolumeManagerWrapper.NonNativeVolumeStatus.ENABLED);
948   volumeManager.ensureInitialized(fulfill.bind(null, volumeManager));
952  * Promise to initialize both the volume manager and the load time data.
953  * @type {!Promise}
954  */
955 var initializePromise =
956     Promise.all([loadTimeDataPromise, volumeManagerPromise]).
957     then(function(args) {
958       var volumeManager = args[1];
959       gallery = new Gallery(volumeManager);
960     });
962 // Loads entries.
963 initializePromise.then(reload);
966  * Enteres the debug mode.
967  */
968 window.debugMe = function() {
969   initializePromise.then(function() {
970     gallery.debugMe();
971   });