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 {HTMLCanvasElement} 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 = 2000;
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.errorBanner_ = new ErrorBanner(this.container_);
375 this.modeButton_ = this.toolbar_.querySelector('button.mode');
376 this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null));
378 this.mosaicMode_ = new MosaicMode(content,
381 this.selectionModel_,
383 this.toggleMode_.bind(this, null));
385 this.slideMode_ = new SlideMode(this.container_,
391 this.selectionModel_,
394 this.toggleMode_.bind(this),
397 this.slideMode_.addEventListener('image-displayed', function() {
398 cr.dispatchSimpleEvent(this, 'image-displayed');
400 this.slideMode_.addEventListener('image-saved', function() {
401 cr.dispatchSimpleEvent(this, 'image-saved');
404 this.deleteButton_ = this.initToolbarButton_('delete', 'GALLERY_DELETE');
405 this.deleteButton_.addEventListener('click', this.delete_.bind(this));
407 this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
408 this.shareButton_.addEventListener(
409 'click', this.onShareButtonClick_.bind(this));
411 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
412 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
414 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
415 this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
417 this.shareDialog_ = new ShareDialog(this.container_);
421 * Initializes a toolbar button.
423 * @param {string} className Class to add.
424 * @param {string} title Button title.
425 * @return {!HTMLElement} Newly created button.
428 Gallery.prototype.initToolbarButton_ = function(className, title) {
429 var button = this.toolbar_.querySelector('button.' + className);
430 button.title = str(title);
437 * @param {!Array.<Entry>} entries Array of entries.
438 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
440 Gallery.prototype.load = function(entries, selectedEntries) {
441 // Obtains max chank size.
442 var maxChunkSize = 20;
443 var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
445 volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) {
448 if (volumeInfo.isReadOnly)
449 this.context_.readonlyDirName = volumeInfo.label;
451 // Make loading list.
453 for (var i = 0; i < entries.length; i++) {
454 var entry = entries[i];
455 entrySet[entry.toURL()] = {
461 for (var i = 0; i < selectedEntries.length; i++) {
462 var entry = selectedEntries[i];
463 entrySet[entry.toURL()] = {
469 var loadingList = [];
470 for (var url in entrySet) {
471 loadingList.push(entrySet[url]);
473 loadingList = loadingList.sort(function(a, b) {
474 if (a.selected && !b.selected)
476 else if (!a.selected && b.selected)
479 return a.index - b.index;
483 // Use the self variable capture-by-closure because it is faster than bind.
485 var loadChunk = function(firstChunk) {
487 var chunk = loadingList.splice(0, maxChunkSize);
491 return new Promise(function(fulfill) {
492 // Obtains metadata for chunk.
493 var entries = chunk.map(function(chunkItem) {
494 return chunkItem.entry;
496 self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill);
497 }).then(function(metadataList) {
498 if (chunk.length !== metadataList.length)
499 return Promise.reject('Failed to load metadata.');
501 // Add items to the model.
503 chunk.forEach(function(chunkItem, index) {
504 var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
505 if (!locationInfo) // Skip the item, since gone.
507 var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]);
508 items.push(new Gallery.Item(
513 /* original */ true));
515 self.dataModel_.push.apply(self.dataModel_, items);
517 // Apply the selection.
518 var selectionUpdated = false;
519 for (var i = 0; i < chunk.length; i++) {
520 if (!chunk[i].selected)
522 var index = self.dataModel_.indexOf(items[i]);
525 self.selectionModel_.setIndexSelected(index, true);
526 selectionUpdated = true;
528 if (selectionUpdated)
531 // Init modes after the first chunk is loaded.
533 // Determine the initial mode.
534 var shouldShowMosaic = selectedEntries.length > 1 ||
535 (self.context_.pageState &&
536 self.context_.pageState.gallery === 'mosaic');
537 self.setCurrentMode_(
538 shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
541 var mosaic = self.mosaicMode_.getMosaic();
544 // Do the initialization for each mode.
545 if (shouldShowMosaic) {
547 self.inactivityWatcher_.check(); // Show the toolbar.
548 cr.dispatchSimpleEvent(self, 'loaded');
550 self.slideMode_.enter(
553 // Flash the toolbar briefly to show it is there.
554 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
557 cr.dispatchSimpleEvent(self, 'loaded');
562 // Continue to load chunks.
563 return loadChunk(/* firstChunk */ false);
566 loadChunk(/* firstChunk */ true).catch(function(error) {
567 console.error(error.stack || error);
572 * Handles user's 'Close' action.
575 Gallery.prototype.onClose_ = function() {
576 this.executeWhenReady(this.context_.onClose);
580 * Handles user's 'Maximize' action (Escape or a click on the X icon).
583 Gallery.prototype.onMaximize_ = function() {
584 this.executeWhenReady(this.context_.onMaximize);
588 * Handles user's 'Maximize' action (Escape or a click on the X icon).
591 Gallery.prototype.onMinimize_ = function() {
592 this.executeWhenReady(this.context_.onMinimize);
596 * Executes a function when the editor is done with the modifications.
597 * @param {function()} callback Function to execute.
599 Gallery.prototype.executeWhenReady = function(callback) {
600 this.currentMode_.executeWhenReady(callback);
604 * @return {Object} File manager private API.
606 Gallery.getFileManagerPrivate = function() {
607 return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
611 * @return {boolean} True if some tool is currently active.
613 Gallery.prototype.hasActiveTool = function() {
614 return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
619 * External user action event handler.
622 Gallery.prototype.onUserAction_ = function() {
623 // Show the toolbar and hide it after the default timeout.
624 this.inactivityWatcher_.kick();
628 * Sets the current mode, update the UI.
629 * @param {Object} mode Current mode.
632 Gallery.prototype.setCurrentMode_ = function(mode) {
633 if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
634 console.error('Invalid Gallery mode');
636 this.currentMode_ = mode;
637 this.container_.setAttribute('mode', this.currentMode_.getName());
638 this.updateSelectionAndState_();
639 this.updateButtons_();
643 * Mode toggle event handler.
644 * @param {function()=} opt_callback Callback.
645 * @param {Event=} opt_event Event that caused this call.
648 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
649 if (!this.modeButton_)
652 if (this.changingMode_) // Do not re-enter while changing the mode.
656 this.onUserAction_();
658 this.changingMode_ = true;
660 var onModeChanged = function() {
661 this.changingMode_ = false;
662 if (opt_callback) opt_callback();
665 var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
667 var mosaic = this.mosaicMode_.getMosaic();
668 var tileRect = mosaic.getTileRect(tileIndex);
670 if (this.currentMode_ === this.slideMode_) {
671 this.setCurrentMode_(this.mosaicMode_);
673 tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
674 this.slideMode_.leave(
677 // Animate back to normal position.
683 this.setCurrentMode_(this.slideMode_);
684 this.slideMode_.enter(
687 // Animate to zoomed position.
688 mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
696 * Deletes the selected items.
699 Gallery.prototype.delete_ = function() {
700 this.onUserAction_();
702 // Clone the sorted selected indexes array.
703 var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
704 if (!indexesToRemove.length)
707 /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
709 var itemsToRemove = this.getSelectedItems();
710 var plural = itemsToRemove.length > 1;
711 var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
713 function deleteNext() {
714 if (!itemsToRemove.length)
715 return; // All deleted.
717 var entry = itemsToRemove.pop().getEntry();
718 entry.remove(deleteNext, function() {
719 console.error('Error deleting: ' + entry.name);
724 // Prevent the Gallery from handling Esc and Enter.
725 this.document_.body.removeEventListener('keydown', this.keyDownBound_);
726 var restoreListener = function() {
727 this.document_.body.addEventListener('keydown', this.keyDownBound_);
731 var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
732 confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
733 confirm.show(strf(plural ?
734 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
737 this.selectionModel_.unselectAll();
738 this.selectionModel_.leadIndex = -1;
739 // Remove items from the data model, starting from the highest index.
740 while (indexesToRemove.length)
741 this.dataModel_.splice(indexesToRemove.pop(), 1);
742 // Delete actual files.
746 // Restore the listener after a timeout so that ESC is processed.
747 setTimeout(restoreListener, 0);
752 * @return {Array.<Gallery.Item>} Current selection.
754 Gallery.prototype.getSelectedItems = function() {
755 return this.selectionModel_.selectedIndexes.map(
756 this.dataModel_.item.bind(this.dataModel_));
760 * @return {Array.<Entry>} Array of currently selected entries.
762 Gallery.prototype.getSelectedEntries = function() {
763 return this.selectionModel_.selectedIndexes.map(function(index) {
764 return this.dataModel_.item(index).getEntry();
769 * @return {?Gallery.Item} Current single selection.
771 Gallery.prototype.getSingleSelectedItem = function() {
772 var items = this.getSelectedItems();
773 if (items.length > 1) {
774 console.error('Unexpected multiple selection');
781 * Selection change event handler.
784 Gallery.prototype.onSelection_ = function() {
785 this.updateSelectionAndState_();
789 * Data model splice event handler.
792 Gallery.prototype.onSplice_ = function() {
793 this.selectionModel_.adjustLength(this.dataModel_.length);
797 * Content change event handler.
798 * @param {Event} event Event.
801 Gallery.prototype.onContentChange_ = function(event) {
802 var index = this.dataModel_.indexOf(event.item);
803 if (index !== this.selectionModel_.selectedIndex)
804 console.error('Content changed for unselected item');
805 this.updateSelectionAndState_();
811 * @param {Event} event Event.
814 Gallery.prototype.onKeyDown_ = function(event) {
815 if (this.currentMode_.onKeyDown(event))
818 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
819 case 'U+0008': // Backspace.
820 // The default handler would call history.back and close the Gallery.
821 event.preventDefault();
824 case 'U+004D': // 'm' switches between Slide and Mosaic mode.
825 this.toggleMode_(null, event);
828 case 'U+0056': // 'v'
829 case 'MediaPlayPause':
830 this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
833 case 'U+007F': // Delete
834 case 'Shift-U+0033': // Shift+'3' (Delete key might be missing).
835 case 'U+0044': // 'd'
841 // Name box and rename support.
844 * Updates the UI related to the selected item and the persistent state.
848 Gallery.prototype.updateSelectionAndState_ = function() {
849 var numSelectedItems = this.selectionModel_.selectedIndexes.length;
850 var selectedEntryURL = null;
852 // If it's selecting something, update the variable values.
853 if (numSelectedItems) {
854 // Delete button is available when all images are NOT readOnly.
855 this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes
857 return !this.dataModel_.item(i).getLocationInfo().isReadOnly;
860 // Obtains selected item.
862 this.dataModel_.item(this.selectionModel_.selectedIndex);
863 this.selectedEntry_ = selectedItem.getEntry();
864 selectedEntryURL = this.selectedEntry_.toURL();
867 selectedItem.touch();
868 this.dataModel_.evictCache();
870 // Update the title and the display name.
871 if (numSelectedItems === 1) {
872 document.title = this.selectedEntry_.name;
873 this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
874 this.filenameEdit_.value =
875 ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
876 this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
878 if (this.context_.curDirEntry) {
879 // If the Gallery was opened on search results the search query will not
880 // be recorded in the app state and the relaunch will just open the
881 // gallery in the curDirEntry directory.
882 document.title = this.context_.curDirEntry.name;
886 this.filenameEdit_.disabled = true;
887 this.filenameEdit_.value =
888 strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
889 this.shareButton_.hidden = true;
893 this.filenameEdit_.disabled = true;
894 this.deleteButton_.disabled = true;
895 this.filenameEdit_.value = '';
896 this.shareButton_.hidden = true;
900 null, // Keep the current directory.
901 selectedEntryURL, // Update the selection.
902 {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
906 * Click event handler on filename edit box
909 Gallery.prototype.onFilenameFocus_ = function() {
910 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
911 this.filenameEdit_.originalValue = this.filenameEdit_.value;
912 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
913 this.onUserAction_();
917 * Blur event handler on filename edit box.
919 * @param {Event} event Blur event.
920 * @return {Promise} Promise fulfilled on renaming completed.
923 Gallery.prototype.onFilenameEditBlur_ = function(event) {
924 var item = this.getSingleSelectedItem();
926 var oldEntry = item.getEntry();
928 item.rename(this.filenameEdit_.value).then(function() {
929 var event = new Event('content');
931 event.oldEntry = oldEntry;
932 event.metadata = null; // Metadata unchanged.
933 this.dataModel_.dispatchEvent(event);
934 }.bind(this), function(error) {
935 if (error === 'NOT_CHANGED')
936 return Promise.resolve();
937 this.filenameEdit_.value =
938 ImageUtil.getDisplayNameFromName(item.getEntry().name);
939 this.filenameEdit_.focus();
940 if (typeof error === 'string')
941 this.prompt_.showStringAt('center', error, 5000);
943 return Promise.reject(error);
944 }.bind(this)).catch(function(error) {
945 console.error(error.stack || error);
949 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
950 this.onUserAction_();
951 return Promise.resolve();
955 * Keydown event handler on filename edit box
958 Gallery.prototype.onFilenameEditKeydown_ = function() {
959 switch (event.keyCode) {
961 this.filenameEdit_.value = this.filenameEdit_.originalValue;
962 this.filenameEdit_.blur();
966 this.filenameEdit_.blur();
969 event.stopPropagation();
973 * @return {boolean} True if file renaming is currently in progress.
976 Gallery.prototype.isRenaming_ = function() {
977 return this.filenameSpacer_.hasAttribute('renaming');
981 * Content area click handler.
984 Gallery.prototype.onContentClick_ = function() {
985 this.filenameEdit_.blur();
989 * Share button handler.
992 Gallery.prototype.onShareButtonClick_ = function() {
993 var item = this.getSingleSelectedItem();
996 this.shareDialog_.show(item.getEntry(), function() {});
1000 * Updates thumbnails.
1003 Gallery.prototype.updateThumbnails_ = function() {
1004 if (this.currentMode_ === this.slideMode_)
1005 this.slideMode_.updateThumbnails();
1007 if (this.mosaicMode_) {
1008 var mosaic = this.mosaicMode_.getMosaic();
1009 if (mosaic.isInitialized())
1018 Gallery.prototype.updateButtons_ = function() {
1019 if (this.modeButton_) {
1021 this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
1023 this.modeButton_.title = str(oppositeMode.getTitle());
1028 * Singleton gallery.
1034 * Initialize the window.
1035 * @param {Object} backgroundComponents Background components.
1037 window.initialize = function(backgroundComponents) {
1038 window.loadTimeData.data = backgroundComponents.stringData;
1039 gallery = new Gallery(backgroundComponents.volumeManager);
1044 * @param {!Array.<Entry>} entries Array of entries.
1045 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
1047 window.loadEntries = function(entries, selectedEntries) {
1048 gallery.load(entries, selectedEntries);