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