| // Copyright (c) 2012 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. |
| |
| 'use strict'; |
| |
| /** |
| * Slide mode displays a single image and has a set of controls to navigate |
| * between the images and to edit an image. |
| * |
| * TODO(kaznacheev): Introduce a parameter object. |
| * |
| * @param {Element} container Main container element. |
| * @param {Element} content Content container element. |
| * @param {Element} toolbar Toolbar element. |
| * @param {ImageEditor.Prompt} prompt Prompt. |
| * @param {cr.ui.ArrayDataModel} dataModel Data model. |
| * @param {cr.ui.ListSelectionModel} selectionModel Selection model. |
| * @param {Object} context Context. |
| * @param {function(function())} toggleMode Function to toggle the Gallery mode. |
| * @param {function(string):string} displayStringFunction String formatting |
| * function. |
| * @constructor |
| */ |
| function SlideMode(container, content, toolbar, prompt, |
| dataModel, selectionModel, context, |
| toggleMode, displayStringFunction) { |
| this.container_ = container; |
| this.document_ = container.ownerDocument; |
| this.content = content; |
| this.toolbar_ = toolbar; |
| this.prompt_ = prompt; |
| this.dataModel_ = dataModel; |
| this.selectionModel_ = selectionModel; |
| this.context_ = context; |
| this.metadataCache_ = context.metadataCache; |
| this.toggleMode_ = toggleMode; |
| this.displayStringFunction_ = displayStringFunction; |
| |
| this.onSelectionBound_ = this.onSelection_.bind(this); |
| this.onSpliceBound_ = this.onSplice_.bind(this); |
| this.onContentBound_ = this.onContentChange_.bind(this); |
| |
| // Unique numeric key, incremented per each load attempt used to discard |
| // old attempts. This can happen especially when changing selection fast or |
| // Internet connection is slow. |
| this.currentUniqueKey_ = 0; |
| |
| this.initListeners_(); |
| this.initDom_(); |
| } |
| |
| /** |
| * SlideMode extends cr.EventTarget. |
| */ |
| SlideMode.prototype.__proto__ = cr.EventTarget.prototype; |
| |
| /** |
| * List of available editor modes. |
| * @type {Array.<ImageEditor.Mode>} |
| */ |
| SlideMode.editorModes = [ |
| new ImageEditor.Mode.InstantAutofix(), |
| new ImageEditor.Mode.Crop(), |
| new ImageEditor.Mode.Exposure(), |
| new ImageEditor.Mode.OneClick( |
| 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)), |
| new ImageEditor.Mode.OneClick( |
| 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1)) |
| ]; |
| |
| /** |
| * @return {string} Mode name. |
| */ |
| SlideMode.prototype.getName = function() { return 'slide' }; |
| |
| /** |
| * @return {string} Mode title. |
| */ |
| SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' }; |
| |
| /** |
| * Initialize the listeners. |
| * @private |
| */ |
| SlideMode.prototype.initListeners_ = function() { |
| window.addEventListener('resize', this.onResize_.bind(this), false); |
| }; |
| |
| /** |
| * Initialize the UI. |
| * @private |
| */ |
| SlideMode.prototype.initDom_ = function() { |
| // Container for displayed image or video. |
| this.imageContainer_ = util.createChild( |
| this.document_.querySelector('.content'), 'image-container'); |
| this.imageContainer_.addEventListener('click', this.onClick_.bind(this)); |
| |
| this.document_.addEventListener('click', this.onDocumentClick_.bind(this)); |
| |
| // Overwrite options and info bubble. |
| this.options_ = util.createChild( |
| this.toolbar_.querySelector('.filename-spacer'), 'options'); |
| |
| this.savedLabel_ = util.createChild(this.options_, 'saved'); |
| this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED'); |
| |
| var overwriteOriginalBox = |
| util.createChild(this.options_, 'overwrite-original'); |
| |
| this.overwriteOriginal_ = util.createChild( |
| overwriteOriginalBox, 'common white', 'input'); |
| this.overwriteOriginal_.type = 'checkbox'; |
| this.overwriteOriginal_.id = 'overwrite-checkbox'; |
| util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) { |
| // Out-of-the box default is 'true' |
| this.overwriteOriginal_.checked = |
| (typeof value !== 'string' || value === 'true'); |
| }.bind(this)); |
| this.overwriteOriginal_.addEventListener('click', |
| this.onOverwriteOriginalClick_.bind(this)); |
| |
| var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label'); |
| overwriteLabel.textContent = |
| this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL'); |
| overwriteLabel.setAttribute('for', 'overwrite-checkbox'); |
| |
| this.bubble_ = util.createChild(this.toolbar_, 'bubble'); |
| this.bubble_.hidden = true; |
| |
| var bubbleContent = util.createChild(this.bubble_); |
| bubbleContent.innerHTML = this.displayStringFunction_( |
| 'GALLERY_OVERWRITE_BUBBLE'); |
| |
| util.createChild(this.bubble_, 'pointer bottom', 'span'); |
| |
| var bubbleClose = util.createChild(this.bubble_, 'close-x'); |
| bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this)); |
| |
| // Video player controls. |
| this.mediaSpacer_ = |
| util.createChild(this.container_, 'video-controls-spacer'); |
| this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool'); |
| this.mediaControls_ = new VideoControls( |
| this.mediaToolbar_, |
| this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'), |
| this.displayStringFunction_.bind(this), |
| this.toggleFullScreen_.bind(this), |
| this.container_); |
| |
| // Ribbon and related controls. |
| this.arrowBox_ = util.createChild(this.container_, 'arrow-box'); |
| |
| this.arrowLeft_ = |
| util.createChild(this.arrowBox_, 'arrow left tool dimmable'); |
| this.arrowLeft_.addEventListener('click', |
| this.advanceManually.bind(this, -1)); |
| util.createChild(this.arrowLeft_); |
| |
| util.createChild(this.arrowBox_, 'arrow-spacer'); |
| |
| this.arrowRight_ = |
| util.createChild(this.arrowBox_, 'arrow right tool dimmable'); |
| this.arrowRight_.addEventListener('click', |
| this.advanceManually.bind(this, 1)); |
| util.createChild(this.arrowRight_); |
| |
| this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer'); |
| this.ribbon_ = new Ribbon(this.document_, |
| this.metadataCache_, this.dataModel_, this.selectionModel_); |
| this.ribbonSpacer_.appendChild(this.ribbon_); |
| |
| // Error indicator. |
| var errorWrapper = util.createChild(this.container_, 'prompt-wrapper'); |
| errorWrapper.setAttribute('pos', 'center'); |
| |
| this.errorBanner_ = util.createChild(errorWrapper, 'error-banner'); |
| |
| util.createChild(this.container_, 'spinner'); |
| |
| var slideShowButton = util.createChild(this.toolbar_, |
| 'button slideshow', 'button'); |
| slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW'); |
| slideShowButton.addEventListener('click', |
| this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST)); |
| |
| var slideShowToolbar = |
| util.createChild(this.container_, 'tool slideshow-toolbar'); |
| util.createChild(slideShowToolbar, 'slideshow-play'). |
| addEventListener('click', this.toggleSlideshowPause_.bind(this)); |
| util.createChild(slideShowToolbar, 'slideshow-end'). |
| addEventListener('click', this.stopSlideshow_.bind(this)); |
| |
| // Editor. |
| |
| this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button'); |
| this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT'); |
| this.editButton_.setAttribute('disabled', ''); // Disabled by default. |
| this.editButton_.addEventListener('click', this.toggleEditor.bind(this)); |
| |
| this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button'); |
| this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT'); |
| this.printButton_.setAttribute('disabled', ''); // Disabled by default. |
| this.printButton_.addEventListener('click', this.print_.bind(this)); |
| |
| this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer'); |
| this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main'); |
| |
| this.editBarMode_ = util.createChild(this.container_, 'edit-modal'); |
| this.editBarModeWrapper_ = util.createChild( |
| this.editBarMode_, 'edit-modal-wrapper'); |
| this.editBarModeWrapper_.hidden = true; |
| |
| // Objects supporting image display and editing. |
| this.viewport_ = new Viewport(); |
| |
| this.imageView_ = new ImageView( |
| this.imageContainer_, |
| this.viewport_, |
| this.metadataCache_); |
| |
| this.editor_ = new ImageEditor( |
| this.viewport_, |
| this.imageView_, |
| this.prompt_, |
| { |
| root: this.container_, |
| image: this.imageContainer_, |
| toolbar: this.editBarMain_, |
| mode: this.editBarModeWrapper_ |
| }, |
| SlideMode.editorModes, |
| this.displayStringFunction_, |
| this.onToolsVisibilityChanged_.bind(this)); |
| |
| this.editor_.getBuffer().addOverlay( |
| new SwipeOverlay(this.advanceManually.bind(this))); |
| }; |
| |
| /** |
| * Load items, display the selected item. |
| * @param {Rect} zoomFromRect Rectangle for zoom effect. |
| * @param {function} displayCallback Called when the image is displayed. |
| * @param {function} loadCallback Called when the image is displayed. |
| */ |
| SlideMode.prototype.enter = function( |
| zoomFromRect, displayCallback, loadCallback) { |
| this.sequenceDirection_ = 0; |
| this.sequenceLength_ = 0; |
| |
| var loadDone = function(loadType, delay) { |
| this.active_ = true; |
| |
| this.selectionModel_.addEventListener('change', this.onSelectionBound_); |
| this.dataModel_.addEventListener('splice', this.onSpliceBound_); |
| this.dataModel_.addEventListener('content', this.onContentBound_); |
| |
| ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); |
| this.ribbon_.enable(); |
| |
| // Wait 1000ms after the animation is done, then prefetch the next image. |
| this.requestPrefetch(1, delay + 1000); |
| |
| if (loadCallback) loadCallback(); |
| }.bind(this); |
| |
| // The latest |leave| call might have left the image animating. Remove it. |
| this.unloadImage_(); |
| |
| if (this.getItemCount_() === 0) { |
| this.displayedIndex_ = -1; |
| //TODO(kaznacheev) Show this message in the grid mode too. |
| this.showErrorBanner_('GALLERY_NO_IMAGES'); |
| loadDone(); |
| } else { |
| // Remember the selection if it is empty or multiple. It will be restored |
| // in |leave| if the user did not changing the selection manually. |
| var currentSelection = this.selectionModel_.selectedIndexes; |
| if (currentSelection.length === 1) |
| this.savedSelection_ = null; |
| else |
| this.savedSelection_ = currentSelection; |
| |
| // Ensure valid single selection. |
| // Note that the SlideMode object is not listening to selection change yet. |
| this.select(Math.max(0, this.getSelectedIndex())); |
| this.displayedIndex_ = this.getSelectedIndex(); |
| |
| var selectedItem = this.getSelectedItem(); |
| // Show the selected item ASAP, then complete the initialization |
| // (loading the ribbon thumbnails can take some time). |
| this.metadataCache_.get(selectedItem.getEntry(), Gallery.METADATA_TYPE, |
| function(metadata) { |
| this.loadItem_(selectedItem.getEntry(), metadata, |
| zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect), |
| displayCallback, loadDone); |
| }.bind(this)); |
| |
| } |
| }; |
| |
| /** |
| * Leave the mode. |
| * @param {Rect} zoomToRect Rectangle for zoom effect. |
| * @param {function} callback Called when the image is committed and |
| * the zoom-out animation has started. |
| */ |
| SlideMode.prototype.leave = function(zoomToRect, callback) { |
| var commitDone = function() { |
| this.stopEditing_(); |
| this.stopSlideshow_(); |
| ImageUtil.setAttribute(this.arrowBox_, 'active', false); |
| this.selectionModel_.removeEventListener( |
| 'change', this.onSelectionBound_); |
| this.dataModel_.removeEventListener('splice', this.onSpliceBound_); |
| this.dataModel_.removeEventListener('content', this.onContentBound_); |
| this.ribbon_.disable(); |
| this.active_ = false; |
| if (this.savedSelection_) |
| this.selectionModel_.selectedIndexes = this.savedSelection_; |
| this.unloadImage_(zoomToRect); |
| callback(); |
| }.bind(this); |
| |
| if (this.getItemCount_() === 0) { |
| this.showErrorBanner_(false); |
| commitDone(); |
| } else { |
| this.commitItem_(commitDone); |
| } |
| |
| // Disable the slide-mode only buttons when leaving. |
| this.editButton_.setAttribute('disabled', ''); |
| this.printButton_.setAttribute('disabled', ''); |
| }; |
| |
| |
| /** |
| * Execute an action when the editor is not busy. |
| * |
| * @param {function} action Function to execute. |
| */ |
| SlideMode.prototype.executeWhenReady = function(action) { |
| this.editor_.executeWhenReady(action); |
| }; |
| |
| /** |
| * @return {boolean} True if the mode has active tools (that should not fade). |
| */ |
| SlideMode.prototype.hasActiveTool = function() { |
| return this.isEditing(); |
| }; |
| |
| /** |
| * @return {number} Item count. |
| * @private |
| */ |
| SlideMode.prototype.getItemCount_ = function() { |
| return this.dataModel_.length; |
| }; |
| |
| /** |
| * @param {number} index Index. |
| * @return {Gallery.Item} Item. |
| */ |
| SlideMode.prototype.getItem = function(index) { |
| return this.dataModel_.item(index); |
| }; |
| |
| /** |
| * @return {Gallery.Item} Selected index. |
| */ |
| SlideMode.prototype.getSelectedIndex = function() { |
| return this.selectionModel_.selectedIndex; |
| }; |
| |
| /** |
| * @return {Rect} Screen rectangle of the selected image. |
| */ |
| SlideMode.prototype.getSelectedImageRect = function() { |
| if (this.getSelectedIndex() < 0) |
| return null; |
| else |
| return this.viewport_.getScreenClipped(); |
| }; |
| |
| /** |
| * @return {Gallery.Item} Selected item. |
| */ |
| SlideMode.prototype.getSelectedItem = function() { |
| return this.getItem(this.getSelectedIndex()); |
| }; |
| |
| /** |
| * Toggles the full screen mode. |
| * @private |
| */ |
| SlideMode.prototype.toggleFullScreen_ = function() { |
| util.toggleFullScreen(this.context_.appWindow, |
| !util.isFullScreen(this.context_.appWindow)); |
| }; |
| |
| /** |
| * Selection change handler. |
| * |
| * Commits the current image and displays the newly selected image. |
| * @private |
| */ |
| SlideMode.prototype.onSelection_ = function() { |
| if (this.selectionModel_.selectedIndexes.length === 0) |
| return; // Temporary empty selection. |
| |
| // Forget the saved selection if the user changed the selection manually. |
| if (!this.isSlideshowOn_()) |
| this.savedSelection_ = null; |
| |
| if (this.getSelectedIndex() === this.displayedIndex_) |
| return; // Do not reselect. |
| |
| this.commitItem_(this.loadSelectedItem_.bind(this)); |
| }; |
| |
| /** |
| * Handles changes in tools visibility, and if the header is dimmed, then |
| * requests disabling the draggable app region. |
| * |
| * @private |
| */ |
| SlideMode.prototype.onToolsVisibilityChanged_ = function() { |
| var headerDimmed = |
| this.document_.querySelector('.header').hasAttribute('dimmed'); |
| this.context_.onAppRegionChanged(!headerDimmed); |
| }; |
| |
| /** |
| * Change the selection. |
| * |
| * @param {number} index New selected index. |
| * @param {number=} opt_slideHint Slide animation direction (-1|1). |
| */ |
| SlideMode.prototype.select = function(index, opt_slideHint) { |
| this.slideHint_ = opt_slideHint; |
| this.selectionModel_.selectedIndex = index; |
| this.selectionModel_.leadIndex = index; |
| }; |
| |
| /** |
| * Load the selected item. |
| * |
| * @private |
| */ |
| SlideMode.prototype.loadSelectedItem_ = function() { |
| var slideHint = this.slideHint_; |
| this.slideHint_ = undefined; |
| |
| var index = this.getSelectedIndex(); |
| if (index === this.displayedIndex_) |
| return; // Do not reselect. |
| |
| var step = slideHint || (index - this.displayedIndex_); |
| |
| if (Math.abs(step) != 1) { |
| // Long leap, the sequence is broken, we have no good prefetch candidate. |
| this.sequenceDirection_ = 0; |
| this.sequenceLength_ = 0; |
| } else if (this.sequenceDirection_ === step) { |
| // Keeping going in sequence. |
| this.sequenceLength_++; |
| } else { |
| // Reversed the direction. Reset the counter. |
| this.sequenceDirection_ = step; |
| this.sequenceLength_ = 1; |
| } |
| |
| if (this.sequenceLength_ <= 1) { |
| // We have just broke the sequence. Touch the current image so that it stays |
| // in the cache longer. |
| this.imageView_.prefetch(this.imageView_.contentEntry_); |
| } |
| |
| this.displayedIndex_ = index; |
| |
| function shouldPrefetch(loadType, step, sequenceLength) { |
| // Never prefetch when selecting out of sequence. |
| if (Math.abs(step) != 1) |
| return false; |
| |
| // Never prefetch after a video load (decoding the next image can freeze |
| // the UI for a second or two). |
| if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE) |
| return false; |
| |
| // Always prefetch if the previous load was from cache. |
| if (loadType === ImageView.LOAD_TYPE_CACHED_FULL) |
| return true; |
| |
| // Prefetch if we have been going in the same direction for long enough. |
| return sequenceLength >= 3; |
| } |
| |
| var selectedItem = this.getSelectedItem(); |
| this.currentUniqueKey_++; |
| var selectedUniqueKey = this.currentUniqueKey_; |
| var onMetadata = function(metadata) { |
| // Discard, since another load has been invoked after this one. |
| if (selectedUniqueKey != this.currentUniqueKey_) return; |
| this.loadItem_(selectedItem.getEntry(), metadata, |
| new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()), |
| function() {} /* no displayCallback */, |
| function(loadType, delay) { |
| // Discard, since another load has been invoked after this one. |
| if (selectedUniqueKey != this.currentUniqueKey_) return; |
| if (shouldPrefetch(loadType, step, this.sequenceLength_)) { |
| this.requestPrefetch(step, delay); |
| } |
| if (this.isSlideshowPlaying_()) |
| this.scheduleNextSlide_(); |
| }.bind(this)); |
| }.bind(this); |
| this.metadataCache_.get( |
| selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata); |
| }; |
| |
| /** |
| * Unload the current image. |
| * |
| * @param {Rect} zoomToRect Rectangle for zoom effect. |
| * @private |
| */ |
| SlideMode.prototype.unloadImage_ = function(zoomToRect) { |
| this.imageView_.unload(zoomToRect); |
| this.container_.removeAttribute('video'); |
| }; |
| |
| /** |
| * Data model 'splice' event handler. |
| * @param {Event} event Event. |
| * @private |
| */ |
| SlideMode.prototype.onSplice_ = function(event) { |
| ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); |
| |
| // Splice invalidates saved indices, drop the saved selection. |
| this.savedSelection_ = null; |
| |
| if (event.removed.length != 1) |
| return; |
| |
| // Delay the selection to let the ribbon splice handler work first. |
| setTimeout(function() { |
| if (event.index < this.dataModel_.length) { |
| // There is the next item, select it. |
| // The next item is now at the same index as the removed one, so we need |
| // to correct displayIndex_ so that loadSelectedItem_ does not think |
| // we are re-selecting the same item (and does right-to-left slide-in |
| // animation). |
| this.displayedIndex_ = event.index - 1; |
| this.select(event.index); |
| } else if (this.dataModel_.length) { |
| // Removed item is the rightmost, but there are more items. |
| this.select(event.index - 1); // Select the new last index. |
| } else { |
| // No items left. Unload the image and show the banner. |
| this.commitItem_(function() { |
| this.unloadImage_(); |
| this.showErrorBanner_('GALLERY_NO_IMAGES'); |
| }.bind(this)); |
| } |
| }.bind(this), 0); |
| }; |
| |
| /** |
| * @param {number} direction -1 for left, 1 for right. |
| * @return {number} Next index in the given direction, with wrapping. |
| * @private |
| */ |
| SlideMode.prototype.getNextSelectedIndex_ = function(direction) { |
| function advance(index, limit) { |
| index += (direction > 0 ? 1 : -1); |
| if (index < 0) |
| return limit - 1; |
| if (index === limit) |
| return 0; |
| return index; |
| } |
| |
| // If the saved selection is multiple the Slideshow should cycle through |
| // the saved selection. |
| if (this.isSlideshowOn_() && |
| this.savedSelection_ && this.savedSelection_.length > 1) { |
| var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()), |
| this.savedSelection_.length); |
| return this.savedSelection_[pos]; |
| } else { |
| return advance(this.getSelectedIndex(), this.getItemCount_()); |
| } |
| }; |
| |
| /** |
| * Advance the selection based on the pressed key ID. |
| * @param {string} keyID Key identifier. |
| */ |
| SlideMode.prototype.advanceWithKeyboard = function(keyID) { |
| this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1); |
| }; |
| |
| /** |
| * Advance the selection as a result of a user action (as opposed to an |
| * automatic change in the slideshow mode). |
| * @param {number} direction -1 for left, 1 for right. |
| */ |
| SlideMode.prototype.advanceManually = function(direction) { |
| if (this.isSlideshowPlaying_()) { |
| this.pauseSlideshow_(); |
| cr.dispatchSimpleEvent(this, 'useraction'); |
| } |
| this.selectNext(direction); |
| }; |
| |
| /** |
| * Select the next item. |
| * @param {number} direction -1 for left, 1 for right. |
| */ |
| SlideMode.prototype.selectNext = function(direction) { |
| this.select(this.getNextSelectedIndex_(direction), direction); |
| }; |
| |
| /** |
| * Select the first item. |
| */ |
| SlideMode.prototype.selectFirst = function() { |
| this.select(0); |
| }; |
| |
| /** |
| * Select the last item. |
| */ |
| SlideMode.prototype.selectLast = function() { |
| this.select(this.getItemCount_() - 1); |
| }; |
| |
| // Loading/unloading |
| |
| /** |
| * Load and display an item. |
| * |
| * @param {FileEntry} entry Item entry to be loaded. |
| * @param {Object} metadata Item metadata. |
| * @param {Object} effect Transition effect object. |
| * @param {function} displayCallback Called when the image is displayed |
| * (which can happen before the image load due to caching). |
| * @param {function} loadCallback Called when the image is fully loaded. |
| * @private |
| */ |
| SlideMode.prototype.loadItem_ = function( |
| entry, metadata, effect, displayCallback, loadCallback) { |
| this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata); |
| |
| this.showSpinner_(true); |
| |
| var loadDone = function(loadType, delay, error) { |
| var video = this.isShowingVideo_(); |
| ImageUtil.setAttribute(this.container_, 'video', video); |
| |
| this.showSpinner_(false); |
| if (loadType === ImageView.LOAD_TYPE_ERROR) { |
| // if we have a specific error, then display it |
| if (error) { |
| this.showErrorBanner_(error); |
| } else { |
| // otherwise try to infer general error |
| this.showErrorBanner_( |
| video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR'); |
| } |
| } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) { |
| this.showErrorBanner_( |
| video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE'); |
| } |
| |
| if (video) { |
| // The editor toolbar does not make sense for video, hide it. |
| this.stopEditing_(); |
| this.mediaControls_.attachMedia(this.imageView_.getVideo()); |
| |
| // TODO(kaznacheev): Add metrics for video playback. |
| } else { |
| ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); |
| |
| var toMillions = function(number) { |
| return Math.round(number / (1000 * 1000)); |
| }; |
| |
| ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), |
| toMillions(metadata.filesystem.size)); |
| |
| var canvas = this.imageView_.getCanvas(); |
| ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), |
| toMillions(canvas.width * canvas.height)); |
| |
| var extIndex = entry.name.lastIndexOf('.'); |
| var ext = extIndex < 0 ? '' : |
| entry.name.substr(extIndex + 1).toLowerCase(); |
| if (ext === 'jpeg') ext = 'jpg'; |
| ImageUtil.metrics.recordEnum( |
| ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); |
| } |
| |
| // Enable or disable buttons for editing and printing. |
| if (video || error) { |
| this.editButton_.setAttribute('disabled', ''); |
| this.printButton_.setAttribute('disabled', ''); |
| } else { |
| this.editButton_.removeAttribute('disabled'); |
| this.printButton_.removeAttribute('disabled'); |
| } |
| |
| // For once edited image, disallow the 'overwrite' setting change. |
| ImageUtil.setAttribute(this.options_, 'saved', |
| !this.getSelectedItem().isOriginal()); |
| |
| util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY, |
| function(value) { |
| var times = typeof value === 'string' ? parseInt(value, 10) : 0; |
| if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) { |
| this.bubble_.hidden = false; |
| if (this.isEditing()) { |
| util.platform.setPreference( |
| SlideMode.OVERWRITE_BUBBLE_KEY, times + 1); |
| } |
| } |
| }.bind(this)); |
| |
| loadCallback(loadType, delay); |
| }.bind(this); |
| |
| var displayDone = function() { |
| cr.dispatchSimpleEvent(this, 'image-displayed'); |
| displayCallback(); |
| }.bind(this); |
| |
| this.editor_.openSession(entry, metadata, effect, |
| this.saveCurrentImage_.bind(this), displayDone, loadDone); |
| }; |
| |
| /** |
| * Commit changes to the current item and reset all messages/indicators. |
| * |
| * @param {function} callback Callback. |
| * @private |
| */ |
| SlideMode.prototype.commitItem_ = function(callback) { |
| this.showSpinner_(false); |
| this.showErrorBanner_(false); |
| this.editor_.getPrompt().hide(); |
| |
| // Detach any media attached to the controls. |
| if (this.mediaControls_.getMedia()) |
| this.mediaControls_.detachMedia(); |
| |
| // If showing the video, then pause it. Note, that it may not be attached |
| // to the media controls yet. |
| if (this.isShowingVideo_()) { |
| this.imageView_.getVideo().pause(); |
| // Force stop downloading, if uncached on Drive. |
| this.imageView_.getVideo().src = ''; |
| this.imageView_.getVideo().load(); |
| } |
| |
| this.editor_.closeSession(callback); |
| }; |
| |
| /** |
| * Request a prefetch for the next image. |
| * |
| * @param {number} direction -1 or 1. |
| * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image |
| * loading from disrupting the animation that might be still in progress. |
| */ |
| SlideMode.prototype.requestPrefetch = function(direction, delay) { |
| if (this.getItemCount_() <= 1) return; |
| |
| var index = this.getNextSelectedIndex_(direction); |
| var nextItemEntry = this.getItem(index).getEntry(); |
| this.imageView_.prefetch(nextItemEntry, delay); |
| }; |
| |
| // Event handlers. |
| |
| /** |
| * Unload handler, to be called from the top frame. |
| * @param {boolean} exiting True if the app is exiting. |
| */ |
| SlideMode.prototype.onUnload = function(exiting) { |
| if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { |
| this.mediaControls_.savePosition(exiting); |
| } |
| }; |
| |
| /** |
| * Click handler for the image container. |
| * |
| * @param {Event} event Mouse click event. |
| * @private |
| */ |
| SlideMode.prototype.onClick_ = function(event) { |
| if (!this.isShowingVideo_() || !this.mediaControls_.getMedia()) |
| return; |
| if (event.ctrlKey) { |
| this.mediaControls_.toggleLoopedModeWithFeedback(true); |
| if (!this.mediaControls_.isPlaying()) |
| this.mediaControls_.togglePlayStateWithFeedback(); |
| } else { |
| this.mediaControls_.togglePlayStateWithFeedback(); |
| } |
| }; |
| |
| /** |
| * Click handler for the entire document. |
| * @param {Event} e Mouse click event. |
| * @private |
| */ |
| SlideMode.prototype.onDocumentClick_ = function(e) { |
| // Close the bubble if clicked outside of it and if it is visible. |
| if (!this.bubble_.contains(e.target) && |
| !this.editButton_.contains(e.target) && |
| !this.arrowLeft_.contains(e.target) && |
| !this.arrowRight_.contains(e.target) && |
| !this.bubble_.hidden) { |
| this.bubble_.hidden = true; |
| } |
| }; |
| |
| /** |
| * Keydown handler. |
| * |
| * @param {Event} event Event. |
| * @return {boolean} True if handled. |
| */ |
| SlideMode.prototype.onKeyDown = function(event) { |
| var keyID = util.getKeyModifiers(event) + event.keyIdentifier; |
| |
| if (this.isSlideshowOn_()) { |
| switch (keyID) { |
| case 'U+001B': // Escape exits the slideshow. |
| this.stopSlideshow_(event); |
| break; |
| |
| case 'U+0020': // Space pauses/resumes the slideshow. |
| this.toggleSlideshowPause_(); |
| break; |
| |
| case 'Up': |
| case 'Down': |
| case 'Left': |
| case 'Right': |
| this.advanceWithKeyboard(keyID); |
| break; |
| } |
| return true; // Consume all keystrokes in the slideshow mode. |
| } |
| |
| if (this.isEditing() && this.editor_.onKeyDown(event)) |
| return true; |
| |
| switch (keyID) { |
| case 'U+0020': // Space toggles the video playback. |
| if (this.isShowingVideo_() && this.mediaControls_.getMedia()) |
| this.mediaControls_.togglePlayStateWithFeedback(); |
| break; |
| |
| case 'Ctrl-U+0050': // Ctrl+'p' prints the current image. |
| if (!this.printButton_.hasAttribute('disabled')) |
| this.print_(); |
| break; |
| |
| case 'U+0045': // 'e' toggles the editor. |
| if (!this.editButton_.hasAttribute('disabled')) |
| this.toggleEditor(event); |
| break; |
| |
| case 'U+001B': // Escape |
| if (!this.isEditing()) |
| return false; // Not handled. |
| this.toggleEditor(event); |
| break; |
| |
| case 'Home': |
| this.selectFirst(); |
| break; |
| case 'End': |
| this.selectLast(); |
| break; |
| case 'Up': |
| case 'Down': |
| case 'Left': |
| case 'Right': |
| this.advanceWithKeyboard(keyID); |
| break; |
| |
| default: return false; |
| } |
| |
| return true; |
| }; |
| |
| /** |
| * Resize handler. |
| * @private |
| */ |
| SlideMode.prototype.onResize_ = function() { |
| this.viewport_.sizeByFrameAndFit(this.container_); |
| this.viewport_.repaint(); |
| }; |
| |
| /** |
| * Update thumbnails. |
| */ |
| SlideMode.prototype.updateThumbnails = function() { |
| this.ribbon_.reset(); |
| if (this.active_) |
| this.ribbon_.redraw(); |
| }; |
| |
| // Saving |
| |
| /** |
| * Save the current image to a file. |
| * |
| * @param {function} callback Callback. |
| * @private |
| */ |
| SlideMode.prototype.saveCurrentImage_ = function(callback) { |
| var item = this.getSelectedItem(); |
| var oldEntry = item.getEntry(); |
| var canvas = this.imageView_.getCanvas(); |
| |
| this.showSpinner_(true); |
| var metadataEncoder = ImageEncoder.encodeMetadata( |
| this.selectedImageMetadata_.media, canvas, 1 /* quality */); |
| |
| this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( |
| metadataEncoder.getMetadata(), this.selectedImageMetadata_); |
| |
| item.saveToFile( |
| this.context_.saveDirEntry, |
| this.shouldOverwriteOriginal_(), |
| canvas, |
| metadataEncoder, |
| function(success) { |
| // TODO(kaznacheev): Implement write error handling. |
| // Until then pretend that the save succeeded. |
| this.showSpinner_(false); |
| this.flashSavedLabel_(); |
| |
| var event = new Event('content'); |
| event.item = item; |
| event.oldEntry = oldEntry; |
| event.metadata = this.selectedImageMetadata_; |
| this.dataModel_.dispatchEvent(event); |
| |
| // Allow changing the 'Overwrite original' setting only if the user |
| // used Undo to restore the original image AND it is not a copy. |
| // Otherwise lock the setting in its current state. |
| var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal(); |
| ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite); |
| |
| if (this.imageView_.getContentRevision() === 1) { // First edit. |
| ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); |
| } |
| |
| if (!util.isSameEntry(oldEntry, item.getEntry())) { |
| this.dataModel_.splice( |
| this.getSelectedIndex(), 0, new Gallery.Item(oldEntry)); |
| // The ribbon will ignore the splice above and redraw after the |
| // select call below (while being obscured by the Editor toolbar, |
| // so there is no need for nice animation here). |
| // SlideMode will ignore the selection change as the displayed item |
| // index has not changed. |
| this.select(++this.displayedIndex_); |
| } |
| callback(); |
| cr.dispatchSimpleEvent(this, 'image-saved'); |
| }.bind(this)); |
| }; |
| |
| /** |
| * Update caches when the selected item has been renamed. |
| * @param {Event} event Event. |
| * @private |
| */ |
| SlideMode.prototype.onContentChange_ = function(event) { |
| var newEntry = event.item.getEntry(); |
| if (util.isSameEntry(newEntry, event.oldEntry)) |
| this.imageView_.changeEntry(newEntry); |
| this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE); |
| }; |
| |
| /** |
| * Flash 'Saved' label briefly to indicate that the image has been saved. |
| * @private |
| */ |
| SlideMode.prototype.flashSavedLabel_ = function() { |
| var setLabelHighlighted = |
| ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); |
| setTimeout(setLabelHighlighted.bind(null, true), 0); |
| setTimeout(setLabelHighlighted.bind(null, false), 300); |
| }; |
| |
| /** |
| * Local storage key for the 'Overwrite original' setting. |
| * @type {string} |
| */ |
| SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original'; |
| |
| /** |
| * Local storage key for the number of times that |
| * the overwrite info bubble has been displayed. |
| * @type {string} |
| */ |
| SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble'; |
| |
| /** |
| * Max number that the overwrite info bubble is shown. |
| * @type {number} |
| */ |
| SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5; |
| |
| /** |
| * @return {boolean} True if 'Overwrite original' is set. |
| * @private |
| */ |
| SlideMode.prototype.shouldOverwriteOriginal_ = function() { |
| return this.overwriteOriginal_.checked; |
| }; |
| |
| /** |
| * 'Overwrite original' checkbox handler. |
| * @param {Event} event Event. |
| * @private |
| */ |
| SlideMode.prototype.onOverwriteOriginalClick_ = function(event) { |
| util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked); |
| }; |
| |
| /** |
| * Overwrite info bubble close handler. |
| * @private |
| */ |
| SlideMode.prototype.onCloseBubble_ = function() { |
| this.bubble_.hidden = true; |
| util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY, |
| SlideMode.OVERWRITE_BUBBLE_MAX_TIMES); |
| }; |
| |
| // Slideshow |
| |
| /** |
| * Slideshow interval in ms. |
| */ |
| SlideMode.SLIDESHOW_INTERVAL = 5000; |
| |
| /** |
| * First slideshow interval in ms. It should be shorter so that the user |
| * is not guessing whether the button worked. |
| */ |
| SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000; |
| |
| /** |
| * Empirically determined duration of the fullscreen toggle animation. |
| */ |
| SlideMode.FULLSCREEN_TOGGLE_DELAY = 500; |
| |
| /** |
| * @return {boolean} True if the slideshow is on. |
| * @private |
| */ |
| SlideMode.prototype.isSlideshowOn_ = function() { |
| return this.container_.hasAttribute('slideshow'); |
| }; |
| |
| /** |
| * Start the slideshow. |
| * @param {number=} opt_interval First interval in ms. |
| * @param {Event=} opt_event Event. |
| */ |
| SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) { |
| // Set the attribute early to prevent the toolbar from flashing when |
| // the slideshow is being started from the mosaic view. |
| this.container_.setAttribute('slideshow', 'playing'); |
| |
| if (this.active_) { |
| this.stopEditing_(); |
| } else { |
| // We are in the Mosaic mode. Toggle the mode but remember to return. |
| this.leaveAfterSlideshow_ = true; |
| this.toggleMode_(this.startSlideshow.bind( |
| this, SlideMode.SLIDESHOW_INTERVAL, opt_event)); |
| return; |
| } |
| |
| if (opt_event) // Caused by user action, notify the Gallery. |
| cr.dispatchSimpleEvent(this, 'useraction'); |
| |
| this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow); |
| if (!this.fullscreenBeforeSlideshow_) { |
| // Wait until the zoom animation from the mosaic mode is done. |
| setTimeout(this.toggleFullScreen_.bind(this), |
| ImageView.ZOOM_ANIMATION_DURATION); |
| opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) + |
| SlideMode.FULLSCREEN_TOGGLE_DELAY; |
| } |
| |
| this.resumeSlideshow_(opt_interval); |
| }; |
| |
| /** |
| * Stop the slideshow. |
| * @param {Event=} opt_event Event. |
| * @private |
| */ |
| SlideMode.prototype.stopSlideshow_ = function(opt_event) { |
| if (!this.isSlideshowOn_()) |
| return; |
| |
| if (opt_event) // Caused by user action, notify the Gallery. |
| cr.dispatchSimpleEvent(this, 'useraction'); |
| |
| this.pauseSlideshow_(); |
| this.container_.removeAttribute('slideshow'); |
| |
| // Do not restore fullscreen if we exited fullscreen while in slideshow. |
| var fullscreen = util.isFullScreen(this.context_.appWindow); |
| var toggleModeDelay = 0; |
| if (!this.fullscreenBeforeSlideshow_ && fullscreen) { |
| this.toggleFullScreen_(); |
| toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY; |
| } |
| if (this.leaveAfterSlideshow_) { |
| this.leaveAfterSlideshow_ = false; |
| setTimeout(this.toggleMode_.bind(this), toggleModeDelay); |
| } |
| }; |
| |
| /** |
| * @return {boolean} True if the slideshow is playing (not paused). |
| * @private |
| */ |
| SlideMode.prototype.isSlideshowPlaying_ = function() { |
| return this.container_.getAttribute('slideshow') === 'playing'; |
| }; |
| |
| /** |
| * Pause/resume the slideshow. |
| * @private |
| */ |
| SlideMode.prototype.toggleSlideshowPause_ = function() { |
| cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools. |
| if (this.isSlideshowPlaying_()) { |
| this.pauseSlideshow_(); |
| } else { |
| this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST); |
| } |
| }; |
| |
| /** |
| * @param {number=} opt_interval Slideshow interval in ms. |
| * @private |
| */ |
| SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) { |
| console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state'); |
| |
| if (this.slideShowTimeout_) |
| clearTimeout(this.slideShowTimeout_); |
| |
| this.slideShowTimeout_ = setTimeout(function() { |
| this.slideShowTimeout_ = null; |
| this.selectNext(1); |
| }.bind(this), |
| opt_interval || SlideMode.SLIDESHOW_INTERVAL); |
| }; |
| |
| /** |
| * Resume the slideshow. |
| * @param {number=} opt_interval Slideshow interval in ms. |
| * @private |
| */ |
| SlideMode.prototype.resumeSlideshow_ = function(opt_interval) { |
| this.container_.setAttribute('slideshow', 'playing'); |
| this.scheduleNextSlide_(opt_interval); |
| }; |
| |
| /** |
| * Pause the slideshow. |
| * @private |
| */ |
| SlideMode.prototype.pauseSlideshow_ = function() { |
| this.container_.setAttribute('slideshow', 'paused'); |
| if (this.slideShowTimeout_) { |
| clearTimeout(this.slideShowTimeout_); |
| this.slideShowTimeout_ = null; |
| } |
| }; |
| |
| /** |
| * @return {boolean} True if the editor is active. |
| */ |
| SlideMode.prototype.isEditing = function() { |
| return this.container_.hasAttribute('editing'); |
| }; |
| |
| /** |
| * Stop editing. |
| * @private |
| */ |
| SlideMode.prototype.stopEditing_ = function() { |
| if (this.isEditing()) |
| this.toggleEditor(); |
| }; |
| |
| /** |
| * Activate/deactivate editor. |
| * @param {Event=} opt_event Event. |
| */ |
| SlideMode.prototype.toggleEditor = function(opt_event) { |
| if (opt_event) // Caused by user action, notify the Gallery. |
| cr.dispatchSimpleEvent(this, 'useraction'); |
| |
| if (!this.active_) { |
| this.toggleMode_(this.toggleEditor.bind(this)); |
| return; |
| } |
| |
| this.stopSlideshow_(); |
| if (!this.isEditing() && this.isShowingVideo_()) |
| return; // No editing for videos. |
| |
| ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing()); |
| |
| if (this.isEditing()) { // isEditing has just been flipped to a new value. |
| if (this.context_.readonlyDirName) { |
| this.editor_.getPrompt().showAt( |
| 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName); |
| } |
| } else { |
| this.editor_.getPrompt().hide(); |
| this.editor_.leaveModeGently(); |
| } |
| }; |
| |
| /** |
| * Prints the current item. |
| * @private |
| */ |
| SlideMode.prototype.print_ = function() { |
| cr.dispatchSimpleEvent(this, 'useraction'); |
| window.print(); |
| }; |
| |
| /** |
| * Display the error banner. |
| * @param {string} message Message. |
| * @private |
| */ |
| SlideMode.prototype.showErrorBanner_ = function(message) { |
| if (message) { |
| this.errorBanner_.textContent = this.displayStringFunction_(message); |
| } |
| ImageUtil.setAttribute(this.container_, 'error', !!message); |
| }; |
| |
| /** |
| * Show/hide the busy spinner. |
| * |
| * @param {boolean} on True if show, false if hide. |
| * @private |
| */ |
| SlideMode.prototype.showSpinner_ = function(on) { |
| if (this.spinnerTimer_) { |
| clearTimeout(this.spinnerTimer_); |
| this.spinnerTimer_ = null; |
| } |
| |
| if (on) { |
| this.spinnerTimer_ = setTimeout(function() { |
| this.spinnerTimer_ = null; |
| ImageUtil.setAttribute(this.container_, 'spinner', true); |
| }.bind(this), 1000); |
| } else { |
| ImageUtil.setAttribute(this.container_, 'spinner', false); |
| } |
| }; |
| |
| /** |
| * @return {boolean} True if the current item is a video. |
| * @private |
| */ |
| SlideMode.prototype.isShowingVideo_ = function() { |
| return !!this.imageView_.getVideo(); |
| }; |
| |
| /** |
| * Overlay that handles swipe gestures. Changes to the next or previous file. |
| * @param {function(number)} callback A callback accepting the swipe direction |
| * (1 means left, -1 right). |
| * @constructor |
| * @implements {ImageBuffer.Overlay} |
| */ |
| function SwipeOverlay(callback) { |
| this.callback_ = callback; |
| } |
| |
| /** |
| * Inherit ImageBuffer.Overlay. |
| */ |
| SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; |
| |
| /** |
| * @param {number} x X pointer position. |
| * @param {number} y Y pointer position. |
| * @param {boolean} touch True if dragging caused by touch. |
| * @return {function} The closure to call on drag. |
| */ |
| SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { |
| if (!touch) |
| return null; |
| var origin = x; |
| var done = false; |
| return function(x, y) { |
| if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { |
| this.callback_(1); |
| done = true; |
| } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { |
| this.callback_(-1); |
| done = true; |
| } |
| }.bind(this); |
| }; |
| |
| /** |
| * If the user touched the image and moved the finger more than SWIPE_THRESHOLD |
| * horizontally it's considered as a swipe gesture (change the current image). |
| */ |
| SwipeOverlay.SWIPE_THRESHOLD = 100; |