| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * Overrided metadata worker's path. |
| * @type {string} |
| */ |
| ContentMetadataProvider.WORKER_SCRIPT = '/js/metadata_worker.js'; |
| |
| /** |
| * Gallery for viewing and editing image files. |
| * |
| * @param {!VolumeManagerWrapper} volumeManager |
| * @constructor |
| * @struct |
| */ |
| function Gallery(volumeManager) { |
| /** |
| * @type {{appWindow: chrome.app.window.AppWindow, readonlyDirName: string, |
| * displayStringFunction: function(), loadTimeData: Object}} |
| * @private |
| */ |
| this.context_ = { |
| appWindow: chrome.app.window.current(), |
| readonlyDirName: '', |
| displayStringFunction: function() { return ''; }, |
| loadTimeData: {}, |
| }; |
| this.container_ = queryRequiredElement('.gallery'); |
| this.document_ = document; |
| this.volumeManager_ = volumeManager; |
| /** |
| * @private {!MetadataModel} |
| * @const |
| */ |
| this.metadataModel_ = MetadataModel.create(volumeManager); |
| /** |
| * @private {!ThumbnailModel} |
| * @const |
| */ |
| this.thumbnailModel_ = new ThumbnailModel(this.metadataModel_); |
| this.selectedEntry_ = null; |
| this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this); |
| this.initialized_ = false; |
| |
| this.dataModel_ = new GalleryDataModel(this.metadataModel_); |
| var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo( |
| VolumeManagerCommon.VolumeType.DOWNLOADS); |
| downloadVolumeInfo.resolveDisplayRoot().then(function(entry) { |
| this.dataModel_.fallbackSaveDirectory = entry; |
| }.bind(this)).catch(function(error) { |
| console.error( |
| 'Failed to obtain the fallback directory: ' + (error.stack || error)); |
| }); |
| this.selectionModel_ = new cr.ui.ListSelectionModel(); |
| |
| /** |
| * @type {(SlideMode|ThumbnailMode)} |
| * @private |
| */ |
| this.currentMode_ = null; |
| |
| /** |
| * @type {boolean} |
| * @private |
| */ |
| this.changingMode_ = false; |
| |
| // ----------------------------------------------------------------- |
| // Initializes the UI. |
| |
| // Initialize the dialog label. |
| cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL'); |
| cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL'); |
| |
| var content = getRequiredElement('content'); |
| content.addEventListener('click', this.onContentClick_.bind(this)); |
| |
| this.topToolbar_ = getRequiredElement('top-toolbar'); |
| this.bottomToolbar_ = getRequiredElement('bottom-toolbar'); |
| |
| this.filenameSpacer_ = queryRequiredElement('.filename-spacer', |
| this.topToolbar_); |
| |
| /** |
| * @private {HTMLInputElement} |
| * @const |
| */ |
| this.filenameEdit_ = /** @type {HTMLInputElement} */ |
| (queryRequiredElement('input', this.filenameSpacer_)); |
| |
| this.filenameCanvas_ = document.createElement('canvas'); |
| this.filenameCanvasContext_ = this.filenameCanvas_.getContext('2d'); |
| |
| // Set font style of canvas context to same font style with rename field. |
| var filenameEditComputedStyle = window.getComputedStyle(this.filenameEdit_); |
| this.filenameCanvasContext_.font = filenameEditComputedStyle.font; |
| |
| this.filenameEdit_.addEventListener('blur', |
| this.onFilenameEditBlur_.bind(this)); |
| this.filenameEdit_.addEventListener('focus', |
| this.onFilenameFocus_.bind(this)); |
| this.filenameEdit_.addEventListener('input', |
| this.resizeRenameField_.bind(this)); |
| this.filenameEdit_.addEventListener('keydown', |
| this.onFilenameEditKeydown_.bind(this)); |
| |
| var buttonSpacer = queryRequiredElement('.button-spacer', this.topToolbar_); |
| |
| this.prompt_ = new ImageEditor.Prompt(this.container_, strf); |
| |
| this.errorBanner_ = new ErrorBanner(this.container_); |
| |
| /** |
| * @private {!HTMLElement} |
| * @const |
| */ |
| this.modeSwitchButton_ = queryRequiredElement('button.mode', |
| this.topToolbar_); |
| GalleryUtil.decorateMouseFocusHandling(this.modeSwitchButton_); |
| this.modeSwitchButton_.addEventListener('click', |
| this.onModeSwitchButtonClicked_.bind(this)); |
| |
| /** |
| * @private {!DimmableUIController} |
| * @const |
| */ |
| this.dimmableUIController_ = new DimmableUIController(this.container_); |
| |
| this.thumbnailMode_ = new ThumbnailMode( |
| assertInstanceof(document.querySelector('.thumbnail-view'), HTMLElement), |
| this.errorBanner_, |
| this.dataModel_, |
| this.selectionModel_, |
| this.onChangeToSlideMode_.bind(this)); |
| this.thumbnailMode_.hide(); |
| |
| this.slideMode_ = new SlideMode(this.container_, |
| content, |
| this.topToolbar_, |
| this.bottomToolbar_, |
| this.prompt_, |
| this.errorBanner_, |
| this.dataModel_, |
| this.selectionModel_, |
| this.metadataModel_, |
| this.thumbnailModel_, |
| this.context_, |
| this.volumeManager_, |
| this.toggleMode_.bind(this), |
| str, |
| this.dimmableUIController_); |
| |
| /** |
| * @private {!HTMLElement} |
| * @const |
| */ |
| this.deleteButton_ = queryRequiredElement('button.delete', this.topToolbar_); |
| GalleryUtil.decorateMouseFocusHandling(this.deleteButton_); |
| this.deleteButton_.addEventListener('click', this.delete_.bind(this)); |
| |
| /** |
| * @private {!HTMLElement} |
| * @const |
| */ |
| this.slideshowButton_ = queryRequiredElement( |
| 'button.slideshow', this.topToolbar_); |
| GalleryUtil.decorateMouseFocusHandling(this.slideshowButton_); |
| |
| /** |
| * @private {!HTMLElement} |
| * @const |
| */ |
| this.shareButton_ = queryRequiredElement('button.share', this.topToolbar_); |
| GalleryUtil.decorateMouseFocusHandling(this.shareButton_); |
| this.shareButton_.addEventListener( |
| 'click', this.onShareButtonClick_.bind(this)); |
| |
| this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); |
| this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); |
| |
| this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); |
| this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this)); |
| |
| this.shareDialog_ = new ShareDialog(this.container_); |
| |
| // ----------------------------------------------------------------- |
| // Initialize listeners. |
| |
| this.keyDownBound_ = this.onKeyDown_.bind(this); |
| this.document_.body.addEventListener('keydown', this.keyDownBound_); |
| |
| // TODO(hirono): Add observer to handle thumbnail update. |
| this.volumeManager_.addEventListener( |
| 'externally-unmounted', this.onExternallyUnmountedBound_); |
| // The 'pagehide' event is called when the app window is closed. |
| window.addEventListener('pagehide', this.onPageHide_.bind(this)); |
| |
| window.addEventListener('resize', this.resizeRenameField_.bind(this)); |
| |
| assertInstanceof(document.querySelector('files-tooltip'), FilesTooltip) |
| .addTargets(document.querySelectorAll('[has-tooltip]')); |
| |
| // We must call this method after elements of all tools have been attached to |
| // the DOM. |
| this.dimmableUIController_.setTools(document.querySelectorAll('.tool')); |
| |
| /** |
| * @private {function(!Event)} |
| * @const |
| */ |
| this.onSubModeChangedBound_ = this.onSubModeChanged_.bind(this); |
| |
| chrome.accessibilityFeatures.largeCursor.onChange.addListener( |
| this.onGetOrChangedAccessibilityConfiguration_.bind( |
| this, 'large-cursor')); |
| chrome.accessibilityFeatures.largeCursor.get({}, |
| this.onGetOrChangedAccessibilityConfiguration_.bind( |
| this, 'large-cursor')); |
| |
| chrome.accessibilityFeatures.highContrast.onChange.addListener( |
| this.onGetOrChangedAccessibilityConfiguration_.bind( |
| this, 'high-contrast')); |
| chrome.accessibilityFeatures.highContrast.get({}, |
| this.onGetOrChangedAccessibilityConfiguration_.bind( |
| this, 'high-contrast')); |
| } |
| |
| /** |
| * Tools fade-out timeout in milliseconds. |
| * @const |
| * @type {number} |
| */ |
| Gallery.FADE_TIMEOUT = 2000; |
| |
| /** |
| * First time tools fade-out timeout in milliseconds. |
| * @const |
| * @type {number} |
| */ |
| Gallery.FIRST_FADE_TIMEOUT = 1000; |
| |
| /** |
| * Time until mosaic is initialized in the background. Used to make gallery |
| * in the slide mode load faster. In milliseconds. |
| * @const |
| * @type {number} |
| */ |
| Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000; |
| |
| /** |
| * Types of metadata Gallery uses (to query the metadata cache). |
| * @const |
| * @type {!Array<string>} |
| */ |
| Gallery.PREFETCH_PROPERTY_NAMES = |
| ['imageWidth', 'imageHeight', 'imageRotation', 'size', 'present']; |
| |
| /** |
| * Modes in Gallery. |
| * @enum {string} |
| */ |
| Gallery.Mode = { |
| SLIDE: 'slide', |
| THUMBNAIL: 'thumbnail' |
| }; |
| |
| /** |
| * Sub modes in Gallery. |
| * @enum {string} |
| * TODO(yawano): Remove sub modes by extracting them as modes. |
| */ |
| Gallery.SubMode = { |
| BROWSE: 'browse', |
| EDIT: 'edit', |
| SLIDESHOW: 'slideshow' |
| }; |
| |
| /** |
| * Updates attributes of container element when accessibility configuration has |
| * been changed. |
| * @param {string} name |
| * @param {Object} details |
| * @private |
| */ |
| Gallery.prototype.onGetOrChangedAccessibilityConfiguration_ = function( |
| name, details) { |
| if (details.value) { |
| this.container_.setAttribute(name, true); |
| } else { |
| this.container_.removeAttribute(name); |
| } |
| }; |
| |
| /** |
| * Closes gallery when a volume containing the selected item is unmounted. |
| * @param {!Event} event The unmount event. |
| * @private |
| */ |
| Gallery.prototype.onExternallyUnmounted_ = function(event) { |
| if (!this.selectedEntry_) |
| return; |
| |
| if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) === |
| event.volumeInfo) { |
| window.close(); |
| } |
| }; |
| |
| /** |
| * Unloads the Gallery. |
| * @private |
| */ |
| Gallery.prototype.onPageHide_ = function() { |
| this.volumeManager_.removeEventListener( |
| 'externally-unmounted', this.onExternallyUnmountedBound_); |
| this.volumeManager_.dispose(); |
| }; |
| |
| /** |
| * Loads the content. |
| * |
| * @param {!Array<!Entry>} selectedEntries Array of selected entries. |
| */ |
| Gallery.prototype.load = function(selectedEntries) { |
| GalleryUtil.createEntrySet(selectedEntries).then(function(allEntries) { |
| this.loadInternal_(allEntries, selectedEntries); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Loads the content. |
| * |
| * @param {!Array<!FileEntry>} entries Array of entries. |
| * @param {!Array<!FileEntry>} selectedEntries Array of selected entries. |
| * @private |
| */ |
| Gallery.prototype.loadInternal_ = function(entries, selectedEntries) { |
| // Add the entries to data model. |
| var items = []; |
| for (var i = 0; i < entries.length; i++) { |
| var locationInfo = this.volumeManager_.getLocationInfo(entries[i]); |
| if (!locationInfo) // Skip the item, since gone. |
| return; |
| items.push(new GalleryItem( |
| entries[i], |
| locationInfo, |
| null, |
| null, |
| true)); |
| } |
| this.dataModel_.splice(0, this.dataModel_.length); |
| this.updateThumbnails_(); // Remove the caches. |
| |
| GalleryDataModel.prototype.splice.apply( |
| this.dataModel_, [0, 0].concat(items)); |
| |
| // Apply the selection. |
| var selectedSet = {}; |
| for (var i = 0; i < selectedEntries.length; i++) { |
| selectedSet[selectedEntries[i].toURL()] = true; |
| } |
| for (var i = 0; i < items.length; i++) { |
| if (!selectedSet[items[i].getEntry().toURL()]) |
| continue; |
| this.selectionModel_.setIndexSelected(i, true); |
| } |
| this.onSelection_(); |
| |
| // If items are empty, stop initialization. |
| if (items.length === 0) { |
| this.dataModel_.splice(0, this.dataModel_.length); |
| return; |
| } |
| |
| // Sort the selected image first |
| var containsInSelection = function(galleryItem) { |
| return selectedEntries.indexOf(galleryItem.getEntry()) >= 0; |
| }; |
| var notContainsInSelection = function(galleryItem) { |
| return !containsInSelection(galleryItem); |
| }; |
| items = items.filter(containsInSelection) |
| .concat(items.filter(notContainsInSelection)); |
| |
| // Load entries. |
| // Use the self variable capture-by-closure because it is faster than bind. |
| var self = this; |
| var thumbnailModel = new ThumbnailModel(this.metadataModel_); |
| var loadNext = function(index) { |
| // Extract chunk. |
| if (index >= items.length) |
| return; |
| var item = items[index]; |
| var entry = item.getEntry(); |
| var metadataPromise = self.metadataModel_.get([entry], |
| Gallery.PREFETCH_PROPERTY_NAMES); |
| var thumbnailPromise = thumbnailModel.get([entry]); |
| return Promise.all([metadataPromise, thumbnailPromise]).then( |
| function(metadataLists) { |
| // Add items to the model. |
| item.setMetadataItem(metadataLists[0][0]); |
| item.setThumbnailMetadataItem(metadataLists[1][0]); |
| |
| var event = new Event('content'); |
| event.item = item; |
| event.oldEntry = entry; |
| event.thumbnailChanged = true; |
| self.dataModel_.dispatchEvent(event); |
| |
| // Continue to load chunks. |
| return loadNext(/* index */ index + 1); |
| }); |
| }; |
| // init modes before loading images. |
| if (!this.initialized_) { |
| // Determine the initial mode. |
| var shouldShowThumbnail = selectedEntries.length > 1 || |
| (this.context_.pageState && |
| this.context_.pageState.gallery === 'thumbnail'); |
| this.setCurrentMode_( |
| shouldShowThumbnail ? this.thumbnailMode_ : this.slideMode_); |
| |
| // Do the initialization for each mode. |
| if (shouldShowThumbnail) { |
| this.thumbnailMode_.show(); |
| this.thumbnailMode_.focus(); |
| } else { |
| this.slideMode_.enter( |
| null, |
| function() { |
| // Flash the toolbar briefly to show it is there. |
| self.dimmableUIController_.kick(Gallery.FIRST_FADE_TIMEOUT); |
| }, |
| function() {}); |
| } |
| this.initialized_ = true; |
| } |
| loadNext(/* index */ 0).catch(function(error) { |
| console.error(error.stack || error); |
| }); |
| }; |
| |
| /** |
| * @return {boolean} True if some tool is currently active. |
| */ |
| Gallery.prototype.hasActiveTool = function() { |
| return (this.currentMode_ && this.currentMode_.hasActiveTool()) || |
| this.isRenaming_(); |
| }; |
| |
| /** |
| * External user action event handler. |
| * @private |
| */ |
| Gallery.prototype.onUserAction_ = function() { |
| // Show the toolbar and hide it after the default timeout. |
| this.dimmableUIController_.kick(); |
| }; |
| |
| /** |
| * Returns the current mode. |
| * @return {Gallery.Mode} |
| */ |
| Gallery.prototype.getCurrentMode = function() { |
| switch (/** @type {(SlideMode|ThumbnailMode)} */ (this.currentMode_)) { |
| case this.slideMode_: |
| return Gallery.Mode.SLIDE; |
| case this.thumbnailMode_: |
| return Gallery.Mode.THUMBNAIL; |
| default: |
| assertNotReached(); |
| } |
| }; |
| |
| /** |
| * Returns sub mode of current mode. If current mode is not set yet, null is |
| * returned. |
| * @return {Gallery.SubMode} |
| */ |
| Gallery.prototype.getCurrentSubMode = function() { |
| assert(this.currentMode_); |
| return this.currentMode_.getSubMode(); |
| }; |
| |
| /** |
| * Sets the current mode, update the UI. |
| * @param {!(SlideMode|ThumbnailMode)} mode Current mode. |
| * @private |
| */ |
| Gallery.prototype.setCurrentMode_ = function(mode) { |
| if (mode !== this.slideMode_ && mode !== this.thumbnailMode_) |
| console.error('Invalid Gallery mode'); |
| |
| if (this.currentMode_) { |
| this.currentMode_.removeEventListener( |
| 'sub-mode-change', this.onSubModeChangedBound_); |
| } |
| this.currentMode_ = mode; |
| this.currentMode_.addEventListener( |
| 'sub-mode-change', this.onSubModeChangedBound_); |
| |
| this.dimmableUIController_.setCurrentMode( |
| this.getCurrentMode(), this.getCurrentSubMode()); |
| |
| this.container_.setAttribute('mode', this.currentMode_.getName()); |
| this.updateSelectionAndState_(); |
| }; |
| |
| /** |
| * Handles sub-mode-change event. |
| * @private |
| */ |
| Gallery.prototype.onSubModeChanged_ = function() { |
| this.dimmableUIController_.setCurrentMode( |
| this.getCurrentMode(), this.getCurrentSubMode()); |
| }; |
| |
| /** |
| * Handles click event of mode switch button. |
| * @param {!Event} event An event. |
| * @private |
| */ |
| Gallery.prototype.onModeSwitchButtonClicked_ = function(event) { |
| this.toggleMode_(undefined /* callback */, event); |
| }; |
| |
| /** |
| * Change to slide mode. |
| * @private |
| */ |
| Gallery.prototype.onChangeToSlideMode_ = function() { |
| if (this.modeSwitchButton_.disabled) |
| return; |
| |
| this.changeCurrentMode_(this.slideMode_); |
| }; |
| |
| /** |
| * Change current mode. |
| * @param {!(SlideMode|ThumbnailMode)} mode Target mode. |
| * @param {Event=} opt_event Event that caused this call. |
| * @return {!Promise} Resolved when mode has been changed. |
| * @private |
| */ |
| Gallery.prototype.changeCurrentMode_ = function(mode, opt_event) { |
| return new Promise(function(fulfill, reject) { |
| // Do not re-enter while changing the mode. |
| if (this.currentMode_ === mode || this.changingMode_) { |
| fulfill(); |
| return; |
| } |
| |
| if (opt_event) |
| this.onUserAction_(); |
| |
| this.changingMode_ = true; |
| |
| var onModeChanged = function() { |
| this.changingMode_ = false; |
| fulfill(); |
| }.bind(this); |
| |
| var thumbnailIndex = Math.max(0, this.selectionModel_.selectedIndex); |
| var thumbnailRect = ImageRect.createFromBounds( |
| this.thumbnailMode_.getThumbnailRect(thumbnailIndex)); |
| |
| if (mode === this.thumbnailMode_) { |
| this.setCurrentMode_(this.thumbnailMode_); |
| this.slideMode_.leave( |
| thumbnailRect, |
| function() { |
| // Show thumbnail mode and perform animation. |
| this.thumbnailMode_.show(); |
| var fromRect = this.slideMode_.getSelectedImageRect(); |
| if (fromRect) { |
| this.thumbnailMode_.performEnterAnimation( |
| thumbnailIndex, fromRect); |
| } |
| this.thumbnailMode_.focus(); |
| |
| onModeChanged(); |
| }.bind(this)); |
| this.bottomToolbar_.hidden = true; |
| } else { |
| this.setCurrentMode_(this.slideMode_); |
| this.slideMode_.enter( |
| thumbnailRect, |
| function() { |
| // Animate to zoomed position. |
| this.thumbnailMode_.hide(); |
| }.bind(this), |
| onModeChanged); |
| this.bottomToolbar_.hidden = false; |
| } |
| }.bind(this)); |
| }; |
| |
| /** |
| * Mode toggle event handler. |
| * @param {function()=} opt_callback Callback. |
| * @param {Event=} opt_event Event that caused this call. |
| * @private |
| */ |
| Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) { |
| // If it's in editing, leave edit mode. |
| if (this.slideMode_.isEditing()) |
| this.slideMode_.toggleEditor(); |
| |
| var targetMode = this.currentMode_ === this.slideMode_ ? |
| this.thumbnailMode_ : this.slideMode_; |
| |
| this.changeCurrentMode_(targetMode, opt_event).then(function() { |
| if (opt_callback) |
| opt_callback(); |
| }); |
| }; |
| |
| /** |
| * Deletes the selected items. |
| * @private |
| */ |
| Gallery.prototype.delete_ = function() { |
| this.onUserAction_(); |
| |
| // Clone the sorted selected indexes array. |
| var indexesToRemove = this.selectionModel_.selectedIndexes.slice(); |
| if (!indexesToRemove.length) |
| return; |
| |
| /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */ |
| |
| var itemsToRemove = this.getSelectedItems(); |
| var plural = itemsToRemove.length > 1; |
| var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName(); |
| |
| function deleteNext() { |
| if (!itemsToRemove.length) |
| return; // All deleted. |
| |
| var entry = itemsToRemove.pop().getEntry(); |
| entry.remove(deleteNext, function() { |
| console.error('Error deleting: ' + entry.name); |
| deleteNext(); |
| }); |
| } |
| |
| // Prevent the Gallery from handling Esc and Enter. |
| this.document_.body.removeEventListener('keydown', this.keyDownBound_); |
| var restoreListener = function() { |
| this.document_.body.addEventListener('keydown', this.keyDownBound_); |
| }.bind(this); |
| |
| var confirm = new FilesConfirmDialog(this.container_); |
| confirm.setOkLabel(str('DELETE_BUTTON_LABEL')); |
| confirm.show(strf(plural ? |
| 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param), |
| function() { |
| restoreListener(); |
| this.selectionModel_.unselectAll(); |
| this.selectionModel_.leadIndex = -1; |
| // Remove items from the data model, starting from the highest index. |
| while (indexesToRemove.length) |
| this.dataModel_.splice(indexesToRemove.pop(), 1); |
| // Delete actual files. |
| deleteNext(); |
| }.bind(this), |
| function() { |
| // Restore the listener after a timeout so that ESC is processed. |
| setTimeout(restoreListener, 0); |
| }, |
| null); |
| }; |
| |
| /** |
| * @return {!Array<GalleryItem>} Current selection. |
| */ |
| Gallery.prototype.getSelectedItems = function() { |
| return this.selectionModel_.selectedIndexes.map( |
| this.dataModel_.item.bind(this.dataModel_)); |
| }; |
| |
| /** |
| * @return {!Array<Entry>} Array of currently selected entries. |
| */ |
| Gallery.prototype.getSelectedEntries = function() { |
| return this.selectionModel_.selectedIndexes.map(function(index) { |
| return this.dataModel_.item(index).getEntry(); |
| }.bind(this)); |
| }; |
| |
| /** |
| * @return {?GalleryItem} Current single selection. |
| */ |
| Gallery.prototype.getSingleSelectedItem = function() { |
| var items = this.getSelectedItems(); |
| if (items.length > 1) { |
| console.error('Unexpected multiple selection'); |
| return null; |
| } |
| return items[0]; |
| }; |
| |
| /** |
| * Selection change event handler. |
| * @private |
| */ |
| Gallery.prototype.onSelection_ = function() { |
| this.updateSelectionAndState_(); |
| }; |
| |
| /** |
| * Data model splice event handler. |
| * @private |
| */ |
| Gallery.prototype.onSplice_ = function() { |
| this.selectionModel_.adjustLength(this.dataModel_.length); |
| this.selectionModel_.selectedIndexes = |
| this.selectionModel_.selectedIndexes.filter(function(index) { |
| return 0 <= index && index < this.dataModel_.length; |
| }.bind(this)); |
| |
| // Disable mode switch button if there is no image. |
| this.modeSwitchButton_.disabled = this.dataModel_.length === 0; |
| }; |
| |
| /** |
| * Content change event handler. |
| * @param {!Event} event Event. |
| * @private |
| */ |
| Gallery.prototype.onContentChange_ = function(event) { |
| this.updateSelectionAndState_(); |
| }; |
| |
| /** |
| * Keydown handler. |
| * |
| * @param {!Event} event |
| * @private |
| */ |
| Gallery.prototype.onKeyDown_ = function(event) { |
| var keyString = util.getKeyModifiers(event) + event.key; |
| |
| // Handle debug shortcut keys. |
| switch (keyString) { |
| case 'Ctrl-Shift-I': // Ctrl+Shift+I |
| chrome.fileManagerPrivate.openInspector('normal'); |
| break; |
| case 'Ctrl-Shift-J': // Ctrl+Shift+J |
| chrome.fileManagerPrivate.openInspector('console'); |
| break; |
| case 'Ctrl-Shift-C': // Ctrl+Shift+C |
| chrome.fileManagerPrivate.openInspector('element'); |
| break; |
| case 'Ctrl-Shift-B': // Ctrl+Shift+B |
| chrome.fileManagerPrivate.openInspector('background'); |
| break; |
| } |
| |
| // Do not capture keys when share dialog is shown. |
| if (this.shareDialog_.isShowing()) |
| return; |
| |
| // Show UIs when user types any key. |
| this.dimmableUIController_.kick(); |
| |
| // Handle mode specific shortcut keys. |
| if (this.currentMode_.onKeyDown(event)) { |
| event.preventDefault(); |
| return; |
| } |
| |
| // Handle application wide shortcut keys. |
| switch (keyString) { |
| case 'Backspace': |
| // The default handler would call history.back and close the Gallery. |
| // Except while typing into text. |
| if(!event.target.classList.contains('text')) |
| event.preventDefault(); |
| break; |
| |
| case 'm': // 'm' switches between Slide and Mosaic mode. |
| if (!this.modeSwitchButton_.disabled) |
| this.toggleMode_(undefined, event); |
| break; |
| |
| case 'v': |
| case 'MediaPlayPause': |
| if (!this.slideshowButton_.disabled) { |
| this.slideMode_.startSlideshow( |
| SlideMode.SLIDESHOW_INTERVAL_FIRST, event); |
| } |
| break; |
| |
| case 'Delete': |
| case 'Shift-3': // Shift+'3' (Delete key might be missing). |
| case 'd': |
| if (!this.deleteButton_.disabled) |
| this.delete_(); |
| break; |
| |
| case 'Escape': |
| window.close(); |
| break; |
| } |
| }; |
| |
| // Name box and rename support. |
| |
| /** |
| * Updates the UI related to the selected item and the persistent state. |
| * |
| * @private |
| */ |
| Gallery.prototype.updateSelectionAndState_ = function() { |
| var numSelectedItems = this.selectionModel_.selectedIndexes.length; |
| var selectedEntryURL = null; |
| |
| // If it's selecting something, update the variable values. |
| if (numSelectedItems) { |
| // Enable slideshow button. |
| this.slideshowButton_.disabled = false; |
| |
| // Delete button is available when all images are NOT readOnly. |
| this.deleteButton_.disabled = !this.selectionModel_.selectedIndexes |
| .every(function(i) { |
| return !this.dataModel_.item(i).getLocationInfo().isReadOnly; |
| }, this); |
| |
| // Obtains selected item. |
| var selectedItem = |
| this.dataModel_.item(this.selectionModel_.selectedIndex); |
| this.selectedEntry_ = selectedItem.getEntry(); |
| selectedEntryURL = this.selectedEntry_.toURL(); |
| |
| // Update cache. |
| selectedItem.touch(); |
| this.dataModel_.evictCache(); |
| |
| // Update the title and the display name. |
| if (numSelectedItems === 1) { |
| document.title = this.selectedEntry_.name; |
| this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly; |
| this.filenameEdit_.value = |
| ImageUtil.getDisplayNameFromName(this.selectedEntry_.name); |
| this.resizeRenameField_(); |
| |
| this.shareButton_.disabled = !selectedItem.getLocationInfo().isDriveBased; |
| } else { |
| if (this.context_.curDirEntry) { |
| // If the Gallery was opened on search results the search query will not |
| // be recorded in the app state and the relaunch will just open the |
| // gallery in the curDirEntry directory. |
| document.title = this.context_.curDirEntry.name; |
| } else { |
| document.title = ''; |
| } |
| this.filenameEdit_.disabled = true; |
| this.filenameEdit_.value = |
| strf('GALLERY_ITEMS_SELECTED', numSelectedItems); |
| this.resizeRenameField_(); |
| |
| this.shareButton_.disabled = true; |
| } |
| } else { |
| document.title = ''; |
| this.filenameEdit_.disabled = true; |
| this.filenameEdit_.value = ''; |
| this.resizeRenameField_(); |
| |
| this.deleteButton_.disabled = true; |
| this.slideshowButton_.disabled = true; |
| this.shareButton_.disabled = true; |
| } |
| |
| util.updateAppState( |
| null, // Keep the current directory. |
| selectedEntryURL, // Update the selection. |
| { |
| gallery: (this.currentMode_ === this.thumbnailMode_ ? |
| 'thumbnail' : 'slide') |
| }); |
| }; |
| |
| /** |
| * Click event handler on filename edit box |
| * @private |
| */ |
| Gallery.prototype.onFilenameFocus_ = function() { |
| ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); |
| this.dimmableUIController_.setRenaming(true); |
| |
| this.filenameEdit_.originalValue = this.filenameEdit_.value; |
| setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); |
| this.onUserAction_(); |
| }; |
| |
| /** |
| * Blur event handler on filename edit box. |
| * |
| * @param {!Event} event Blur event. |
| * @private |
| */ |
| Gallery.prototype.onFilenameEditBlur_ = function(event) { |
| var item = this.getSingleSelectedItem(); |
| if (item) { |
| var oldEntry = item.getEntry(); |
| |
| item.rename(this.filenameEdit_.value).then(function() { |
| var event = new Event('content'); |
| event.item = item; |
| event.oldEntry = oldEntry; |
| event.thumbnailChanged = false; |
| this.dataModel_.dispatchEvent(event); |
| }.bind(this), function(error) { |
| if (error === 'NOT_CHANGED') |
| return Promise.resolve(); |
| this.filenameEdit_.value = |
| ImageUtil.getDisplayNameFromName(item.getEntry().name); |
| this.resizeRenameField_(); |
| this.filenameEdit_.focus(); |
| if (typeof error === 'string') |
| this.prompt_.showStringAt('center', error, 5000); |
| else |
| return Promise.reject(error); |
| }.bind(this)).catch(function(error) { |
| console.error(error.stack || error); |
| }); |
| } |
| |
| ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); |
| this.dimmableUIController_.setRenaming(false); |
| this.onUserAction_(); |
| }; |
| |
| /** |
| * Minimum width of rename field. |
| * @const {number} |
| */ |
| Gallery.MIN_WIDTH_RENAME_FIELD = 160; // px |
| |
| /** |
| * End padding for rename field. |
| * @const {number} |
| */ |
| Gallery.END_PADDING_RENAME_FIELD = 20; // px |
| |
| /** |
| * Resize rename field depending on its content. |
| * @private |
| */ |
| Gallery.prototype.resizeRenameField_ = function() { |
| var size = this.filenameCanvasContext_.measureText(this.filenameEdit_.value); |
| |
| var width = Math.min(Math.max( |
| size.width + Gallery.END_PADDING_RENAME_FIELD, |
| Gallery.MIN_WIDTH_RENAME_FIELD), window.innerWidth / 2); |
| |
| this.filenameEdit_.style.width = width + 'px'; |
| }; |
| |
| /** |
| * Keydown event handler on filename edit box |
| * @param {!Event} event A keyboard event. |
| * @private |
| */ |
| Gallery.prototype.onFilenameEditKeydown_ = function(event) { |
| event = assertInstanceof(event, KeyboardEvent); |
| switch (event.keyCode) { |
| case 27: // Escape |
| this.filenameEdit_.value = this.filenameEdit_.originalValue; |
| this.resizeRenameField_(); |
| this.filenameEdit_.blur(); |
| break; |
| |
| case 13: // Enter |
| this.filenameEdit_.blur(); |
| break; |
| } |
| event.stopPropagation(); |
| }; |
| |
| /** |
| * @return {boolean} True if file renaming is currently in progress. |
| * @private |
| */ |
| Gallery.prototype.isRenaming_ = function() { |
| return this.filenameSpacer_.hasAttribute('renaming'); |
| }; |
| |
| /** |
| * Content area click handler. |
| * @private |
| */ |
| Gallery.prototype.onContentClick_ = function() { |
| this.filenameEdit_.blur(); |
| }; |
| |
| /** |
| * Share button handler. |
| * @private |
| */ |
| Gallery.prototype.onShareButtonClick_ = function() { |
| var item = this.getSingleSelectedItem(); |
| if (!item) |
| return; |
| this.shareDialog_.showEntry(item.getEntry(), function() {}); |
| }; |
| |
| /** |
| * Updates thumbnails. |
| * @private |
| */ |
| Gallery.prototype.updateThumbnails_ = function() { |
| if (this.currentMode_ === this.slideMode_) |
| this.slideMode_.updateThumbnails(); |
| }; |
| |
| /** |
| * Singleton gallery. |
| * @type {Gallery} |
| */ |
| var gallery = null; |
| |
| /** |
| * (Re-)loads entries. |
| */ |
| function reload() { |
| initializePromise.then(function() { |
| util.URLsToEntries(window.appState.urls, function(entries) { |
| gallery.load(entries); |
| }); |
| }); |
| } |
| |
| /** |
| * Promise to initialize the load time data. |
| * @type {!Promise} |
| */ |
| var loadTimeDataPromise = new Promise(function(fulfill, reject) { |
| chrome.fileManagerPrivate.getStrings(function(strings) { |
| window.loadTimeData.data = strings; |
| i18nTemplate.process(document, loadTimeData); |
| fulfill(true); |
| }); |
| }); |
| |
| /** |
| * Promise to initialize volume manager. |
| * @type {!Promise} |
| */ |
| var volumeManagerPromise = new Promise(function(fulfill, reject) { |
| var volumeManager = new VolumeManagerWrapper(AllowedPaths.ANY_PATH); |
| volumeManager.ensureInitialized(fulfill.bind(null, volumeManager)); |
| }); |
| |
| /** |
| * Promise to initialize both the volume manager and the load time data. |
| * @type {!Promise} |
| */ |
| var initializePromise = |
| Promise.all([loadTimeDataPromise, volumeManagerPromise]). |
| then(function(args) { |
| var volumeManager = args[1]; |
| gallery = new Gallery(volumeManager); |
| }); |
| |
| // Loads entries. |
| initializePromise.then(reload); |