Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / foreground / js / photo / gallery.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * Called from the main frame when unloading.
9  * @param {boolean=} opt_exiting True if the app is exiting.
10  */
11 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting) }
12
13 /**
14  * Gallery for viewing and editing image files.
15  *
16  * @param {Object} context Object containing the following:
17  *     {function(string)} onNameChange Called every time a selected
18  *         item name changes (on rename and on selection change).
19  *     {AppWindow} appWindow
20  *     {function(string)} onBack
21  *     {function()} onClose
22  *     {function()} onMaximize
23  *     {function()} onMinimize
24  *     {function(boolean)} onAppRegionChanged
25  *     {MetadataCache} metadataCache
26  *     {Array.<Object>} shareActions
27  *     {string} readonlyDirName Directory name for readonly warning or null.
28  *     {DirEntry} saveDirEntry Directory to save to.
29  *     {function(string)} displayStringFunction.
30  * @param {VolumeManagerWrapper} volumeManager The VolumeManager instance of
31  *      the system.
32  * @class
33  * @constructor
34  */
35 function Gallery(context, volumeManager) {
36   this.container_ = document.querySelector('.gallery');
37   this.document_ = document;
38   this.context_ = context;
39   this.metadataCache_ = context.metadataCache;
40   this.volumeManager_ = volumeManager;
41   this.selectedEntry_ = null;
42   this.metadataCacheObserverId_ = null;
43   this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
44
45   this.dataModel_ = new cr.ui.ArrayDataModel([]);
46   this.selectionModel_ = new cr.ui.ListSelectionModel();
47   loadTimeData.data = context.loadTimeData;
48
49   this.initDom_();
50   this.initListeners_();
51 }
52
53 /**
54  * Gallery extends cr.EventTarget.
55  */
56 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
57
58 /**
59  * Creates and initializes a Gallery object based on a context.
60  *
61  * @param {Object} context Gallery context.
62  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
63  * @param {Array.<Entry>} entries Array of entries.
64  * @param {Array.<Entry>} selectedEntries Array of selected entries.
65  */
66 Gallery.open = function(context, volumeManager, entries, selectedEntries) {
67   Gallery.instance = new Gallery(context, volumeManager);
68   Gallery.instance.load(entries, selectedEntries);
69 };
70
71 /**
72  * Tools fade-out timeout im milliseconds.
73  * @const
74  * @type {number}
75  */
76 Gallery.FADE_TIMEOUT = 3000;
77
78 /**
79  * First time tools fade-out timeout im milliseconds.
80  * @const
81  * @type {number}
82  */
83 Gallery.FIRST_FADE_TIMEOUT = 1000;
84
85 /**
86  * Time until mosaic is initialized in the background. Used to make gallery
87  * in the slide mode load faster. In miiliseconds.
88  * @const
89  * @type {number}
90  */
91 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
92
93 /**
94  * Types of metadata Gallery uses (to query the metadata cache).
95  * @const
96  * @type {string}
97  */
98 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming|drive';
99
100 /**
101  * Initializes listeners.
102  * @private
103  */
104 Gallery.prototype.initListeners_ = function() {
105   this.document_.oncontextmenu = function(e) { e.preventDefault(); };
106   this.keyDownBound_ = this.onKeyDown_.bind(this);
107   this.document_.body.addEventListener('keydown', this.keyDownBound_);
108
109   this.inactivityWatcher_ = new MouseInactivityWatcher(
110       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
111
112   // Search results may contain files from different subdirectories so
113   // the observer is not going to work.
114   if (!this.context_.searchResults && this.context_.curDirEntry) {
115     this.metadataCacheObserverId_ = this.metadataCache_.addObserver(
116         this.context_.curDirEntry,
117         MetadataCache.CHILDREN,
118         'thumbnail',
119         this.updateThumbnails_.bind(this));
120   }
121   this.volumeManager_.addEventListener(
122       'externally-unmounted', this.onExternallyUnmountedBound_);
123 };
124
125 /**
126  * Closes gallery when a volume containing the selected item is unmounted.
127  * @param {Event} event The unmount event.
128  * @private
129  */
130 Gallery.prototype.onExternallyUnmounted_ = function(event) {
131   if (!this.selectedEntry_)
132     return;
133
134   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
135       event.volumeInfo) {
136     this.onBack_();
137   }
138 };
139
140 /**
141  * Unloads the Gallery.
142  * @param {boolean} exiting True if the app is exiting.
143  */
144 Gallery.prototype.onUnload = function(exiting) {
145   if (this.metadataCacheObserverId_ !== null)
146     this.metadataCache_.removeObserver(this.metadataCacheObserverId_);
147   this.volumeManager_.removeEventListener(
148       'externally-unmounted', this.onExternallyUnmountedBound_);
149   this.slideMode_.onUnload(exiting);
150 };
151
152 /**
153  * Initializes DOM UI
154  * @private
155  */
156 Gallery.prototype.initDom_ = function() {
157   // Initialize the dialog label.
158   cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
159   cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
160
161   var content = util.createChild(this.container_, 'content');
162   content.addEventListener('click', this.onContentClick_.bind(this));
163
164   this.header_ = util.createChild(this.container_, 'header tool dimmable');
165   this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable');
166
167   var backButton = util.createChild(this.container_,
168                                     'back-button tool dimmable');
169   util.createChild(backButton);
170   backButton.addEventListener('click', this.onBack_.bind(this));
171
172   var preventDefault = function(event) { event.preventDefault(); };
173
174   var minimizeButton = util.createChild(this.header_,
175                                         'minimize-button tool dimmable',
176                                         'button');
177   minimizeButton.tabIndex = -1;
178   minimizeButton.addEventListener('click', this.onMinimize_.bind(this));
179   minimizeButton.addEventListener('mousedown', preventDefault);
180
181   var maximizeButton = util.createChild(this.header_,
182                                         'maximize-button tool dimmable',
183                                         'button');
184   maximizeButton.tabIndex = -1;
185   maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
186   maximizeButton.addEventListener('mousedown', preventDefault);
187
188   var closeButton = util.createChild(this.header_,
189                                      'close-button tool dimmable',
190                                      'button');
191   closeButton.tabIndex = -1;
192   closeButton.addEventListener('click', this.onClose_.bind(this));
193   closeButton.addEventListener('mousedown', preventDefault);
194
195   this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer');
196   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
197                                         'namebox', 'input');
198
199   this.filenameEdit_.setAttribute('type', 'text');
200   this.filenameEdit_.addEventListener('blur',
201       this.onFilenameEditBlur_.bind(this));
202
203   this.filenameEdit_.addEventListener('focus',
204       this.onFilenameFocus_.bind(this));
205
206   this.filenameEdit_.addEventListener('keydown',
207       this.onFilenameEditKeydown_.bind(this));
208
209   util.createChild(this.toolbar_, 'button-spacer');
210
211   this.prompt_ = new ImageEditor.Prompt(this.container_, str);
212
213   this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button');
214   this.modeButton_.addEventListener('click',
215       this.toggleMode_.bind(this, null));
216
217   this.mosaicMode_ = new MosaicMode(content,
218                                     this.dataModel_,
219                                     this.selectionModel_,
220                                     this.metadataCache_,
221                                     this.volumeManager_,
222                                     this.toggleMode_.bind(this, null));
223
224   this.slideMode_ = new SlideMode(this.container_,
225                                   content,
226                                   this.toolbar_,
227                                   this.prompt_,
228                                   this.dataModel_,
229                                   this.selectionModel_,
230                                   this.context_,
231                                   this.toggleMode_.bind(this),
232                                   str);
233
234   this.slideMode_.addEventListener('image-displayed', function() {
235     cr.dispatchSimpleEvent(this, 'image-displayed');
236   }.bind(this));
237   this.slideMode_.addEventListener('image-saved', function() {
238     cr.dispatchSimpleEvent(this, 'image-saved');
239   }.bind(this));
240
241   var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE');
242   deleteButton.addEventListener('click', this.delete_.bind(this));
243
244   this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE');
245   this.shareButton_.setAttribute('disabled', '');
246   this.shareButton_.addEventListener('click', this.toggleShare_.bind(this));
247
248   this.shareMenu_ = util.createChild(this.container_, 'share-menu');
249   this.shareMenu_.hidden = true;
250   util.createChild(this.shareMenu_, 'bubble-point');
251
252   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
253   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
254
255   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
256   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
257 };
258
259 /**
260  * Creates toolbar button.
261  *
262  * @param {string} className Class to add.
263  * @param {string} title Button title.
264  * @return {HTMLElement} Newly created button.
265  * @private
266  */
267 Gallery.prototype.createToolbarButton_ = function(className, title) {
268   var button = util.createChild(this.toolbar_, className, 'button');
269   button.title = str(title);
270   return button;
271 };
272
273 /**
274  * Loads the content.
275  *
276  * @param {Array.<Entry>} entries Array of entries.
277  * @param {Array.<Entry>} selectedEntries Array of selected entries.
278  */
279 Gallery.prototype.load = function(entries, selectedEntries) {
280   var items = [];
281   for (var index = 0; index < entries.length; ++index) {
282     items.push(new Gallery.Item(entries[index]));
283   }
284   this.dataModel_.push.apply(this.dataModel_, items);
285
286   this.selectionModel_.adjustLength(this.dataModel_.length);
287
288   // Comparing Entries by reference is not safe. Therefore we have to use URLs.
289   var entryIndexesByURLs = {};
290   for (var index = 0; index < entries.length; index++) {
291     entryIndexesByURLs[entries[index].toURL()] = index;
292   }
293
294   for (var i = 0; i !== selectedEntries.length; i++) {
295     var selectedIndex = entryIndexesByURLs[selectedEntries[i].toURL()];
296     if (selectedIndex !== undefined)
297       this.selectionModel_.setIndexSelected(selectedIndex, true);
298     else
299       console.error('Cannot select ' + selectedEntries[i]);
300   }
301
302   if (this.selectionModel_.selectedIndexes.length === 0)
303     this.onSelection_();
304
305   var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic();
306
307   // Mosaic view should show up if most of the selected files are images.
308   var imagesCount = 0;
309   for (var i = 0; i !== selectedEntries.length; i++) {
310     if (FileType.getMediaType(selectedEntries[i]) === 'image')
311       imagesCount++;
312   }
313   var mostlyImages = imagesCount > (selectedEntries.length / 2.0);
314
315   var forcedMosaic = (this.context_.pageState &&
316        this.context_.pageState.gallery === 'mosaic');
317
318   var showMosaic = (mostlyImages && selectedEntries.length > 1) || forcedMosaic;
319   if (mosaic && showMosaic) {
320     this.setCurrentMode_(this.mosaicMode_);
321     mosaic.init();
322     mosaic.show();
323     this.inactivityWatcher_.check();  // Show the toolbar.
324     cr.dispatchSimpleEvent(this, 'loaded');
325   } else {
326     this.setCurrentMode_(this.slideMode_);
327     var maybeLoadMosaic = function() {
328       if (mosaic)
329         mosaic.init();
330       cr.dispatchSimpleEvent(this, 'loaded');
331     }.bind(this);
332     /* TODO: consider nice blow-up animation for the first image */
333     this.slideMode_.enter(null, function() {
334         // Flash the toolbar briefly to show it is there.
335         this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
336       }.bind(this),
337       maybeLoadMosaic);
338   }
339 };
340
341 /**
342  * Closes the Gallery and go to Files.app.
343  * @private
344  */
345 Gallery.prototype.back_ = function() {
346   if (util.isFullScreen(this.context_.appWindow)) {
347     util.toggleFullScreen(this.context_.appWindow,
348                           false);  // Leave the full screen mode.
349   }
350   this.context_.onBack(this.getSelectedEntries());
351 };
352
353 /**
354  * Handles user's 'Back' action (Escape or a click on the X icon).
355  * @private
356  */
357 Gallery.prototype.onBack_ = function() {
358   this.executeWhenReady(this.back_.bind(this));
359 };
360
361 /**
362  * Handles user's 'Close' action.
363  * @private
364  */
365 Gallery.prototype.onClose_ = function() {
366   this.executeWhenReady(this.context_.onClose);
367 };
368
369 /**
370  * Handles user's 'Maximize' action (Escape or a click on the X icon).
371  * @private
372  */
373 Gallery.prototype.onMaximize_ = function() {
374   this.executeWhenReady(this.context_.onMaximize);
375 };
376
377 /**
378  * Handles user's 'Maximize' action (Escape or a click on the X icon).
379  * @private
380  */
381 Gallery.prototype.onMinimize_ = function() {
382   this.executeWhenReady(this.context_.onMinimize);
383 };
384
385 /**
386  * Executes a function when the editor is done with the modifications.
387  * @param {function} callback Function to execute.
388  */
389 Gallery.prototype.executeWhenReady = function(callback) {
390   this.currentMode_.executeWhenReady(callback);
391 };
392
393 /**
394  * @return {Object} File browser private API.
395  */
396 Gallery.getFileBrowserPrivate = function() {
397   return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate;
398 };
399
400 /**
401  * @return {boolean} True if some tool is currently active.
402  */
403 Gallery.prototype.hasActiveTool = function() {
404   return this.currentMode_.hasActiveTool() ||
405       this.isSharing_() || this.isRenaming_();
406 };
407
408 /**
409 * External user action event handler.
410 * @private
411 */
412 Gallery.prototype.onUserAction_ = function() {
413   this.closeShareMenu_();
414   // Show the toolbar and hide it after the default timeout.
415   this.inactivityWatcher_.kick();
416 };
417
418 /**
419  * Sets the current mode, update the UI.
420  * @param {Object} mode Current mode.
421  * @private
422  */
423 Gallery.prototype.setCurrentMode_ = function(mode) {
424   if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
425     console.error('Invalid Gallery mode');
426
427   this.currentMode_ = mode;
428   this.container_.setAttribute('mode', this.currentMode_.getName());
429   this.updateSelectionAndState_();
430   this.updateButtons_();
431 };
432
433 /**
434  * Mode toggle event handler.
435  * @param {function=} opt_callback Callback.
436  * @param {Event=} opt_event Event that caused this call.
437  * @private
438  */
439 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
440   if (!this.modeButton_)
441     return;
442
443   if (this.changingMode_) // Do not re-enter while changing the mode.
444     return;
445
446   if (opt_event)
447     this.onUserAction_();
448
449   this.changingMode_ = true;
450
451   var onModeChanged = function() {
452     this.changingMode_ = false;
453     if (opt_callback) opt_callback();
454   }.bind(this);
455
456   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
457
458   var mosaic = this.mosaicMode_.getMosaic();
459   var tileRect = mosaic.getTileRect(tileIndex);
460
461   if (this.currentMode_ === this.slideMode_) {
462     this.setCurrentMode_(this.mosaicMode_);
463     mosaic.transform(
464         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
465     this.slideMode_.leave(tileRect,
466         function() {
467           // Animate back to normal position.
468           mosaic.transform();
469           mosaic.show();
470           onModeChanged();
471         }.bind(this));
472   } else {
473     this.setCurrentMode_(this.slideMode_);
474     this.slideMode_.enter(tileRect,
475         function() {
476           // Animate to zoomed position.
477           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
478           mosaic.hide();
479         }.bind(this),
480         onModeChanged);
481   }
482 };
483
484 /**
485  * Deletes the selected items.
486  * @private
487  */
488 Gallery.prototype.delete_ = function() {
489   this.onUserAction_();
490
491   // Clone the sorted selected indexes array.
492   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
493   if (!indexesToRemove.length)
494     return;
495
496   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
497
498   var itemsToRemove = this.getSelectedItems();
499   var plural = itemsToRemove.length > 1;
500   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
501
502   function deleteNext() {
503     if (!itemsToRemove.length)
504       return;  // All deleted.
505
506     // TODO(hirono): Use fileOperationManager.
507     var entry = itemsToRemove.pop().getEntry();
508     entry.remove(deleteNext, function() {
509       util.flog('Error deleting: ' + entry.name, deleteNext);
510     });
511   }
512
513   // Prevent the Gallery from handling Esc and Enter.
514   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
515   var restoreListener = function() {
516     this.document_.body.addEventListener('keydown', this.keyDownBound_);
517   }.bind(this);
518
519
520   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
521   confirm.show(strf(plural ?
522       'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
523       function() {
524         restoreListener();
525         this.selectionModel_.unselectAll();
526         this.selectionModel_.leadIndex = -1;
527         // Remove items from the data model, starting from the highest index.
528         while (indexesToRemove.length)
529           this.dataModel_.splice(indexesToRemove.pop(), 1);
530         // Delete actual files.
531         deleteNext();
532       }.bind(this),
533       function() {
534         // Restore the listener after a timeout so that ESC is processed.
535         setTimeout(restoreListener, 0);
536       });
537 };
538
539 /**
540  * @return {Array.<Gallery.Item>} Current selection.
541  */
542 Gallery.prototype.getSelectedItems = function() {
543   return this.selectionModel_.selectedIndexes.map(
544       this.dataModel_.item.bind(this.dataModel_));
545 };
546
547 /**
548  * @return {Array.<Entry>} Array of currently selected entries.
549  */
550 Gallery.prototype.getSelectedEntries = function() {
551   return this.selectionModel_.selectedIndexes.map(function(index) {
552     return this.dataModel_.item(index).getEntry();
553   }.bind(this));
554 };
555
556 /**
557  * @return {Gallery.Item} Current single selection.
558  */
559 Gallery.prototype.getSingleSelectedItem = function() {
560   var items = this.getSelectedItems();
561   if (items.length > 1)
562     throw new Error('Unexpected multiple selection');
563   return items[0];
564 };
565
566 /**
567   * Selection change event handler.
568   * @private
569   */
570 Gallery.prototype.onSelection_ = function() {
571   this.updateSelectionAndState_();
572   this.updateShareMenu_();
573 };
574
575 /**
576   * Data model splice event handler.
577   * @private
578   */
579 Gallery.prototype.onSplice_ = function() {
580   this.selectionModel_.adjustLength(this.dataModel_.length);
581 };
582
583 /**
584  * Content change event handler.
585  * @param {Event} event Event.
586  * @private
587 */
588 Gallery.prototype.onContentChange_ = function(event) {
589   var index = this.dataModel_.indexOf(event.item);
590   if (index !== this.selectionModel_.selectedIndex)
591     console.error('Content changed for unselected item');
592   this.updateSelectionAndState_();
593 };
594
595 /**
596  * Keydown handler.
597  *
598  * @param {Event} event Event.
599  * @private
600  */
601 Gallery.prototype.onKeyDown_ = function(event) {
602   var wasSharing = this.isSharing_();
603   this.closeShareMenu_();
604
605   if (this.currentMode_.onKeyDown(event))
606     return;
607
608   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
609     case 'U+0008': // Backspace.
610       // The default handler would call history.back and close the Gallery.
611       event.preventDefault();
612       break;
613
614     case 'U+001B':  // Escape
615       // Swallow Esc if it closed the Share menu, otherwise close the Gallery.
616       if (!wasSharing)
617         this.onBack_();
618       break;
619
620     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
621       this.toggleMode_(null, event);
622       break;
623
624     case 'U+0056':  // 'v'
625       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
626       break;
627
628     case 'U+007F':  // Delete
629     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
630       this.delete_();
631       break;
632   }
633 };
634
635 // Name box and rename support.
636
637 /**
638  * Updates the UI related to the selected item and the persistent state.
639  *
640  * @private
641  */
642 Gallery.prototype.updateSelectionAndState_ = function() {
643   var numSelectedItems = this.selectionModel_.selectedIndexes.length;
644   var displayName = '';
645   var selectedEntryURL = null;
646
647   // If it's selecting something, update the variable values.
648   if (numSelectedItems) {
649     var selectedItem =
650         this.dataModel_.item(this.selectionModel_.selectedIndex);
651     this.selectedEntry_ = selectedItem.getEntry();
652     selectedEntryURL = this.selectedEntry_.toURL();
653
654     if (numSelectedItems === 1) {
655       window.top.document.title = this.selectedEntry_.name;
656       displayName = ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
657     } else if (this.context_.curDirEntry) {
658       // If the Gallery was opened on search results the search query will not
659       // be recorded in the app state and the relaunch will just open the
660       // gallery in the curDirEntry directory.
661       window.top.document.title = this.context_.curDirEntry.name;
662       displayName = strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
663     }
664   }
665
666   window.top.util.updateAppState(
667       null,  // Keep the current directory.
668       selectedEntryURL,  // Update the selection.
669       {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
670
671   // We can't rename files in readonly directory.
672   // We can only rename a single file.
673   this.filenameEdit_.disabled = numSelectedItems !== 1 ||
674                                 this.context_.readonlyDirName;
675   this.filenameEdit_.value = displayName;
676 };
677
678 /**
679  * Click event handler on filename edit box
680  * @private
681  */
682 Gallery.prototype.onFilenameFocus_ = function() {
683   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
684   this.filenameEdit_.originalValue = this.filenameEdit_.value;
685   setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
686   this.onUserAction_();
687 };
688
689 /**
690  * Blur event handler on filename edit box.
691  *
692  * @param {Event} event Blur event.
693  * @return {boolean} if default action should be prevented.
694  * @private
695  */
696 Gallery.prototype.onFilenameEditBlur_ = function(event) {
697   if (this.filenameEdit_.value && this.filenameEdit_.value[0] === '.') {
698     this.prompt_.show('GALLERY_FILE_HIDDEN_NAME', 5000);
699     this.filenameEdit_.focus();
700     event.stopPropagation();
701     event.preventDefault();
702     return false;
703   }
704
705   var item = this.getSingleSelectedItem();
706   var oldEntry = item.getEntry();
707
708   var onFileExists = function() {
709     this.prompt_.show('GALLERY_FILE_EXISTS', 3000);
710     this.filenameEdit_.value = name;
711     this.filenameEdit_.focus();
712   }.bind(this);
713
714   var onSuccess = function() {
715     var event = new Event('content');
716     event.item = item;
717     event.oldEntry = oldEntry;
718     event.metadata = null;  // Metadata unchanged.
719     this.dataModel_.dispatchEvent(event);
720   }.bind(this);
721
722   if (this.filenameEdit_.value) {
723     this.getSingleSelectedItem().rename(
724         this.filenameEdit_.value, onSuccess, onFileExists);
725   }
726
727   ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
728   this.onUserAction_();
729 };
730
731 /**
732  * Keydown event handler on filename edit box
733  * @private
734  */
735 Gallery.prototype.onFilenameEditKeydown_ = function() {
736   switch (event.keyCode) {
737     case 27:  // Escape
738       this.filenameEdit_.value = this.filenameEdit_.originalValue;
739       this.filenameEdit_.blur();
740       break;
741
742     case 13:  // Enter
743       this.filenameEdit_.blur();
744       break;
745   }
746   event.stopPropagation();
747 };
748
749 /**
750  * @return {boolean} True if file renaming is currently in progress.
751  * @private
752  */
753 Gallery.prototype.isRenaming_ = function() {
754   return this.filenameSpacer_.hasAttribute('renaming');
755 };
756
757 /**
758  * Content area click handler.
759  * @private
760  */
761 Gallery.prototype.onContentClick_ = function() {
762   this.closeShareMenu_();
763   this.filenameEdit_.blur();
764 };
765
766 // Share button support.
767
768 /**
769  * @return {boolean} True if the Share menu is active.
770  * @private
771  */
772 Gallery.prototype.isSharing_ = function() {
773   return !this.shareMenu_.hidden;
774 };
775
776 /**
777  * Close Share menu if it is open.
778  * @private
779  */
780 Gallery.prototype.closeShareMenu_ = function() {
781   if (this.isSharing_())
782     this.toggleShare_();
783 };
784
785 /**
786  * Share button handler.
787  * @private
788  */
789 Gallery.prototype.toggleShare_ = function() {
790   if (!this.shareButton_.hasAttribute('disabled'))
791     this.shareMenu_.hidden = !this.shareMenu_.hidden;
792   this.inactivityWatcher_.check();
793 };
794
795 /**
796  * Updates available actions list based on the currently selected urls.
797  * @private.
798  */
799 Gallery.prototype.updateShareMenu_ = function() {
800   var entries = this.getSelectedEntries();
801
802   function isShareAction(task) {
803     var taskParts = task.taskId.split('|');
804     return taskParts[0] !== chrome.runtime.id;
805   }
806
807   var api = Gallery.getFileBrowserPrivate();
808   var mimeTypes = [];  // TODO(kaznacheev) Collect mime types properly.
809
810   var createShareMenu = function(tasks) {
811     var wasHidden = this.shareMenu_.hidden;
812     this.shareMenu_.hidden = true;
813     var items = this.shareMenu_.querySelectorAll('.item');
814     for (var i = 0; i !== items.length; i++) {
815       items[i].parentNode.removeChild(items[i]);
816     }
817
818     for (var t = 0; t !== tasks.length; t++) {
819       var task = tasks[t];
820       if (!isShareAction(task)) continue;
821
822       var item = util.createChild(this.shareMenu_, 'item');
823       item.textContent = task.title;
824       item.style.backgroundImage = 'url(' + task.iconUrl + ')';
825       item.addEventListener('click', function(taskId) {
826         this.toggleShare_();  // Hide the menu.
827         // TODO(hirono): Use entries instead of URLs.
828         this.executeWhenReady(
829             api.executeTask.bind(
830                 api,
831                 taskId,
832                 util.entriesToURLs(entries),
833                 function(result) {
834                   var alertDialog =
835                       new cr.ui.dialogs.AlertDialog(this.container_);
836                   util.isTeleported(window).then(function(teleported) {
837                     if (teleported)
838                       util.showOpenInOtherDesktopAlert(alertDialog, entries);
839                   }.bind(this));
840                 }.bind(this)));
841       }.bind(this, task.taskId));
842     }
843
844     var empty = this.shareMenu_.querySelector('.item') === null;
845     ImageUtil.setAttribute(this.shareButton_, 'disabled', empty);
846     this.shareMenu_.hidden = wasHidden || empty;
847   }.bind(this);
848
849   // Create or update the share menu with a list of sharing tasks and show
850   // or hide the share button.
851   // TODO(mtomasz): Pass Entries directly, instead of URLs.
852   if (!entries.length)
853     createShareMenu([]);  // Empty list of tasks, since there is no selection.
854   else
855     api.getFileTasks(util.entriesToURLs(entries), mimeTypes, createShareMenu);
856 };
857
858 /**
859  * Updates thumbnails.
860  * @private
861  */
862 Gallery.prototype.updateThumbnails_ = function() {
863   if (this.currentMode_ === this.slideMode_)
864     this.slideMode_.updateThumbnails();
865
866   if (this.mosaicMode_) {
867     var mosaic = this.mosaicMode_.getMosaic();
868     if (mosaic.isInitialized())
869       mosaic.reload();
870   }
871 };
872
873 /**
874  * Updates buttons.
875  * @private
876  */
877 Gallery.prototype.updateButtons_ = function() {
878   if (this.modeButton_) {
879     var oppositeMode =
880         this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
881                                                 this.slideMode_;
882     this.modeButton_.title = str(oppositeMode.getTitle());
883   }
884 };