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.
6 * Called from the main frame when unloading.
7 * @param {boolean=} opt_exiting True if the app is exiting.
9 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting); }
12 * Overrided metadata worker's path.
16 ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
19 * Data model for gallery.
21 * @param {MetadataCache} metadataCache Metadata cache.
23 * @extends {cr.ui.ArrayDataModel}
25 function GalleryDataModel(metadataCache) {
26 cr.ui.ArrayDataModel.call(this, []);
30 * @type {MetadataCache}
33 this.metadataCache_ = metadataCache;
36 * Directory where the image is saved if the image is located in a read-only
38 * @type {DirectoryEntry}
40 this.fallbackSaveDirectory = null;
44 * Maximum number of full size image cache.
49 GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3;
52 * Maximum number of screen size image cache.
57 GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5;
59 GalleryDataModel.prototype = {
60 __proto__: cr.ui.ArrayDataModel.prototype
66 * @param {VolumeManager} volumeManager Volume manager instance.
67 * @param {Gallery.Item} item Original gallery item.
68 * @param {HTMLCanvasElement} canvas Canvas containing new image.
69 * @param {boolean} overwrite Whether to overwrite the image to the item or not.
70 * @return {Promise} Promise to be fulfilled with when the operation completes.
72 GalleryDataModel.prototype.saveItem = function(
73 volumeManager, item, canvas, overwrite) {
74 var oldEntry = item.getEntry();
75 var oldMetadata = item.getMetadata();
76 var oldLocationInfo = item.getLocationInfo();
77 var metadataEncoder = ImageEncoder.encodeMetadata(
78 item.getMetadata(), canvas, 1 /* quality */);
79 var newMetadata = ContentProvider.ConvertContentMetadata(
80 metadataEncoder.getMetadata(),
81 MetadataCache.cloneMetadata(item.getMetadata()));
82 if (newMetadata.filesystem)
83 newMetadata.filesystem.modificationTime = new Date();
84 if (newMetadata.external)
85 newMetadata.external.present = true;
87 return new Promise(function(fulfill, reject) {
90 this.fallbackSaveDirectory,
96 reject('Failed to save the image.');
100 // The item's entry is updated to the latest entry. Update metadata.
101 item.setMetadata(newMetadata);
103 // Current entry is updated.
104 // Dispatch an event.
105 var event = new Event('content');
107 event.oldEntry = oldEntry;
108 event.metadata = newMetadata;
109 this.dispatchEvent(event);
111 if (util.isSameEntry(oldEntry, item.getEntry())) {
112 // Need an update of metdataCache.
113 this.metadataCache_.set(
115 Gallery.METADATA_TYPE,
118 // New entry is added and the item now tracks it.
119 // Add another item for the old entry.
120 var anotherItem = new Gallery.Item(
126 // The item must be added behind the existing item so that it does
127 // not change the index of the existing item.
128 // TODO(hirono): Update the item index of the selection model
130 this.splice(this.indexOf(item) + 1, 0, anotherItem);
139 * Evicts image caches in the items.
140 * @param {Gallery.Item} currentSelectedItem Current selected item.
142 GalleryDataModel.prototype.evictCache = function(currentSelectedItem) {
143 // Sort the item by the last accessed date.
144 var sorted = this.slice().sort(function(a, b) {
145 return b.getLastAccessedDate() - a.getLastAccessedDate();
149 var contentCacheCount = 0;
150 var screenCacheCount = 0;
151 for (var i = 0; i < sorted.length; i++) {
152 if (sorted[i].contentImage) {
153 if (++contentCacheCount > GalleryDataModel.MAX_FULL_IMAGE_CACHE_) {
154 if (sorted[i].contentImage.parentNode) {
155 console.error('The content image has a parent node.');
157 // Force to free the buffer of the canvas by assigning zero size.
158 sorted[i].contentImage.width = 0;
159 sorted[i].contentImage.height = 0;
160 sorted[i].contentImage = null;
164 if (sorted[i].screenImage) {
165 if (++screenCacheCount > GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_) {
166 if (sorted[i].screenImage.parentNode) {
167 console.error('The screen image has a parent node.');
169 // Force to free the buffer of the canvas by assigning zero size.
170 sorted[i].screenImage.width = 0;
171 sorted[i].screenImage.height = 0;
172 sorted[i].screenImage = null;
180 * Gallery for viewing and editing image files.
182 * @param {!VolumeManager} volumeManager The VolumeManager instance of the
186 function Gallery(volumeManager) {
188 appWindow: chrome.app.window.current(),
189 onClose: function() { close(); },
190 onMaximize: function() {
191 var appWindow = chrome.app.window.current();
192 if (appWindow.isMaximized())
195 appWindow.maximize();
197 onMinimize: function() { chrome.app.window.current().minimize(); },
198 onAppRegionChanged: function() {},
199 metadataCache: MetadataCache.createFull(volumeManager),
201 displayStringFunction: function() { return ''; },
204 this.container_ = document.querySelector('.gallery');
205 this.document_ = document;
206 this.metadataCache_ = this.context_.metadataCache;
207 this.volumeManager_ = volumeManager;
208 this.selectedEntry_ = null;
209 this.metadataCacheObserverId_ = null;
210 this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this);
212 this.dataModel_ = new GalleryDataModel(
213 this.context_.metadataCache);
214 var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo(
215 VolumeManagerCommon.VolumeType.DOWNLOADS);
216 downloadVolumeInfo.resolveDisplayRoot().then(function(entry) {
217 this.dataModel_.fallbackSaveDirectory = entry;
218 }.bind(this)).catch(function(error) {
220 'Failed to obtain the fallback directory: ' + (error.stack || error));
222 this.selectionModel_ = new cr.ui.ListSelectionModel();
225 this.initListeners_();
229 * Gallery extends cr.EventTarget.
231 Gallery.prototype.__proto__ = cr.EventTarget.prototype;
234 * Tools fade-out timeout in milliseconds.
238 Gallery.FADE_TIMEOUT = 2000;
241 * First time tools fade-out timeout in milliseconds.
245 Gallery.FIRST_FADE_TIMEOUT = 1000;
248 * Time until mosaic is initialized in the background. Used to make gallery
249 * in the slide mode load faster. In milliseconds.
253 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
256 * Types of metadata Gallery uses (to query the metadata cache).
260 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|external';
263 * Initializes listeners.
266 Gallery.prototype.initListeners_ = function() {
267 this.keyDownBound_ = this.onKeyDown_.bind(this);
268 this.document_.body.addEventListener('keydown', this.keyDownBound_);
270 this.inactivityWatcher_ = new MouseInactivityWatcher(
271 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
273 // Search results may contain files from different subdirectories so
274 // the observer is not going to work.
275 if (!this.context_.searchResults && this.context_.curDirEntry) {
276 this.metadataCacheObserverId_ = this.metadataCache_.addObserver(
277 this.context_.curDirEntry,
278 MetadataCache.CHILDREN,
280 this.updateThumbnails_.bind(this));
282 this.volumeManager_.addEventListener(
283 'externally-unmounted', this.onExternallyUnmountedBound_);
287 * Closes gallery when a volume containing the selected item is unmounted.
288 * @param {!Event} event The unmount event.
291 Gallery.prototype.onExternallyUnmounted_ = function(event) {
292 if (!this.selectedEntry_)
295 if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
302 * Unloads the Gallery.
303 * @param {boolean} exiting True if the app is exiting.
305 Gallery.prototype.onUnload = function(exiting) {
306 if (this.metadataCacheObserverId_ !== null)
307 this.metadataCache_.removeObserver(this.metadataCacheObserverId_);
308 this.volumeManager_.removeEventListener(
309 'externally-unmounted', this.onExternallyUnmountedBound_);
310 this.slideMode_.onUnload(exiting);
317 Gallery.prototype.initDom_ = function() {
318 // Initialize the dialog label.
319 cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL');
320 cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL');
322 var content = document.querySelector('#content');
323 content.addEventListener('click', this.onContentClick_.bind(this));
325 this.header_ = document.querySelector('#header');
326 this.toolbar_ = document.querySelector('#toolbar');
328 var preventDefault = function(event) { event.preventDefault(); };
330 var minimizeButton = util.createChild(this.header_,
331 'minimize-button tool dimmable',
333 minimizeButton.tabIndex = -1;
334 minimizeButton.addEventListener('click', this.onMinimize_.bind(this));
335 minimizeButton.addEventListener('mousedown', preventDefault);
337 var maximizeButton = util.createChild(this.header_,
338 'maximize-button tool dimmable',
340 maximizeButton.tabIndex = -1;
341 maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
342 maximizeButton.addEventListener('mousedown', preventDefault);
344 var closeButton = util.createChild(this.header_,
345 'close-button tool dimmable',
347 closeButton.tabIndex = -1;
348 closeButton.addEventListener('click', this.onClose_.bind(this));
349 closeButton.addEventListener('mousedown', preventDefault);
351 this.filenameSpacer_ = this.toolbar_.querySelector('.filename-spacer');
352 this.filenameEdit_ = util.createChild(this.filenameSpacer_,
355 this.filenameEdit_.setAttribute('type', 'text');
356 this.filenameEdit_.addEventListener('blur',
357 this.onFilenameEditBlur_.bind(this));
359 this.filenameEdit_.addEventListener('focus',
360 this.onFilenameFocus_.bind(this));
362 this.filenameEdit_.addEventListener('keydown',
363 this.onFilenameEditKeydown_.bind(this));
365 var middleSpacer = this.filenameSpacer_ =
366 this.toolbar_.querySelector('.middle-spacer');
367 var buttonSpacer = this.toolbar_.querySelector('button-spacer');
369 this.prompt_ = new ImageEditor.Prompt(this.container_, strf);
371 this.errorBanner_ = new ErrorBanner(this.container_);
373 this.modeButton_ = this.toolbar_.querySelector('button.mode');
374 this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null));
376 this.mosaicMode_ = new MosaicMode(content,
379 this.selectionModel_,
381 this.toggleMode_.bind(this, null));
383 this.slideMode_ = new SlideMode(this.container_,
389 this.selectionModel_,
392 this.toggleMode_.bind(this),
395 this.slideMode_.addEventListener('image-displayed', function() {
396 cr.dispatchSimpleEvent(this, 'image-displayed');
398 this.slideMode_.addEventListener('image-saved', function() {
399 cr.dispatchSimpleEvent(this, 'image-saved');
402 this.deleteButton_ = this.initToolbarButton_('delete', 'GALLERY_DELETE');
403 this.deleteButton_.addEventListener('click', this.delete_.bind(this));
405 this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE');
406 this.shareButton_.addEventListener(
407 'click', this.onShareButtonClick_.bind(this));
409 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
410 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
412 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
413 this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
415 this.shareDialog_ = new ShareDialog(this.container_);
419 * Initializes a toolbar button.
421 * @param {string} className Class to add.
422 * @param {string} title Button title.
423 * @return {!HTMLElement} Newly created button.
426 Gallery.prototype.initToolbarButton_ = function(className, title) {
427 var button = this.toolbar_.querySelector('button.' + className);
428 button.title = str(title);
435 * @param {!Array.<Entry>} entries Array of entries.
436 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
438 Gallery.prototype.load = function(entries, selectedEntries) {
439 // Obtains max chank size.
440 var maxChunkSize = 20;
441 var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]);
443 volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) {
446 if (volumeInfo.isReadOnly)
447 this.context_.readonlyDirName = volumeInfo.label;
449 // Make loading list.
451 for (var i = 0; i < entries.length; i++) {
452 var entry = entries[i];
453 entrySet[entry.toURL()] = {
459 for (var i = 0; i < selectedEntries.length; i++) {
460 var entry = selectedEntries[i];
461 entrySet[entry.toURL()] = {
467 var loadingList = [];
468 for (var url in entrySet) {
469 loadingList.push(entrySet[url]);
471 loadingList = loadingList.sort(function(a, b) {
472 if (a.selected && !b.selected)
474 else if (!a.selected && b.selected)
477 return a.index - b.index;
481 // Use the self variable capture-by-closure because it is faster than bind.
483 var loadChunk = function(firstChunk) {
485 var chunk = loadingList.splice(0, maxChunkSize);
489 return new Promise(function(fulfill) {
490 // Obtains metadata for chunk.
491 var entries = chunk.map(function(chunkItem) {
492 return chunkItem.entry;
494 self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill);
495 }).then(function(metadataList) {
496 if (chunk.length !== metadataList.length)
497 return Promise.reject('Failed to load metadata.');
499 // Add items to the model.
501 chunk.forEach(function(chunkItem, index) {
502 var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry);
503 if (!locationInfo) // Skip the item, since gone.
505 var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]);
506 items.push(new Gallery.Item(
511 /* original */ true));
513 self.dataModel_.push.apply(self.dataModel_, items);
515 // Apply the selection.
516 var selectionUpdated = false;
517 for (var i = 0; i < chunk.length; i++) {
518 if (!chunk[i].selected)
520 var index = self.dataModel_.indexOf(items[i]);
523 self.selectionModel_.setIndexSelected(index, true);
524 selectionUpdated = true;
526 if (selectionUpdated)
529 // Init modes after the first chunk is loaded.
531 // Determine the initial mode.
532 var shouldShowMosaic = selectedEntries.length > 1 ||
533 (self.context_.pageState &&
534 self.context_.pageState.gallery === 'mosaic');
535 self.setCurrentMode_(
536 shouldShowMosaic ? self.mosaicMode_ : self.slideMode_);
539 var mosaic = self.mosaicMode_.getMosaic();
542 // Do the initialization for each mode.
543 if (shouldShowMosaic) {
545 self.inactivityWatcher_.check(); // Show the toolbar.
546 cr.dispatchSimpleEvent(self, 'loaded');
548 self.slideMode_.enter(
551 // Flash the toolbar briefly to show it is there.
552 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
555 cr.dispatchSimpleEvent(self, 'loaded');
560 // Continue to load chunks.
561 return loadChunk(/* firstChunk */ false);
564 loadChunk(/* firstChunk */ true).catch(function(error) {
565 console.error(error.stack || error);
570 * Handles user's 'Close' action.
573 Gallery.prototype.onClose_ = function() {
574 this.executeWhenReady(this.context_.onClose);
578 * Handles user's 'Maximize' action (Escape or a click on the X icon).
581 Gallery.prototype.onMaximize_ = function() {
582 this.executeWhenReady(this.context_.onMaximize);
586 * Handles user's 'Maximize' action (Escape or a click on the X icon).
589 Gallery.prototype.onMinimize_ = function() {
590 this.executeWhenReady(this.context_.onMinimize);
594 * Executes a function when the editor is done with the modifications.
595 * @param {function()} callback Function to execute.
597 Gallery.prototype.executeWhenReady = function(callback) {
598 this.currentMode_.executeWhenReady(callback);
602 * @return {Object} File manager private API.
604 Gallery.getFileManagerPrivate = function() {
605 return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate;
609 * @return {boolean} True if some tool is currently active.
611 Gallery.prototype.hasActiveTool = function() {
612 return (this.currentMode_ && this.currentMode_.hasActiveTool()) ||
617 * External user action event handler.
620 Gallery.prototype.onUserAction_ = function() {
621 // Show the toolbar and hide it after the default timeout.
622 this.inactivityWatcher_.kick();
626 * Sets the current mode, update the UI.
627 * @param {Object} mode Current mode.
630 Gallery.prototype.setCurrentMode_ = function(mode) {
631 if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
632 console.error('Invalid Gallery mode');
634 this.currentMode_ = mode;
635 this.container_.setAttribute('mode', this.currentMode_.getName());
636 this.updateSelectionAndState_();
637 this.updateButtons_();
641 * Mode toggle event handler.
642 * @param {function()=} opt_callback Callback.
643 * @param {Event=} opt_event Event that caused this call.
646 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
647 if (!this.modeButton_)
650 if (this.changingMode_) // Do not re-enter while changing the mode.
654 this.onUserAction_();
656 this.changingMode_ = true;
658 var onModeChanged = function() {
659 this.changingMode_ = false;
660 if (opt_callback) opt_callback();
663 var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
665 var mosaic = this.mosaicMode_.getMosaic();
666 var tileRect = mosaic.getTileRect(tileIndex);
668 if (this.currentMode_ === this.slideMode_) {
669 this.setCurrentMode_(this.mosaicMode_);
671 tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
672 this.slideMode_.leave(
675 // Animate back to normal position.
681 this.setCurrentMode_(this.slideMode_);
682 this.slideMode_.enter(
685 // Animate to zoomed position.
686 mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
694 * Deletes the selected items.
697 Gallery.prototype.delete_ = function() {
698 this.onUserAction_();
700 // Clone the sorted selected indexes array.
701 var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
702 if (!indexesToRemove.length)
705 /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
707 var itemsToRemove = this.getSelectedItems();
708 var plural = itemsToRemove.length > 1;
709 var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
711 function deleteNext() {
712 if (!itemsToRemove.length)
713 return; // All deleted.
715 var entry = itemsToRemove.pop().getEntry();
716 entry.remove(deleteNext, function() {
717 console.error('Error deleting: ' + entry.name);
722 // Prevent the Gallery from handling Esc and Enter.
723 this.document_.body.removeEventListener('keydown', this.keyDownBound_);
724 var restoreListener = function() {
725 this.document_.body.addEventListener('keydown', this.keyDownBound_);
729 var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
730 confirm.setOkLabel(str('DELETE_BUTTON_LABEL'));
731 confirm.show(strf(plural ?
732 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param),
735 this.selectionModel_.unselectAll();
736 this.selectionModel_.leadIndex = -1;
737 // Remove items from the data model, starting from the highest index.
738 while (indexesToRemove.length)
739 this.dataModel_.splice(indexesToRemove.pop(), 1);
740 // Delete actual files.
744 // Restore the listener after a timeout so that ESC is processed.
745 setTimeout(restoreListener, 0);
750 * @return {Array.<Gallery.Item>} Current selection.
752 Gallery.prototype.getSelectedItems = function() {
753 return this.selectionModel_.selectedIndexes.map(
754 this.dataModel_.item.bind(this.dataModel_));
758 * @return {Array.<Entry>} Array of currently selected entries.
760 Gallery.prototype.getSelectedEntries = function() {
761 return this.selectionModel_.selectedIndexes.map(function(index) {
762 return this.dataModel_.item(index).getEntry();
767 * @return {?Gallery.Item} Current single selection.
769 Gallery.prototype.getSingleSelectedItem = function() {
770 var items = this.getSelectedItems();
771 if (items.length > 1) {
772 console.error('Unexpected multiple selection');
779 * Selection change event handler.
782 Gallery.prototype.onSelection_ = function() {
783 this.updateSelectionAndState_();
787 * Data model splice event handler.
790 Gallery.prototype.onSplice_ = function() {
791 this.selectionModel_.adjustLength(this.dataModel_.length);
795 * Content change event handler.
796 * @param {Event} event Event.
799 Gallery.prototype.onContentChange_ = function(event) {
800 var index = this.dataModel_.indexOf(event.item);
801 if (index !== this.selectionModel_.selectedIndex)
802 console.error('Content changed for unselected item');
803 this.updateSelectionAndState_();
809 * @param {Event} event Event.
812 Gallery.prototype.onKeyDown_ = function(event) {
813 if (this.currentMode_.onKeyDown(event))
816 switch (util.getKeyModifiers(event) + event.keyIdentifier) {
817 case 'U+0008': // Backspace.
818 // The default handler would call history.back and close the Gallery.
819 event.preventDefault();
822 case 'U+004D': // 'm' switches between Slide and Mosaic mode.
823 this.toggleMode_(null, event);
826 case 'U+0056': // 'v'
827 case 'MediaPlayPause':
828 this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
831 case 'U+007F': // Delete
832 case 'Shift-U+0033': // Shift+'3' (Delete key might be missing).
833 case 'U+0044': // 'd'
839 // Name box and rename support.
842 * Updates the UI related to the selected item and the persistent state.
846 Gallery.prototype.updateSelectionAndState_ = function() {
847 var numSelectedItems = this.selectionModel_.selectedIndexes.length;
848 var selectedEntryURL = null;
850 // If it's selecting something, update the variable values.
851 if (numSelectedItems) {
852 // Delete button is available when all images are NOT readOnly.
853 this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes
855 return !this.dataModel_.item(i).getLocationInfo().isReadOnly;
858 // Obtains selected item.
860 this.dataModel_.item(this.selectionModel_.selectedIndex);
861 this.selectedEntry_ = selectedItem.getEntry();
862 selectedEntryURL = this.selectedEntry_.toURL();
865 selectedItem.touch();
866 this.dataModel_.evictCache();
868 // Update the title and the display name.
869 if (numSelectedItems === 1) {
870 document.title = this.selectedEntry_.name;
871 this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly;
872 this.filenameEdit_.value =
873 ImageUtil.getDisplayNameFromName(this.selectedEntry_.name);
874 this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased;
876 if (this.context_.curDirEntry) {
877 // If the Gallery was opened on search results the search query will not
878 // be recorded in the app state and the relaunch will just open the
879 // gallery in the curDirEntry directory.
880 document.title = this.context_.curDirEntry.name;
884 this.filenameEdit_.disabled = true;
885 this.filenameEdit_.value =
886 strf('GALLERY_ITEMS_SELECTED', numSelectedItems);
887 this.shareButton_.hidden = true;
891 this.filenameEdit_.disabled = true;
892 this.deleteButton_.disabled = true;
893 this.filenameEdit_.value = '';
894 this.shareButton_.hidden = true;
898 null, // Keep the current directory.
899 selectedEntryURL, // Update the selection.
900 {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
904 * Click event handler on filename edit box
907 Gallery.prototype.onFilenameFocus_ = function() {
908 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
909 this.filenameEdit_.originalValue = this.filenameEdit_.value;
910 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
911 this.onUserAction_();
915 * Blur event handler on filename edit box.
917 * @param {Event} event Blur event.
918 * @return {Promise} Promise fulfilled on renaming completed.
921 Gallery.prototype.onFilenameEditBlur_ = function(event) {
922 var item = this.getSingleSelectedItem();
924 var oldEntry = item.getEntry();
926 item.rename(this.filenameEdit_.value).then(function() {
927 var event = new Event('content');
929 event.oldEntry = oldEntry;
930 event.metadata = null; // Metadata unchanged.
931 this.dataModel_.dispatchEvent(event);
932 }.bind(this), function(error) {
933 if (error === 'NOT_CHANGED')
934 return Promise.resolve();
935 this.filenameEdit_.value =
936 ImageUtil.getDisplayNameFromName(item.getEntry().name);
937 this.filenameEdit_.focus();
938 if (typeof error === 'string')
939 this.prompt_.showStringAt('center', error, 5000);
941 return Promise.reject(error);
942 }.bind(this)).catch(function(error) {
943 console.error(error.stack || error);
947 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
948 this.onUserAction_();
949 return Promise.resolve();
953 * Keydown event handler on filename edit box
956 Gallery.prototype.onFilenameEditKeydown_ = function() {
957 switch (event.keyCode) {
959 this.filenameEdit_.value = this.filenameEdit_.originalValue;
960 this.filenameEdit_.blur();
964 this.filenameEdit_.blur();
967 event.stopPropagation();
971 * @return {boolean} True if file renaming is currently in progress.
974 Gallery.prototype.isRenaming_ = function() {
975 return this.filenameSpacer_.hasAttribute('renaming');
979 * Content area click handler.
982 Gallery.prototype.onContentClick_ = function() {
983 this.filenameEdit_.blur();
987 * Share button handler.
990 Gallery.prototype.onShareButtonClick_ = function() {
991 var item = this.getSingleSelectedItem();
994 this.shareDialog_.show(item.getEntry(), function() {});
998 * Updates thumbnails.
1001 Gallery.prototype.updateThumbnails_ = function() {
1002 if (this.currentMode_ === this.slideMode_)
1003 this.slideMode_.updateThumbnails();
1005 if (this.mosaicMode_) {
1006 var mosaic = this.mosaicMode_.getMosaic();
1007 if (mosaic.isInitialized())
1016 Gallery.prototype.updateButtons_ = function() {
1017 if (this.modeButton_) {
1019 this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
1021 this.modeButton_.title = str(oppositeMode.getTitle());
1026 * Singleton gallery.
1032 * Initialize the window.
1033 * @param {Object} backgroundComponents Background components.
1035 window.initialize = function(backgroundComponents) {
1036 window.loadTimeData.data = backgroundComponents.stringData;
1037 gallery = new Gallery(backgroundComponents.volumeManager);
1042 * @param {!Array.<Entry>} entries Array of entries.
1043 * @param {!Array.<Entry>} selectedEntries Array of selected entries.
1045 window.loadEntries = function(entries, selectedEntries) {
1046 gallery.load(entries, selectedEntries);