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.
8 * Called from the main frame when unloading.
9 * @param {boolean=} opt_exiting True if the app is exiting.
11 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting); }
14 * Overrided metadata worker's path.
18 ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
21 * Data model for gallery.
23 * @param {MetadataCache} metadataCache Metadata cache.
25 * @extends {cr.ui.ArrayDataModel}
27 function GalleryDataModel(metadataCache) {
28 cr.ui.ArrayDataModel.call(this, []);
32 * @type {MetadataCache}
35 this.metadataCache_ = metadataCache;
38 * Directory where the image is saved if the image is located in a read-only
40 * @type {DirectoryEntry}
42 this.fallbackSaveDirectory = null;
46 * Maximum number of full size image cache.
51 GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3;
54 * Maximum number of screen size image cache.
59 GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5;
61 GalleryDataModel.prototype = {
62 __proto__: cr.ui.ArrayDataModel.prototype
68 * @param {VolumeManager} volumeManager Volume manager instance.
69 * @param {Gallery.Item} item Original gallery item.
70 * @param {Canvas} canvas Canvas containing new image.
71 * @param {boolean} overwrite Whether to overwrite the image to the item or not.
72 * @return {Promise} Promise to be fulfilled with when the operation completes.
74 GalleryDataModel.prototype.saveItem = function(
75 volumeManager, item, canvas, overwrite) {
76 var oldEntry = item.getEntry();
77 var oldMetadata = item.getMetadata();
78 var oldLocationInfo = item.getLocationInfo();
79 var metadataEncoder = ImageEncoder.encodeMetadata(
80 item.getMetadata(), canvas, 1 /* quality */);
81 var newMetadata = ContentProvider.ConvertContentMetadata(
82 metadataEncoder.getMetadata(),
83 MetadataCache.cloneMetadata(item.getMetadata()));
84 if (newMetadata.filesystem)
85 newMetadata.filesystem.modificationTime = new Date();
86 if (newMetadata.external)
87 newMetadata.external.present = true;
89 return new Promise(function(fulfill, reject) {
92 this.fallbackSaveDirectory,
98 reject('Failed to save the image.');
102 // The item's entry is updated to the latest entry. Update metadata.
103 item.setMetadata(newMetadata);
105 // Current entry is updated.
106 // Dispatch an event.
107 var event = new Event('content');
109 event.oldEntry = oldEntry;
110 event.metadata = newMetadata;
111 this.dispatchEvent(event);
113 if (util.isSameEntry(oldEntry, item.getEntry())) {
114 // Need an update of metdataCache.
115 this.metadataCache_.set(
117 Gallery.METADATA_TYPE,
120 // New entry is added and the item now tracks it.
121 // Add another item for the old entry.
122 var anotherItem = new Gallery.Item(
128 // The item must be added behind the existing item so that it does
129 // not change the index of the existing item.
130 // TODO(hirono): Update the item index of the selection model
132 this.splice(this.indexOf(item) + 1, 0, anotherItem);
141 * Evicts image caches in the items.
142 * @param {Gallery.Item} currentSelectedItem Current selected item.
144 GalleryDataModel.prototype.evictCache = function(currentSelectedItem) {
145 // Sort the item by the last accessed date.
146 var sorted = this.slice().sort(function(a, b) {
147 return b.getLastAccessedDate() - a.getLastAccessedDate();
151 var contentCacheCount = 0;
152 var screenCacheCount = 0;
153 for (var i = 0; i < sorted.length; i++) {
154 if (sorted[i].contentImage) {
155 if (++contentCacheCount > GalleryDataModel.MAX_FULL_IMAGE_CACHE_) {
156 if (sorted[i].contentImage.parentNode) {
157 console.error('The content image has a parent node.');
159 // Force to free the buffer of the canvas by assigning zero size.
160 sorted[i].contentImage.width = 0;
161 sorted[i].contentImage.height = 0;
162 sorted[i].contentImage = null;
166 if (sorted[i].screenImage) {
167 if (++screenCacheCount > GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_) {
168 if (sorted[i].screenImage.parentNode) {
169 console.error('The screen image has a parent node.');
171 // Force to free the buffer of the canvas by assigning zero size.
172 sorted[i].screenImage.width = 0;
173 sorted[i].screenImage.height = 0;
174 sorted[i].screenImage = null;
182 * Gallery for viewing and editing image files.
184 * @param {!VolumeManager} volumeManager The VolumeManager instance of the
188 function Gallery(volumeManager) {
190 appWindow: chrome.app.window.current(),
191 onClose: function() { close(); },
192 onMaximize: function() {
193 var appWindow = chrome.app.window.current();
194 if (appWindow.isMaximized())
197 appWindow.maximize();
199 onMinimize: function() { chrome.app.window.current().minimize(); },
200 onAppRegionChanged: function() {},
201 metadataCache: MetadataCache.createFull(volumeManager),
203 displayStringFunction: function() { return ''; },
206 this.container_ = document.querySelector('.gallery');
207 this.document_ = document;
208 this.metadataCache_ = this.context_.metadataCache;
209 this.volumeManager_ = volumeManager;
210 this.selectedEntry_ = null;
211 this.metadataCacheObserverId_ = null;
212 this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
214 this.dataModel_ = new GalleryDataModel(
215 this.context_.metadataCache);
216 var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
217 VolumeManagerCommon.VolumeType.DOWNLOADS);
218 downloadVolumeInfo.resolveDisplayRoot().then(function(entry) {
219 this.dataModel_.fallbackSaveDirectory = entry;
220 }.bind(this)).catch(function(error) {
222 'Failed to obtain the fallback directory: ' + (error.stack || error));
224 this.selectionModel_ = new cr.ui.ListSelectionModel();
227 this.initListeners_();
231 * Gallery extends cr.EventTarget.
233 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
236 * Tools fade-out timeout in milliseconds.
240 Gallery.FADE_TIMEOUT = 3000;
243 * First time tools fade-out timeout in milliseconds.
247 Gallery.FIRST_FADE_TIMEOUT = 1000;
250 * Time until mosaic is initialized in the background. Used to make gallery
251 * in the slide mode load faster. In milliseconds.
255 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
258 * Types of metadata Gallery uses (to query the metadata cache).
262 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|external';
265 * Initializes listeners.
268 Gallery.prototype.initListeners_ = function() {
269 this.keyDownBound_ = this.onKeyDown_.bind(this);
270 this.document_.body.addEventListener('keydown', this.keyDownBound_);
272 this.inactivityWatcher_ = new MouseInactivityWatcher(
273 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
275 // Search results may contain files from different subdirectories so
276 // the observer is not going to work.
277 if (!this.context_.searchResults && this.context_.curDirEntry) {
278 this.metadataCacheObserverId_ = this.metadataCache_.addObserver(
279 this.context_.curDirEntry,
280 MetadataCache.CHILDREN,
282 this.updateThumbnails_.bind(this));
284 this.volumeManager_.addEventListener(
285 'externally-unmounted', this.onExternallyUnmountedBound_);
289 * Closes gallery when a volume containing the selected item is unmounted.
290 * @param {!Event} event The unmount event.
293 Gallery.prototype.onExternallyUnmounted_ = function(event) {
294 if (!this.selectedEntry_)
297 if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
304 * Unloads the Gallery.
305 * @param {boolean} exiting True if the app is exiting.
307 Gallery.prototype.onUnload = function(exiting) {
308 if (this.metadataCacheObserverId_ !== null)
309 this.metadataCache_.removeObserver(this.metadataCacheObserverId_);
310 this.volumeManager_.removeEventListener(
311 'externally-unmounted', this.onExternallyUnmountedBound_);
312 this.slideMode_.onUnload(exiting);
319 Gallery.prototype.initDom_ = function() {
320 // Initialize the dialog label.
321 cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
322 cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
324 var content = document.querySelector('#content');
325 content.addEventListener('click', this.onContentClick_.bind(this));
327 this.header_ = document.querySelector('#header');
328 this.toolbar_ = document.querySelector('#toolbar');
330 var preventDefault = function(event) { event.preventDefault(); };
332 var minimizeButton = util.createChild(this.header_,
333 'minimize-button tool dimmable',
335 minimizeButton.tabIndex = -1;
336 minimizeButton.addEventListener('click', this.onMinimize_.bind(this));
337 minimizeButton.addEventListener('mousedown', preventDefault);
339 var maximizeButton = util.createChild(this.header_,
340 'maximize-button tool dimmable',
342 maximizeButton.tabIndex = -1;
343 maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
344 maximizeButton.addEventListener('mousedown', preventDefault);
346 var closeButton = util.createChild(this.header_,
347 'close-button tool dimmable',
349 closeButton.tabIndex = -1;
350 closeButton.addEventListener('click', this.onClose_.bind(this));
351 closeButton.addEventListener('mousedown', preventDefault);
353 this.filenameSpacer_ = this.toolbar_.querySelector('.filename-spacer');
354 this.filenameEdit_ = util.createChild(this.filenameSpacer_,
357 this.filenameEdit_.setAttribute('type', 'text');
358 this.filenameEdit_.addEventListener('blur',
359 this.onFilenameEditBlur_.bind(this));
361 this.filenameEdit_.addEventListener('focus',
362 this.onFilenameFocus_.bind(this));
364 this.filenameEdit_.addEventListener('keydown',
365 this.onFilenameEditKeydown_.bind(this));
367 var middleSpacer = this.filenameSpacer_ =
368 this.toolbar_.querySelector('.middle-spacer');
369 var buttonSpacer = this.toolbar_.querySelector('button-spacer');
371 this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
373 this.modeButton_ = this.toolbar_.querySelector('button.mode');
374 this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null));
376 this.mosaicMode_ = new MosaicMode(content,
378 this.selectionModel_,
380 this.toggleMode_.bind(this, null));
382 this.slideMode_ = new SlideMode(this.container_,
387 this.selectionModel_,
390 this.toggleMode_.bind(this),
393 this.slideMode_.addEventListener('image-displayed', function() {
394 cr.dispatchSimpleEvent(this, 'image-displayed');
396 this.slideMode_.addEventListener('image-saved', function() {
397 cr.dispatchSimpleEvent(this, 'image-saved');
400 var deleteButton = this.initToolbarButton_('delete', 'GALLERY_DELETE');
401 deleteButton.addEventListener('click', this.delete_.bind(this));
403 this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
404 this.shareButton_.addEventListener(
405 'click', this.onShareButtonClick_.bind(this));
407 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
408 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
410 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
411 this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
413 this.shareDialog_ = new ShareDialog(this.container_);
417 * Initializes a toolbar button.
419 * @param {string} className Class to add.
420 * @param {string} title Button title.
421 * @return {!HTMLElement} Newly created button.
424 Gallery.prototype.initToolbarButton_ = function(className, title) {
425 var button = this.toolbar_.querySelector('button.' + className);
426 button.title = str(title);
433 * @param {!Array.<Entry>} entries Array of entries.
434 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
436 Gallery.prototype.load = function(entries, selectedEntries) {
437 // Obtains max chank size.
438 var maxChunkSize = 20;
439 var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
441 volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) {
444 if (volumeInfo.isReadOnly)
445 this.context_.readonlyDirName = volumeInfo.label;
447 // Make loading list.
449 for (var i = 0; i < entries.length; i++) {
450 var entry = entries[i];
451 entrySet[entry.toURL()] = {
457 for (var i = 0; i < selectedEntries.length; i++) {
458 var entry = selectedEntries[i];
459 entrySet[entry.toURL()] = {
465 var loadingList = [];
466 for (var url in entrySet) {
467 loadingList.push(entrySet[url]);
469 loadingList = loadingList.sort(function(a, b) {
470 if (a.selected && !b.selected)
472 else if (!a.selected && b.selected)
475 return a.index - b.index;
479 // Use the self variable capture-by-closure because it is faster than bind.
481 var loadChunk = function(firstChunk) {
483 var chunk = loadingList.splice(0, maxChunkSize);
487 return new Promise(function(fulfill) {
488 // Obtains metadata for chunk.
489 var entries = chunk.map(function(chunkItem) {
490 return chunkItem.entry;
492 self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill);
493 }).then(function(metadataList) {
494 if (chunk.length !== metadataList.length)
495 return Promise.reject('Failed to load metadata.');
497 // Add items to the model.
499 chunk.forEach(function(chunkItem, index) {
500 var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
501 if (!locationInfo) // Skip the item, since gone.
503 var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]);
504 items.push(new Gallery.Item(
509 /* original */ true));
511 self.dataModel_.push.apply(self.dataModel_, items);
513 // Apply the selection.
514 var selectionUpdated = false;
515 for (var i = 0; i < chunk.length; i++) {
516 if (!chunk[i].selected)
518 var index = self.dataModel_.indexOf(items[i]);
521 self.selectionModel_.setIndexSelected(index, true);
522 selectionUpdated = true;
524 if (selectionUpdated)
527 // Init modes after the first chunk is loaded.
529 // Determine the initial mode.
530 var shouldShowMosaic = selectedEntries.length > 1 ||
531 (self.context_.pageState &&
532 self.context_.pageState.gallery === 'mosaic');
533 self.setCurrentMode_(
534 shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
537 var mosaic = self.mosaicMode_.getMosaic();
540 // Do the initialization for each mode.
541 if (shouldShowMosaic) {
543 self.inactivityWatcher_.check(); // Show the toolbar.
544 cr.dispatchSimpleEvent(self, 'loaded');
546 self.slideMode_.enter(
549 // Flash the toolbar briefly to show it is there.
550 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
553 cr.dispatchSimpleEvent(self, 'loaded');
558 // Continue to load chunks.
559 return loadChunk(/* firstChunk */ false);
562 loadChunk(/* firstChunk */ true).catch(function(error) {
563 console.error(error.stack || error);
568 * Handles user's 'Close' action.
571 Gallery.prototype.onClose_ = function() {
572 this.executeWhenReady(this.context_.onClose);
576 * Handles user's 'Maximize' action (Escape or a click on the X icon).
579 Gallery.prototype.onMaximize_ = function() {
580 this.executeWhenReady(this.context_.onMaximize);
584 * Handles user's 'Maximize' action (Escape or a click on the X icon).
587 Gallery.prototype.onMinimize_ = function() {
588 this.executeWhenReady(this.context_.onMinimize);
592 * Executes a function when the editor is done with the modifications.
593 * @param {function} callback Function to execute.
595 Gallery.prototype.executeWhenReady = function(callback) {
596 this.currentMode_.executeWhenReady(callback);
600 * @return {Object} File manager private API.
602 Gallery.getFileManagerPrivate = function() {
603 return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
607 * @return {boolean} True if some tool is currently active.
609 Gallery.prototype.hasActiveTool = function() {
610 return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
615 * External user action event handler.
618 Gallery.prototype.onUserAction_ = function() {
619 // Show the toolbar and hide it after the default timeout.
620 this.inactivityWatcher_.kick();
624 * Sets the current mode, update the UI.
625 * @param {Object} mode Current mode.
628 Gallery.prototype.setCurrentMode_ = function(mode) {
629 if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
630 console.error('Invalid Gallery mode');
632 this.currentMode_ = mode;
633 this.container_.setAttribute('mode', this.currentMode_.getName());
634 this.updateSelectionAndState_();
635 this.updateButtons_();
639 * Mode toggle event handler.
640 * @param {function=} opt_callback Callback.
641 * @param {Event=} opt_event Event that caused this call.
644 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
645 if (!this.modeButton_)
648 if (this.changingMode_) // Do not re-enter while changing the mode.
652 this.onUserAction_();
654 this.changingMode_ = true;
656 var onModeChanged = function() {
657 this.changingMode_ = false;
658 if (opt_callback) opt_callback();
661 var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
663 var mosaic = this.mosaicMode_.getMosaic();
664 var tileRect = mosaic.getTileRect(tileIndex);
666 if (this.currentMode_ === this.slideMode_) {
667 this.setCurrentMode_(this.mosaicMode_);
669 tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
670 this.slideMode_.leave(
673 // Animate back to normal position.
679 this.setCurrentMode_(this.slideMode_);
680 this.slideMode_.enter(
683 // Animate to zoomed position.
684 mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
692 * Deletes the selected items.
695 Gallery.prototype.delete_ = function() {
696 this.onUserAction_();
698 // Clone the sorted selected indexes array.
699 var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
700 if (!indexesToRemove.length)
703 /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
705 var itemsToRemove = this.getSelectedItems();
706 var plural = itemsToRemove.length > 1;
707 var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
709 function deleteNext() {
710 if (!itemsToRemove.length)
711 return; // All deleted.
713 var entry = itemsToRemove.pop().getEntry();
714 entry.remove(deleteNext, function() {
715 util.flog('Error deleting: ' + entry.name, deleteNext);
719 // Prevent the Gallery from handling Esc and Enter.
720 this.document_.body.removeEventListener('keydown', this.keyDownBound_);
721 var restoreListener = function() {
722 this.document_.body.addEventListener('keydown', this.keyDownBound_);
726 var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
727 confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
728 confirm.show(strf(plural ?
729 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
732 this.selectionModel_.unselectAll();
733 this.selectionModel_.leadIndex = -1;
734 // Remove items from the data model, starting from the highest index.
735 while (indexesToRemove.length)
736 this.dataModel_.splice(indexesToRemove.pop(), 1);
737 // Delete actual files.
741 // Restore the listener after a timeout so that ESC is processed.
742 setTimeout(restoreListener, 0);
747 * @return {Array.<Gallery.Item>} Current selection.
749 Gallery.prototype.getSelectedItems = function() {
750 return this.selectionModel_.selectedIndexes.map(
751 this.dataModel_.item.bind(this.dataModel_));
755 * @return {Array.<Entry>} Array of currently selected entries.
757 Gallery.prototype.getSelectedEntries = function() {
758 return this.selectionModel_.selectedIndexes.map(function(index) {
759 return this.dataModel_.item(index).getEntry();
764 * @return {?Gallery.Item} Current single selection.
766 Gallery.prototype.getSingleSelectedItem = function() {
767 var items = this.getSelectedItems();
768 if (items.length > 1) {
769 console.error('Unexpected multiple selection');
776 * Selection change event handler.
779 Gallery.prototype.onSelection_ = function() {
780 this.updateSelectionAndState_();
784 * Data model splice event handler.
787 Gallery.prototype.onSplice_ = function() {
788 this.selectionModel_.adjustLength(this.dataModel_.length);
792 * Content change event handler.
793 * @param {Event} event Event.
796 Gallery.prototype.onContentChange_ = function(event) {
797 var index = this.dataModel_.indexOf(event.item);
798 if (index !== this.selectionModel_.selectedIndex)
799 console.error('Content changed for unselected item');
800 this.updateSelectionAndState_();
806 * @param {Event} event Event.
809 Gallery.prototype.onKeyDown_ = function(event) {
810 if (this.currentMode_.onKeyDown(event))
813 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
814 case 'U+0008': // Backspace.
815 // The default handler would call history.back and close the Gallery.
816 event.preventDefault();
819 case 'U+004D': // 'm' switches between Slide and Mosaic mode.
820 this.toggleMode_(null, event);
823 case 'U+0056': // 'v'
824 case 'MediaPlayPause':
825 this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
828 case 'U+007F': // Delete
829 case 'Shift-U+0033': // Shift+'3' (Delete key might be missing).
830 case 'U+0044': // 'd'
836 // Name box and rename support.
839 * Updates the UI related to the selected item and the persistent state.
843 Gallery.prototype.updateSelectionAndState_ = function() {
844 var numSelectedItems = this.selectionModel_.selectedIndexes.length;
845 var selectedEntryURL = null;
847 // If it's selecting something, update the variable values.
848 if (numSelectedItems) {
849 // Obtains selected item.
851 this.dataModel_.item(this.selectionModel_.selectedIndex);
852 this.selectedEntry_ = selectedItem.getEntry();
853 selectedEntryURL = this.selectedEntry_.toURL();
856 selectedItem.touch();
857 this.dataModel_.evictCache();
859 // Update the title and the display name.
860 if (numSelectedItems === 1) {
861 document.title = this.selectedEntry_.name;
862 this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
863 this.filenameEdit_.value =
864 ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
865 this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
867 if (this.context_.curDirEntry) {
868 // If the Gallery was opened on search results the search query will not
869 // be recorded in the app state and the relaunch will just open the
870 // gallery in the curDirEntry directory.
871 document.title = this.context_.curDirEntry.name;
875 this.filenameEdit_.disabled = true;
876 this.filenameEdit_.value =
877 strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
878 this.shareButton_.hidden = true;
882 this.filenameEdit_.disabled = true;
883 this.filenameEdit_.value = '';
884 this.shareButton_.hidden = true;
888 null, // Keep the current directory.
889 selectedEntryURL, // Update the selection.
890 {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
894 * Click event handler on filename edit box
897 Gallery.prototype.onFilenameFocus_ = function() {
898 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
899 this.filenameEdit_.originalValue = this.filenameEdit_.value;
900 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
901 this.onUserAction_();
905 * Blur event handler on filename edit box.
907 * @param {Event} event Blur event.
908 * @return {Promise} Promise fulfilled on renaming completed.
911 Gallery.prototype.onFilenameEditBlur_ = function(event) {
912 var item = this.getSingleSelectedItem();
914 var oldEntry = item.getEntry();
916 item.rename(this.filenameEdit_.value).then(function() {
917 var event = new Event('content');
919 event.oldEntry = oldEntry;
920 event.metadata = null; // Metadata unchanged.
921 this.dataModel_.dispatchEvent(event);
922 }.bind(this), function(error) {
923 if (error === 'NOT_CHANGED')
924 return Promise.resolve();
925 this.filenameEdit_.value =
926 ImageUtil.getDisplayNameFromName(item.getEntry().name);
927 this.filenameEdit_.focus();
928 if (typeof error === 'string')
929 this.prompt_.showStringAt('center', error, 5000);
931 return Promise.reject(error);
932 }.bind(this)).catch(function(error) {
933 console.error(error.stack || error);
937 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
938 this.onUserAction_();
939 return Promise.resolve();
943 * Keydown event handler on filename edit box
946 Gallery.prototype.onFilenameEditKeydown_ = function() {
947 switch (event.keyCode) {
949 this.filenameEdit_.value = this.filenameEdit_.originalValue;
950 this.filenameEdit_.blur();
954 this.filenameEdit_.blur();
957 event.stopPropagation();
961 * @return {boolean} True if file renaming is currently in progress.
964 Gallery.prototype.isRenaming_ = function() {
965 return this.filenameSpacer_.hasAttribute('renaming');
969 * Content area click handler.
972 Gallery.prototype.onContentClick_ = function() {
973 this.filenameEdit_.blur();
977 * Share button handler.
980 Gallery.prototype.onShareButtonClick_ = function() {
981 var item = this.getSingleSelectedItem();
984 this.shareDialog_.show(item.getEntry(), function() {});
988 * Updates thumbnails.
991 Gallery.prototype.updateThumbnails_ = function() {
992 if (this.currentMode_ === this.slideMode_)
993 this.slideMode_.updateThumbnails();
995 if (this.mosaicMode_) {
996 var mosaic = this.mosaicMode_.getMosaic();
997 if (mosaic.isInitialized())
1006 Gallery.prototype.updateButtons_ = function() {
1007 if (this.modeButton_) {
1009 this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
1011 this.modeButton_.title = str(oppositeMode.getTitle());
1016 * Singleton gallery.
1022 * Initialize the window.
1023 * @param {Object} backgroundComponents Background components.
1025 window.initialize = function(backgroundComponents) {
1026 window.loadTimeData.data = backgroundComponents.stringData;
1027 gallery = new Gallery(backgroundComponents.volumeManager);
1032 * @param {!Array.<Entry>} entries Array of entries.
1033 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
1035 window.loadEntries = function(entries, selectedEntries) {
1036 gallery.load(entries, selectedEntries);