- add sources.
[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
47   this.dataModel_ = new cr.ui.ArrayDataModel([]);
48   this.selectionModel_ = new cr.ui.ListSelectionModel();
49   this.displayStringFunction_ = context.displayStringFunction;
50
51   this.initDom_();
52   this.initListeners_();
53 }
54
55 /**
56  * Gallery extends cr.EventTarget.
57  */
58 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
59
60 /**
61  * Create and initialize a Gallery object based on a context.
62  *
63  * @param {Object} context Gallery context.
64  * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
65  * @param {Array.<string>} urls Array of urls.
66  * @param {Array.<string>} selectedUrls Array of selected urls.
67  */
68 Gallery.open = function(context, volumeManager, urls, selectedUrls) {
69   Gallery.instance = new Gallery(context, volumeManager);
70   Gallery.instance.load(urls, selectedUrls);
71 };
72
73 /**
74  * Tools fade-out timeout im milliseconds.
75  * @const
76  * @type {number}
77  */
78 Gallery.FADE_TIMEOUT = 3000;
79
80 /**
81  * First time tools fade-out timeout im milliseconds.
82  * @const
83  * @type {number}
84  */
85 Gallery.FIRST_FADE_TIMEOUT = 1000;
86
87 /**
88  * Time until mosaic is initialized in the background. Used to make gallery
89  * in the slide mode load faster. In miiliseconds.
90  * @const
91  * @type {number}
92  */
93 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
94
95 /**
96  * Types of metadata Gallery uses (to query the metadata cache).
97  * @const
98  * @type {string}
99  */
100 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming|drive';
101
102 /**
103  * Initialize listeners.
104  * @private
105  */
106 Gallery.prototype.initListeners_ = function() {
107   this.document_.oncontextmenu = function(e) { e.preventDefault(); };
108   this.keyDownBound_ = this.onKeyDown_.bind(this);
109   this.document_.body.addEventListener('keydown', this.keyDownBound_);
110
111   this.inactivityWatcher_ = new MouseInactivityWatcher(
112       this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
113
114   // Search results may contain files from different subdirectories so
115   // the observer is not going to work.
116   if (!this.context_.searchResults && this.context_.curDirEntry) {
117     this.thumbnailObserverId_ = this.metadataCache_.addObserver(
118         this.context_.curDirEntry,
119         MetadataCache.CHILDREN,
120         'thumbnail',
121         this.updateThumbnails_.bind(this));
122   }
123
124   this.volumeManager_.addEventListener('externally-unmounted',
125       this.onExternallyUnmounted_.bind(this));
126 };
127
128 /**
129  * Closes gallery when a volume containing the selected item is unmounted.
130  * @param {Event} event The unmount event.
131  * @private
132  */
133 Gallery.prototype.onExternallyUnmounted_ = function(event) {
134   if (!this.selectedItemFilesystemPath_)
135     return;
136   if (this.selectedItemFilesystemPath_.indexOf(event.mountPath) == 0)
137     this.onBack_();
138 };
139
140 /**
141  * Beforeunload handler.
142  * @return {string?} User-visible message on null if it is OK to close.
143  */
144 Gallery.prototype.onBeforeUnload = function() {
145   return this.slideMode_.onBeforeUnload();
146 };
147
148 /**
149  * Unload the Gallery.
150  * @param {boolean} exiting True if the app is exiting.
151  */
152 Gallery.prototype.onUnload = function(exiting) {
153   if (!this.context_.searchResults) {
154     this.metadataCache_.removeObserver(this.thumbnailObserverId_);
155   }
156   this.slideMode_.onUnload(exiting);
157 };
158
159 /**
160  * Initializes DOM UI
161  * @private
162  */
163 Gallery.prototype.initDom_ = function() {
164   var content = util.createChild(this.container_, 'content');
165   content.addEventListener('click', this.onContentClick_.bind(this));
166
167   this.header_ = util.createChild(this.container_, 'header tool dimmable');
168   this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable');
169
170   var backButton = util.createChild(this.container_,
171                                     'back-button tool dimmable');
172   util.createChild(backButton);
173   backButton.addEventListener('click', this.onBack_.bind(this));
174
175   var preventDefault = function(event) { event.preventDefault(); };
176
177   var maximizeButton = util.createChild(this.header_,
178                                         'maximize-button tool dimmable',
179                                         'button');
180   maximizeButton.tabIndex = -1;
181   maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
182   maximizeButton.addEventListener('mousedown', preventDefault);
183
184   var closeButton = util.createChild(this.header_,
185                                      'close-button tool dimmable',
186                                      'button');
187   closeButton.tabIndex = -1;
188   closeButton.addEventListener('click', this.onClose_.bind(this));
189   closeButton.addEventListener('mousedown', preventDefault);
190
191   this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer');
192   this.filenameEdit_ = util.createChild(this.filenameSpacer_,
193                                         'namebox', 'input');
194
195   this.filenameEdit_.setAttribute('type', 'text');
196   this.filenameEdit_.addEventListener('blur',
197       this.onFilenameEditBlur_.bind(this));
198
199   this.filenameEdit_.addEventListener('focus',
200       this.onFilenameFocus_.bind(this));
201
202   this.filenameEdit_.addEventListener('keydown',
203       this.onFilenameEditKeydown_.bind(this));
204
205   util.createChild(this.toolbar_, 'button-spacer');
206
207   this.prompt_ = new ImageEditor.Prompt(
208       this.container_, this.displayStringFunction_);
209
210   this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button');
211   this.modeButton_.addEventListener('click',
212       this.toggleMode_.bind(this, null));
213
214   this.mosaicMode_ = new MosaicMode(content,
215                                     this.dataModel_,
216                                     this.selectionModel_,
217                                     this.metadataCache_,
218                                     this.toggleMode_.bind(this, null));
219
220   this.slideMode_ = new SlideMode(this.container_,
221                                   content,
222                                   this.toolbar_,
223                                   this.prompt_,
224                                   this.dataModel_,
225                                   this.selectionModel_,
226                                   this.context_,
227                                   this.toggleMode_.bind(this),
228                                   this.displayStringFunction_);
229
230   this.slideMode_.addEventListener('image-displayed', function() {
231     cr.dispatchSimpleEvent(this, 'image-displayed');
232   }.bind(this));
233   this.slideMode_.addEventListener('image-saved', function() {
234     cr.dispatchSimpleEvent(this, 'image-saved');
235   }.bind(this));
236
237   var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE');
238   deleteButton.addEventListener('click', this.delete_.bind(this));
239
240   this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE');
241   this.shareButton_.setAttribute('disabled', '');
242   this.shareButton_.addEventListener('click', this.toggleShare_.bind(this));
243
244   this.shareMenu_ = util.createChild(this.container_, 'share-menu');
245   this.shareMenu_.hidden = true;
246   util.createChild(this.shareMenu_, 'bubble-point');
247
248   this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
249   this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
250
251   this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
252   this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
253 };
254
255 /**
256  * Creates toolbar button.
257  *
258  * @param {string} className Class to add.
259  * @param {string} title Button title.
260  * @return {HTMLElement} Newly created button.
261  * @private
262  */
263 Gallery.prototype.createToolbarButton_ = function(className, title) {
264   var button = util.createChild(this.toolbar_, className, 'button');
265   button.title = this.displayStringFunction_(title);
266   return button;
267 };
268
269 /**
270  * Load the content.
271  *
272  * @param {Array.<string>} urls Array of urls.
273  * @param {Array.<string>} selectedUrls Array of selected urls.
274  */
275 Gallery.prototype.load = function(urls, selectedUrls) {
276   var items = [];
277   for (var index = 0; index < urls.length; ++index) {
278     items.push(new Gallery.Item(urls[index]));
279   }
280   this.dataModel_.push.apply(this.dataModel_, items);
281
282   this.selectionModel_.adjustLength(this.dataModel_.length);
283
284   for (var i = 0; i != selectedUrls.length; i++) {
285     var selectedIndex = urls.indexOf(selectedUrls[i]);
286     if (selectedIndex >= 0)
287       this.selectionModel_.setIndexSelected(selectedIndex, true);
288     else
289       console.error('Cannot select ' + selectedUrls[i]);
290   }
291
292   if (this.selectionModel_.selectedIndexes.length == 0)
293     this.onSelection_();
294
295   var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic();
296
297   // Mosaic view should show up if most of the selected files are images.
298   var imagesCount = 0;
299   for (var i = 0; i != selectedUrls.length; i++) {
300     if (FileType.getMediaType(selectedUrls[i]) == 'image')
301       imagesCount++;
302   }
303   var mostlyImages = imagesCount > (selectedUrls.length / 2.0);
304
305   var forcedMosaic = (this.context_.pageState &&
306        this.context_.pageState.gallery == 'mosaic');
307
308   var showMosaic = (mostlyImages && selectedUrls.length > 1) || forcedMosaic;
309   if (mosaic && showMosaic) {
310     this.setCurrentMode_(this.mosaicMode_);
311     mosaic.init();
312     mosaic.show();
313     this.inactivityWatcher_.check();  // Show the toolbar.
314     cr.dispatchSimpleEvent(this, 'loaded');
315   } else {
316     this.setCurrentMode_(this.slideMode_);
317     var maybeLoadMosaic = function() {
318       if (mosaic)
319         mosaic.init();
320       cr.dispatchSimpleEvent(this, 'loaded');
321     }.bind(this);
322     /* TODO: consider nice blow-up animation for the first image */
323     this.slideMode_.enter(null, function() {
324         // Flash the toolbar briefly to show it is there.
325         this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
326       }.bind(this),
327       maybeLoadMosaic);
328   }
329 };
330
331 /**
332  * Close the Gallery and go to Files.app.
333  * @private
334  */
335 Gallery.prototype.back_ = function() {
336   if (util.isFullScreen(this.context_.appWindow)) {
337     util.toggleFullScreen(this.context_.appWindow,
338                           false);  // Leave the full screen mode.
339   }
340   this.context_.onBack(this.getSelectedUrls());
341 };
342
343 /**
344  * Handle user's 'Back' action (Escape or a click on the X icon).
345  * @private
346  */
347 Gallery.prototype.onBack_ = function() {
348   this.executeWhenReady(this.back_.bind(this));
349 };
350
351 /**
352  * Handle user's 'Close' action.
353  * @private
354  */
355 Gallery.prototype.onClose_ = function() {
356   this.executeWhenReady(this.context_.onClose);
357 };
358
359 /**
360  * Handle user's 'Maximize' action (Escape or a click on the X icon).
361  * @private
362  */
363 Gallery.prototype.onMaximize_ = function() {
364   this.executeWhenReady(this.context_.onMaximize);
365 };
366
367 /**
368  * Execute a function when the editor is done with the modifications.
369  * @param {function} callback Function to execute.
370  */
371 Gallery.prototype.executeWhenReady = function(callback) {
372   this.currentMode_.executeWhenReady(callback);
373 };
374
375 /**
376  * @return {Object} File browser private API.
377  */
378 Gallery.getFileBrowserPrivate = function() {
379   return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate;
380 };
381
382 /**
383  * @return {boolean} True if some tool is currently active.
384  */
385 Gallery.prototype.hasActiveTool = function() {
386   return this.currentMode_.hasActiveTool() ||
387       this.isSharing_() || this.isRenaming_();
388 };
389
390 /**
391 * External user action event handler.
392 * @private
393 */
394 Gallery.prototype.onUserAction_ = function() {
395   this.closeShareMenu_();
396   // Show the toolbar and hide it after the default timeout.
397   this.inactivityWatcher_.kick();
398 };
399
400 /**
401  * Set the current mode, update the UI.
402  * @param {Object} mode Current mode.
403  * @private
404  */
405 Gallery.prototype.setCurrentMode_ = function(mode) {
406   if (mode != this.slideMode_ && mode != this.mosaicMode_)
407     console.error('Invalid Gallery mode');
408
409   this.currentMode_ = mode;
410   this.container_.setAttribute('mode', this.currentMode_.getName());
411   this.updateSelectionAndState_();
412   this.updateButtons_();
413 };
414
415 /**
416  * Mode toggle event handler.
417  * @param {function=} opt_callback Callback.
418  * @param {Event=} opt_event Event that caused this call.
419  * @private
420  */
421 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
422   if (!this.modeButton_)
423     return;
424
425   if (this.changingMode_) // Do not re-enter while changing the mode.
426     return;
427
428   if (opt_event)
429     this.onUserAction_();
430
431   this.changingMode_ = true;
432
433   var onModeChanged = function() {
434     this.changingMode_ = false;
435     if (opt_callback) opt_callback();
436   }.bind(this);
437
438   var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
439
440   var mosaic = this.mosaicMode_.getMosaic();
441   var tileRect = mosaic.getTileRect(tileIndex);
442
443   if (this.currentMode_ == this.slideMode_) {
444     this.setCurrentMode_(this.mosaicMode_);
445     mosaic.transform(
446         tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
447     this.slideMode_.leave(tileRect,
448         function() {
449           // Animate back to normal position.
450           mosaic.transform();
451           mosaic.show();
452           onModeChanged();
453         }.bind(this));
454   } else {
455     this.setCurrentMode_(this.slideMode_);
456     this.slideMode_.enter(tileRect,
457         function() {
458           // Animate to zoomed position.
459           mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
460           mosaic.hide();
461         }.bind(this),
462         onModeChanged);
463   }
464 };
465
466 /**
467  * Deletes the selected items.
468  * @private
469  */
470 Gallery.prototype.delete_ = function() {
471   this.onUserAction_();
472
473   // Clone the sorted selected indexes array.
474   var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
475   if (!indexesToRemove.length)
476     return;
477
478   /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
479
480   var itemsToRemove = this.getSelectedItems();
481   var plural = itemsToRemove.length > 1;
482   var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
483
484   function deleteNext() {
485     if (!itemsToRemove.length)
486       return;  // All deleted.
487
488     var url = itemsToRemove.pop().getUrl();
489     webkitResolveLocalFileSystemURL(url,
490         function(entry) {
491           entry.remove(deleteNext,
492               util.flog('Error deleting ' + url, deleteNext));
493         },
494         util.flog('Error resolving ' + url, deleteNext));
495   }
496
497   // Prevent the Gallery from handling Esc and Enter.
498   this.document_.body.removeEventListener('keydown', this.keyDownBound_);
499   var restoreListener = function() {
500     this.document_.body.addEventListener('keydown', this.keyDownBound_);
501   }.bind(this);
502
503   cr.ui.dialogs.BaseDialog.OK_LABEL = this.displayStringFunction_(
504       'GALLERY_OK_LABEL');
505   cr.ui.dialogs.BaseDialog.CANCEL_LABEL =
506       this.displayStringFunction_('GALLERY_CANCEL_LABEL');
507   var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
508   confirm.show(
509       this.displayStringFunction_(plural ? 'GALLERY_CONFIRM_DELETE_SOME' :
510           'GALLERY_CONFIRM_DELETE_ONE', param),
511       function() {
512         restoreListener();
513         this.selectionModel_.unselectAll();
514         this.selectionModel_.leadIndex = -1;
515         // Remove items from the data model, starting from the highest index.
516         while (indexesToRemove.length)
517           this.dataModel_.splice(indexesToRemove.pop(), 1);
518         // Delete actual files.
519         deleteNext();
520       }.bind(this),
521       function() {
522         // Restore the listener after a timeout so that ESC is processed.
523         setTimeout(restoreListener, 0);
524       });
525 };
526
527 /**
528  * @return {Array.<Gallery.Item>} Current selection.
529  */
530 Gallery.prototype.getSelectedItems = function() {
531   return this.selectionModel_.selectedIndexes.map(
532       this.dataModel_.item.bind(this.dataModel_));
533 };
534
535 /**
536  * @return {Array.<string>} Array of currently selected urls.
537  */
538 Gallery.prototype.getSelectedUrls = function() {
539   return this.selectionModel_.selectedIndexes.map(function(index) {
540     return this.dataModel_.item(index).getUrl();
541   }.bind(this));
542 };
543
544 /**
545  * @return {Gallery.Item} Current single selection.
546  */
547 Gallery.prototype.getSingleSelectedItem = function() {
548   var items = this.getSelectedItems();
549   if (items.length > 1)
550     throw new Error('Unexpected multiple selection');
551   return items[0];
552 };
553
554 /**
555   * Selection change event handler.
556   * @private
557   */
558 Gallery.prototype.onSelection_ = function() {
559   this.updateSelectionAndState_();
560   this.updateShareMenu_();
561 };
562
563 /**
564   * Data model splice event handler.
565   * @private
566   */
567 Gallery.prototype.onSplice_ = function() {
568   this.selectionModel_.adjustLength(this.dataModel_.length);
569 };
570
571 /**
572  * Content change event handler.
573  * @param {Event} event Event.
574  * @private
575 */
576 Gallery.prototype.onContentChange_ = function(event) {
577   var index = this.dataModel_.indexOf(event.item);
578   if (index != this.selectionModel_.selectedIndex)
579     console.error('Content changed for unselected item');
580   this.updateSelectionAndState_();
581 };
582
583 /**
584  * Keydown handler.
585  *
586  * @param {Event} event Event.
587  * @private
588  */
589 Gallery.prototype.onKeyDown_ = function(event) {
590   var wasSharing = this.isSharing_();
591   this.closeShareMenu_();
592
593   if (this.currentMode_.onKeyDown(event))
594     return;
595
596   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
597     case 'U+0008': // Backspace.
598       // The default handler would call history.back and close the Gallery.
599       event.preventDefault();
600       break;
601
602     case 'U+001B':  // Escape
603       // Swallow Esc if it closed the Share menu, otherwise close the Gallery.
604       if (!wasSharing)
605         this.onBack_();
606       break;
607
608     case 'U+004D':  // 'm' switches between Slide and Mosaic mode.
609       this.toggleMode_(null, event);
610       break;
611
612     case 'U+0056':  // 'v'
613       this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
614       break;
615
616     case 'U+007F':  // Delete
617     case 'Shift-U+0033':  // Shift+'3' (Delete key might be missing).
618       this.delete_();
619       break;
620   }
621 };
622
623 // Name box and rename support.
624
625 /**
626  * Update the UI related to the selected item and the persistent state.
627  *
628  * @private
629  */
630 Gallery.prototype.updateSelectionAndState_ = function() {
631   var path;
632   var displayName = '';
633
634   var selectedItems = this.getSelectedItems();
635   if (selectedItems.length == 1) {
636     var item = selectedItems[0];
637     path = util.extractFilePath(item.getUrl());
638     var fullName = item.getFileName();
639     window.top.document.title = fullName;
640     displayName = ImageUtil.getFileNameFromFullName(fullName);
641   } else if (selectedItems.length > 1 && this.context_.curDirEntry) {
642     // If the Gallery was opened on search results the search query will not be
643     // recorded in the app state and the relaunch will just open the gallery
644     // in the curDirEntry directory.
645     path = this.context_.curDirEntry.fullPath;
646     window.top.document.title = this.context_.curDirEntry.name;
647     displayName =
648         this.displayStringFunction_('GALLERY_ITEMS_SELECTED',
649                                     selectedItems.length);
650   }
651
652   window.top.util.updateAppState(path,
653       {gallery: (this.currentMode_ == this.mosaicMode_ ? 'mosaic' : 'slide')});
654
655   // We can't rename files in readonly directory.
656   // We can only rename a single file.
657   this.filenameEdit_.disabled = selectedItems.length != 1 ||
658                                 this.context_.readonlyDirName;
659
660   this.filenameEdit_.value = displayName;
661
662   // Resolve real filesystem path of the current file.
663   if (this.selectionModel_.selectedIndexes.length) {
664     var selectedIndex = this.selectionModel_.selectedIndex;
665     var selectedItem =
666         this.dataModel_.item(this.selectionModel_.selectedIndex);
667
668     this.selectedItemFilesystemPath_ = null;
669     webkitResolveLocalFileSystemURL(selectedItem.getUrl(),
670       function(entry) {
671         if (this.selectionModel_.selectedIndex != selectedIndex)
672           return;
673         this.selectedItemFilesystemPath_ = entry.fullPath;
674       }.bind(this));
675   }
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('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 oldUrl = item.getUrl();
707
708   var onFileExists = function() {
709     this.prompt_.show('file_exists', 3000);
710     this.filenameEdit_.value = name;
711     this.filenameEdit_.focus();
712   }.bind(this);
713
714   var onSuccess = function() {
715     var e = new Event('content');
716     e.item = item;
717     e.oldUrl = oldUrl;
718     e.metadata = null;  // Metadata unchanged.
719     this.dataModel_.dispatchEvent(e);
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 urls = this.getSelectedUrls();
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         this.executeWhenReady(api.executeTask.bind(api, taskId, urls));
828       }.bind(this, task.taskId));
829     }
830
831     var empty = this.shareMenu_.querySelector('.item') == null;
832     ImageUtil.setAttribute(this.shareButton_, 'disabled', empty);
833     this.shareMenu_.hidden = wasHidden || empty;
834   }.bind(this);
835
836   // Create or update the share menu with a list of sharing tasks and show
837   // or hide the share button.
838   if (!urls.length)
839     createShareMenu([]);  // Empty list of tasks, since there is no selection.
840   else
841     api.getFileTasks(urls, mimeTypes, createShareMenu);
842 };
843
844 /**
845  * Updates thumbnails.
846  * @private
847  */
848 Gallery.prototype.updateThumbnails_ = function() {
849   if (this.currentMode_ == this.slideMode_)
850     this.slideMode_.updateThumbnails();
851
852   if (this.mosaicMode_) {
853     var mosaic = this.mosaicMode_.getMosaic();
854     if (mosaic.isInitialized())
855       mosaic.reload();
856   }
857 };
858
859 /**
860  * Updates buttons.
861  * @private
862  */
863 Gallery.prototype.updateButtons_ = function() {
864   if (this.modeButton_) {
865     var oppositeMode =
866         this.currentMode_ == this.slideMode_ ? this.mosaicMode_ :
867                                                this.slideMode_;
868     this.modeButton_.title =
869         this.displayStringFunction_(oppositeMode.getTitle());
870   }
871 };