1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
8 * Slide mode displays a single image and has a set of controls to navigate
9 * between the images and to edit an image.
11 * TODO(kaznacheev): Introduce a parameter object.
13 * @param {Element} container Main container element.
14 * @param {Element} content Content container element.
15 * @param {Element} toolbar Toolbar element.
16 * @param {ImageEditor.Prompt} prompt Prompt.
17 * @param {cr.ui.ArrayDataModel} dataModel Data model.
18 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
19 * @param {Object} context Context.
20 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
21 * @param {function(string):string} displayStringFunction String formatting
25 function SlideMode(container, content, toolbar, prompt,
26 dataModel, selectionModel, context,
27 toggleMode, displayStringFunction) {
28 this.container_ = container;
29 this.document_ = container.ownerDocument;
30 this.content = content;
31 this.toolbar_ = toolbar;
32 this.prompt_ = prompt;
33 this.dataModel_ = dataModel;
34 this.selectionModel_ = selectionModel;
35 this.context_ = context;
36 this.metadataCache_ = context.metadataCache;
37 this.toggleMode_ = toggleMode;
38 this.displayStringFunction_ = displayStringFunction;
40 this.onSelectionBound_ = this.onSelection_.bind(this);
41 this.onSpliceBound_ = this.onSplice_.bind(this);
42 this.onContentBound_ = this.onContentChange_.bind(this);
44 // Unique numeric key, incremented per each load attempt used to discard
45 // old attempts. This can happen especially when changing selection fast or
46 // Internet connection is slow.
47 this.currentUniqueKey_ = 0;
49 this.initListeners_();
54 * SlideMode extends cr.EventTarget.
56 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
59 * List of available editor modes.
60 * @type {Array.<ImageEditor.Mode>}
62 SlideMode.editorModes = [
63 new ImageEditor.Mode.InstantAutofix(),
64 new ImageEditor.Mode.Crop(),
65 new ImageEditor.Mode.Exposure(),
66 new ImageEditor.Mode.OneClick(
67 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
68 new ImageEditor.Mode.OneClick(
69 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
73 * @return {string} Mode name.
75 SlideMode.prototype.getName = function() { return 'slide' };
78 * @return {string} Mode title.
80 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' };
83 * Initialize the listeners.
86 SlideMode.prototype.initListeners_ = function() {
87 window.addEventListener('resize', this.onResize_.bind(this), false);
94 SlideMode.prototype.initDom_ = function() {
95 // Container for displayed image or video.
96 this.imageContainer_ = util.createChild(
97 this.document_.querySelector('.content'), 'image-container');
98 this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
100 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
102 // Overwrite options and info bubble.
103 this.options_ = util.createChild(
104 this.toolbar_.querySelector('.filename-spacer'), 'options');
106 this.savedLabel_ = util.createChild(this.options_, 'saved');
107 this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
109 var overwriteOriginalBox =
110 util.createChild(this.options_, 'overwrite-original');
112 this.overwriteOriginal_ = util.createChild(
113 overwriteOriginalBox, 'common white', 'input');
114 this.overwriteOriginal_.type = 'checkbox';
115 this.overwriteOriginal_.id = 'overwrite-checkbox';
116 util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) {
117 // Out-of-the box default is 'true'
118 this.overwriteOriginal_.checked =
119 (typeof value !== 'string' || value === 'true');
121 this.overwriteOriginal_.addEventListener('click',
122 this.onOverwriteOriginalClick_.bind(this));
124 var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
125 overwriteLabel.textContent =
126 this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
127 overwriteLabel.setAttribute('for', 'overwrite-checkbox');
129 this.bubble_ = util.createChild(this.toolbar_, 'bubble');
130 this.bubble_.hidden = true;
132 var bubbleContent = util.createChild(this.bubble_);
133 bubbleContent.innerHTML = this.displayStringFunction_(
134 'GALLERY_OVERWRITE_BUBBLE');
136 util.createChild(this.bubble_, 'pointer bottom', 'span');
138 var bubbleClose = util.createChild(this.bubble_, 'close-x');
139 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
141 // Video player controls.
143 util.createChild(this.container_, 'video-controls-spacer');
144 this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool');
145 this.mediaControls_ = new VideoControls(
147 this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'),
148 this.displayStringFunction_.bind(this),
149 this.toggleFullScreen_.bind(this),
152 // Ribbon and related controls.
153 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
156 util.createChild(this.arrowBox_, 'arrow left tool dimmable');
157 this.arrowLeft_.addEventListener('click',
158 this.advanceManually.bind(this, -1));
159 util.createChild(this.arrowLeft_);
161 util.createChild(this.arrowBox_, 'arrow-spacer');
164 util.createChild(this.arrowBox_, 'arrow right tool dimmable');
165 this.arrowRight_.addEventListener('click',
166 this.advanceManually.bind(this, 1));
167 util.createChild(this.arrowRight_);
169 this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer');
170 this.ribbon_ = new Ribbon(this.document_,
171 this.metadataCache_, this.dataModel_, this.selectionModel_);
172 this.ribbonSpacer_.appendChild(this.ribbon_);
175 var errorWrapper = util.createChild(this.container_, 'prompt-wrapper');
176 errorWrapper.setAttribute('pos', 'center');
178 this.errorBanner_ = util.createChild(errorWrapper, 'error-banner');
180 util.createChild(this.container_, 'spinner');
182 var slideShowButton = util.createChild(this.toolbar_,
183 'button slideshow', 'button');
184 slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
185 slideShowButton.addEventListener('click',
186 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
188 var slideShowToolbar =
189 util.createChild(this.container_, 'tool slideshow-toolbar');
190 util.createChild(slideShowToolbar, 'slideshow-play').
191 addEventListener('click', this.toggleSlideshowPause_.bind(this));
192 util.createChild(slideShowToolbar, 'slideshow-end').
193 addEventListener('click', this.stopSlideshow_.bind(this));
197 this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button');
198 this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
199 this.editButton_.setAttribute('disabled', ''); // Disabled by default.
200 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
202 this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button');
203 this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
204 this.printButton_.setAttribute('disabled', ''); // Disabled by default.
205 this.printButton_.addEventListener('click', this.print_.bind(this));
207 this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer');
208 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
210 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
211 this.editBarModeWrapper_ = util.createChild(
212 this.editBarMode_, 'edit-modal-wrapper');
213 this.editBarModeWrapper_.hidden = true;
215 // Objects supporting image display and editing.
216 this.viewport_ = new Viewport();
218 this.imageView_ = new ImageView(
219 this.imageContainer_,
221 this.metadataCache_);
223 this.editor_ = new ImageEditor(
228 root: this.container_,
229 image: this.imageContainer_,
230 toolbar: this.editBarMain_,
231 mode: this.editBarModeWrapper_
233 SlideMode.editorModes,
234 this.displayStringFunction_,
235 this.onToolsVisibilityChanged_.bind(this));
237 this.editor_.getBuffer().addOverlay(
238 new SwipeOverlay(this.advanceManually.bind(this)));
242 * Load items, display the selected item.
243 * @param {Rect} zoomFromRect Rectangle for zoom effect.
244 * @param {function} displayCallback Called when the image is displayed.
245 * @param {function} loadCallback Called when the image is displayed.
247 SlideMode.prototype.enter = function(
248 zoomFromRect, displayCallback, loadCallback) {
249 this.sequenceDirection_ = 0;
250 this.sequenceLength_ = 0;
252 var loadDone = function(loadType, delay) {
255 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
256 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
257 this.dataModel_.addEventListener('content', this.onContentBound_);
259 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
260 this.ribbon_.enable();
262 // Wait 1000ms after the animation is done, then prefetch the next image.
263 this.requestPrefetch(1, delay + 1000);
265 if (loadCallback) loadCallback();
268 // The latest |leave| call might have left the image animating. Remove it.
271 if (this.getItemCount_() === 0) {
272 this.displayedIndex_ = -1;
273 //TODO(kaznacheev) Show this message in the grid mode too.
274 this.showErrorBanner_('GALLERY_NO_IMAGES');
277 // Remember the selection if it is empty or multiple. It will be restored
278 // in |leave| if the user did not changing the selection manually.
279 var currentSelection = this.selectionModel_.selectedIndexes;
280 if (currentSelection.length === 1)
281 this.savedSelection_ = null;
283 this.savedSelection_ = currentSelection;
285 // Ensure valid single selection.
286 // Note that the SlideMode object is not listening to selection change yet.
287 this.select(Math.max(0, this.getSelectedIndex()));
288 this.displayedIndex_ = this.getSelectedIndex();
290 var selectedItem = this.getSelectedItem();
291 // Show the selected item ASAP, then complete the initialization
292 // (loading the ribbon thumbnails can take some time).
293 this.metadataCache_.get(selectedItem.getEntry(), Gallery.METADATA_TYPE,
295 this.loadItem_(selectedItem.getEntry(), metadata,
296 zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect),
297 displayCallback, loadDone);
305 * @param {Rect} zoomToRect Rectangle for zoom effect.
306 * @param {function} callback Called when the image is committed and
307 * the zoom-out animation has started.
309 SlideMode.prototype.leave = function(zoomToRect, callback) {
310 var commitDone = function() {
312 this.stopSlideshow_();
313 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
314 this.selectionModel_.removeEventListener(
315 'change', this.onSelectionBound_);
316 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
317 this.dataModel_.removeEventListener('content', this.onContentBound_);
318 this.ribbon_.disable();
319 this.active_ = false;
320 if (this.savedSelection_)
321 this.selectionModel_.selectedIndexes = this.savedSelection_;
322 this.unloadImage_(zoomToRect);
326 if (this.getItemCount_() === 0) {
327 this.showErrorBanner_(false);
330 this.commitItem_(commitDone);
333 // Disable the slide-mode only buttons when leaving.
334 this.editButton_.setAttribute('disabled', '');
335 this.printButton_.setAttribute('disabled', '');
340 * Execute an action when the editor is not busy.
342 * @param {function} action Function to execute.
344 SlideMode.prototype.executeWhenReady = function(action) {
345 this.editor_.executeWhenReady(action);
349 * @return {boolean} True if the mode has active tools (that should not fade).
351 SlideMode.prototype.hasActiveTool = function() {
352 return this.isEditing();
356 * @return {number} Item count.
359 SlideMode.prototype.getItemCount_ = function() {
360 return this.dataModel_.length;
364 * @param {number} index Index.
365 * @return {Gallery.Item} Item.
367 SlideMode.prototype.getItem = function(index) {
368 return this.dataModel_.item(index);
372 * @return {Gallery.Item} Selected index.
374 SlideMode.prototype.getSelectedIndex = function() {
375 return this.selectionModel_.selectedIndex;
379 * @return {Rect} Screen rectangle of the selected image.
381 SlideMode.prototype.getSelectedImageRect = function() {
382 if (this.getSelectedIndex() < 0)
385 return this.viewport_.getScreenClipped();
389 * @return {Gallery.Item} Selected item.
391 SlideMode.prototype.getSelectedItem = function() {
392 return this.getItem(this.getSelectedIndex());
396 * Toggles the full screen mode.
399 SlideMode.prototype.toggleFullScreen_ = function() {
400 util.toggleFullScreen(this.context_.appWindow,
401 !util.isFullScreen(this.context_.appWindow));
405 * Selection change handler.
407 * Commits the current image and displays the newly selected image.
410 SlideMode.prototype.onSelection_ = function() {
411 if (this.selectionModel_.selectedIndexes.length === 0)
412 return; // Temporary empty selection.
414 // Forget the saved selection if the user changed the selection manually.
415 if (!this.isSlideshowOn_())
416 this.savedSelection_ = null;
418 if (this.getSelectedIndex() === this.displayedIndex_)
419 return; // Do not reselect.
421 this.commitItem_(this.loadSelectedItem_.bind(this));
425 * Handles changes in tools visibility, and if the header is dimmed, then
426 * requests disabling the draggable app region.
430 SlideMode.prototype.onToolsVisibilityChanged_ = function() {
432 this.document_.querySelector('.header').hasAttribute('dimmed');
433 this.context_.onAppRegionChanged(!headerDimmed);
437 * Change the selection.
439 * @param {number} index New selected index.
440 * @param {number=} opt_slideHint Slide animation direction (-1|1).
442 SlideMode.prototype.select = function(index, opt_slideHint) {
443 this.slideHint_ = opt_slideHint;
444 this.selectionModel_.selectedIndex = index;
445 this.selectionModel_.leadIndex = index;
449 * Load the selected item.
453 SlideMode.prototype.loadSelectedItem_ = function() {
454 var slideHint = this.slideHint_;
455 this.slideHint_ = undefined;
457 var index = this.getSelectedIndex();
458 if (index === this.displayedIndex_)
459 return; // Do not reselect.
461 var step = slideHint || (index - this.displayedIndex_);
463 if (Math.abs(step) != 1) {
464 // Long leap, the sequence is broken, we have no good prefetch candidate.
465 this.sequenceDirection_ = 0;
466 this.sequenceLength_ = 0;
467 } else if (this.sequenceDirection_ === step) {
468 // Keeping going in sequence.
469 this.sequenceLength_++;
471 // Reversed the direction. Reset the counter.
472 this.sequenceDirection_ = step;
473 this.sequenceLength_ = 1;
476 if (this.sequenceLength_ <= 1) {
477 // We have just broke the sequence. Touch the current image so that it stays
478 // in the cache longer.
479 this.imageView_.prefetch(this.imageView_.contentEntry_);
482 this.displayedIndex_ = index;
484 function shouldPrefetch(loadType, step, sequenceLength) {
485 // Never prefetch when selecting out of sequence.
486 if (Math.abs(step) != 1)
489 // Never prefetch after a video load (decoding the next image can freeze
490 // the UI for a second or two).
491 if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE)
494 // Always prefetch if the previous load was from cache.
495 if (loadType === ImageView.LOAD_TYPE_CACHED_FULL)
498 // Prefetch if we have been going in the same direction for long enough.
499 return sequenceLength >= 3;
502 var selectedItem = this.getSelectedItem();
503 this.currentUniqueKey_++;
504 var selectedUniqueKey = this.currentUniqueKey_;
505 var onMetadata = function(metadata) {
506 // Discard, since another load has been invoked after this one.
507 if (selectedUniqueKey != this.currentUniqueKey_) return;
508 this.loadItem_(selectedItem.getEntry(), metadata,
509 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
510 function() {} /* no displayCallback */,
511 function(loadType, delay) {
512 // Discard, since another load has been invoked after this one.
513 if (selectedUniqueKey != this.currentUniqueKey_) return;
514 if (shouldPrefetch(loadType, step, this.sequenceLength_)) {
515 this.requestPrefetch(step, delay);
517 if (this.isSlideshowPlaying_())
518 this.scheduleNextSlide_();
521 this.metadataCache_.get(
522 selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata);
526 * Unload the current image.
528 * @param {Rect} zoomToRect Rectangle for zoom effect.
531 SlideMode.prototype.unloadImage_ = function(zoomToRect) {
532 this.imageView_.unload(zoomToRect);
533 this.container_.removeAttribute('video');
537 * Data model 'splice' event handler.
538 * @param {Event} event Event.
541 SlideMode.prototype.onSplice_ = function(event) {
542 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
544 // Splice invalidates saved indices, drop the saved selection.
545 this.savedSelection_ = null;
547 if (event.removed.length != 1)
550 // Delay the selection to let the ribbon splice handler work first.
551 setTimeout(function() {
552 if (event.index < this.dataModel_.length) {
553 // There is the next item, select it.
554 // The next item is now at the same index as the removed one, so we need
555 // to correct displayIndex_ so that loadSelectedItem_ does not think
556 // we are re-selecting the same item (and does right-to-left slide-in
558 this.displayedIndex_ = event.index - 1;
559 this.select(event.index);
560 } else if (this.dataModel_.length) {
561 // Removed item is the rightmost, but there are more items.
562 this.select(event.index - 1); // Select the new last index.
564 // No items left. Unload the image and show the banner.
565 this.commitItem_(function() {
567 this.showErrorBanner_('GALLERY_NO_IMAGES');
574 * @param {number} direction -1 for left, 1 for right.
575 * @return {number} Next index in the given direction, with wrapping.
578 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
579 function advance(index, limit) {
580 index += (direction > 0 ? 1 : -1);
588 // If the saved selection is multiple the Slideshow should cycle through
589 // the saved selection.
590 if (this.isSlideshowOn_() &&
591 this.savedSelection_ && this.savedSelection_.length > 1) {
592 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
593 this.savedSelection_.length);
594 return this.savedSelection_[pos];
596 return advance(this.getSelectedIndex(), this.getItemCount_());
601 * Advance the selection based on the pressed key ID.
602 * @param {string} keyID Key identifier.
604 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
605 this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1);
609 * Advance the selection as a result of a user action (as opposed to an
610 * automatic change in the slideshow mode).
611 * @param {number} direction -1 for left, 1 for right.
613 SlideMode.prototype.advanceManually = function(direction) {
614 if (this.isSlideshowPlaying_()) {
615 this.pauseSlideshow_();
616 cr.dispatchSimpleEvent(this, 'useraction');
618 this.selectNext(direction);
622 * Select the next item.
623 * @param {number} direction -1 for left, 1 for right.
625 SlideMode.prototype.selectNext = function(direction) {
626 this.select(this.getNextSelectedIndex_(direction), direction);
630 * Select the first item.
632 SlideMode.prototype.selectFirst = function() {
637 * Select the last item.
639 SlideMode.prototype.selectLast = function() {
640 this.select(this.getItemCount_() - 1);
646 * Load and display an item.
648 * @param {FileEntry} entry Item entry to be loaded.
649 * @param {Object} metadata Item metadata.
650 * @param {Object} effect Transition effect object.
651 * @param {function} displayCallback Called when the image is displayed
652 * (which can happen before the image load due to caching).
653 * @param {function} loadCallback Called when the image is fully loaded.
656 SlideMode.prototype.loadItem_ = function(
657 entry, metadata, effect, displayCallback, loadCallback) {
658 this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata);
660 this.showSpinner_(true);
662 var loadDone = function(loadType, delay, error) {
663 var video = this.isShowingVideo_();
664 ImageUtil.setAttribute(this.container_, 'video', video);
666 this.showSpinner_(false);
667 if (loadType === ImageView.LOAD_TYPE_ERROR) {
668 // if we have a specific error, then display it
670 this.showErrorBanner_(error);
672 // otherwise try to infer general error
673 this.showErrorBanner_(
674 video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR');
676 } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) {
677 this.showErrorBanner_(
678 video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE');
682 // The editor toolbar does not make sense for video, hide it.
684 this.mediaControls_.attachMedia(this.imageView_.getVideo());
686 // TODO(kaznacheev): Add metrics for video playback.
688 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
690 var toMillions = function(number) {
691 return Math.round(number / (1000 * 1000));
694 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
695 toMillions(metadata.filesystem.size));
697 var canvas = this.imageView_.getCanvas();
698 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
699 toMillions(canvas.width * canvas.height));
701 var extIndex = entry.name.lastIndexOf('.');
702 var ext = extIndex < 0 ? '' :
703 entry.name.substr(extIndex + 1).toLowerCase();
704 if (ext === 'jpeg') ext = 'jpg';
705 ImageUtil.metrics.recordEnum(
706 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
709 // Enable or disable buttons for editing and printing.
710 if (video || error) {
711 this.editButton_.setAttribute('disabled', '');
712 this.printButton_.setAttribute('disabled', '');
714 this.editButton_.removeAttribute('disabled');
715 this.printButton_.removeAttribute('disabled');
718 // For once edited image, disallow the 'overwrite' setting change.
719 ImageUtil.setAttribute(this.options_, 'saved',
720 !this.getSelectedItem().isOriginal());
722 util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
724 var times = typeof value === 'string' ? parseInt(value, 10) : 0;
725 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
726 this.bubble_.hidden = false;
727 if (this.isEditing()) {
728 util.platform.setPreference(
729 SlideMode.OVERWRITE_BUBBLE_KEY, times + 1);
734 loadCallback(loadType, delay);
737 var displayDone = function() {
738 cr.dispatchSimpleEvent(this, 'image-displayed');
742 this.editor_.openSession(entry, metadata, effect,
743 this.saveCurrentImage_.bind(this), displayDone, loadDone);
747 * Commit changes to the current item and reset all messages/indicators.
749 * @param {function} callback Callback.
752 SlideMode.prototype.commitItem_ = function(callback) {
753 this.showSpinner_(false);
754 this.showErrorBanner_(false);
755 this.editor_.getPrompt().hide();
757 // Detach any media attached to the controls.
758 if (this.mediaControls_.getMedia())
759 this.mediaControls_.detachMedia();
761 // If showing the video, then pause it. Note, that it may not be attached
762 // to the media controls yet.
763 if (this.isShowingVideo_()) {
764 this.imageView_.getVideo().pause();
765 // Force stop downloading, if uncached on Drive.
766 this.imageView_.getVideo().src = '';
767 this.imageView_.getVideo().load();
770 this.editor_.closeSession(callback);
774 * Request a prefetch for the next image.
776 * @param {number} direction -1 or 1.
777 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
778 * loading from disrupting the animation that might be still in progress.
780 SlideMode.prototype.requestPrefetch = function(direction, delay) {
781 if (this.getItemCount_() <= 1) return;
783 var index = this.getNextSelectedIndex_(direction);
784 var nextItemEntry = this.getItem(index).getEntry();
785 this.imageView_.prefetch(nextItemEntry, delay);
791 * Unload handler, to be called from the top frame.
792 * @param {boolean} exiting True if the app is exiting.
794 SlideMode.prototype.onUnload = function(exiting) {
795 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) {
796 this.mediaControls_.savePosition(exiting);
801 * beforeunload handler, to be called from the top frame.
802 * @return {string} Message to show if there are unsaved changes.
804 SlideMode.prototype.onBeforeUnload = function() {
805 if (this.editor_.isBusy())
806 return this.displayStringFunction_('GALLERY_UNSAVED_CHANGES');
811 * Click handler for the image container.
813 * @param {Event} event Mouse click event.
816 SlideMode.prototype.onClick_ = function(event) {
817 if (!this.isShowingVideo_() || !this.mediaControls_.getMedia())
820 this.mediaControls_.toggleLoopedModeWithFeedback(true);
821 if (!this.mediaControls_.isPlaying())
822 this.mediaControls_.togglePlayStateWithFeedback();
824 this.mediaControls_.togglePlayStateWithFeedback();
829 * Click handler for the entire document.
830 * @param {Event} e Mouse click event.
833 SlideMode.prototype.onDocumentClick_ = function(e) {
834 // Close the bubble if clicked outside of it and if it is visible.
835 if (!this.bubble_.contains(e.target) &&
836 !this.editButton_.contains(e.target) &&
837 !this.arrowLeft_.contains(e.target) &&
838 !this.arrowRight_.contains(e.target) &&
839 !this.bubble_.hidden) {
840 this.bubble_.hidden = true;
847 * @param {Event} event Event.
848 * @return {boolean} True if handled.
850 SlideMode.prototype.onKeyDown = function(event) {
851 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
853 if (this.isSlideshowOn_()) {
855 case 'U+001B': // Escape exits the slideshow.
856 this.stopSlideshow_(event);
859 case 'U+0020': // Space pauses/resumes the slideshow.
860 this.toggleSlideshowPause_();
867 this.advanceWithKeyboard(keyID);
870 return true; // Consume all keystrokes in the slideshow mode.
873 if (this.isEditing() && this.editor_.onKeyDown(event))
877 case 'U+0020': // Space toggles the video playback.
878 if (this.isShowingVideo_() && this.mediaControls_.getMedia())
879 this.mediaControls_.togglePlayStateWithFeedback();
882 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
883 if (!this.printButton_.hasAttribute('disabled'))
887 case 'U+0045': // 'e' toggles the editor.
888 if (!this.editButton_.hasAttribute('disabled'))
889 this.toggleEditor(event);
892 case 'U+001B': // Escape
893 if (!this.isEditing())
894 return false; // Not handled.
895 this.toggleEditor(event);
908 this.advanceWithKeyboard(keyID);
911 default: return false;
921 SlideMode.prototype.onResize_ = function() {
922 this.viewport_.sizeByFrameAndFit(this.container_);
923 this.viewport_.repaint();
929 SlideMode.prototype.updateThumbnails = function() {
930 this.ribbon_.reset();
932 this.ribbon_.redraw();
938 * Save the current image to a file.
940 * @param {function} callback Callback.
943 SlideMode.prototype.saveCurrentImage_ = function(callback) {
944 var item = this.getSelectedItem();
945 var oldEntry = item.getEntry();
946 var canvas = this.imageView_.getCanvas();
948 this.showSpinner_(true);
949 var metadataEncoder = ImageEncoder.encodeMetadata(
950 this.selectedImageMetadata_.media, canvas, 1 /* quality */);
952 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata(
953 metadataEncoder.getMetadata(), this.selectedImageMetadata_);
956 this.context_.saveDirEntry,
957 this.shouldOverwriteOriginal_(),
961 // TODO(kaznacheev): Implement write error handling.
962 // Until then pretend that the save succeeded.
963 this.showSpinner_(false);
964 this.flashSavedLabel_();
966 var event = new Event('content');
968 event.oldEntry = oldEntry;
969 event.metadata = this.selectedImageMetadata_;
970 this.dataModel_.dispatchEvent(event);
972 // Allow changing the 'Overwrite original' setting only if the user
973 // used Undo to restore the original image AND it is not a copy.
974 // Otherwise lock the setting in its current state.
975 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
976 ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
978 if (this.imageView_.getContentRevision() === 1) { // First edit.
979 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
982 if (!util.isSameEntry(oldEntry, item.getEntry())) {
983 this.dataModel_.splice(
984 this.getSelectedIndex(), 0, new Gallery.Item(oldEntry));
985 // The ribbon will ignore the splice above and redraw after the
986 // select call below (while being obscured by the Editor toolbar,
987 // so there is no need for nice animation here).
988 // SlideMode will ignore the selection change as the displayed item
989 // index has not changed.
990 this.select(++this.displayedIndex_);
993 cr.dispatchSimpleEvent(this, 'image-saved');
998 * Update caches when the selected item has been renamed.
999 * @param {Event} event Event.
1002 SlideMode.prototype.onContentChange_ = function(event) {
1003 var newEntry = event.item.getEntry();
1004 if (util.isSameEntry(newEntry, event.oldEntry))
1005 this.imageView_.changeEntry(newEntry);
1006 this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE);
1010 * Flash 'Saved' label briefly to indicate that the image has been saved.
1013 SlideMode.prototype.flashSavedLabel_ = function() {
1014 var setLabelHighlighted =
1015 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1016 setTimeout(setLabelHighlighted.bind(null, true), 0);
1017 setTimeout(setLabelHighlighted.bind(null, false), 300);
1021 * Local storage key for the 'Overwrite original' setting.
1024 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
1027 * Local storage key for the number of times that
1028 * the overwrite info bubble has been displayed.
1031 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1034 * Max number that the overwrite info bubble is shown.
1037 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1040 * @return {boolean} True if 'Overwrite original' is set.
1043 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
1044 return this.overwriteOriginal_.checked;
1048 * 'Overwrite original' checkbox handler.
1049 * @param {Event} event Event.
1052 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
1053 util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked);
1057 * Overwrite info bubble close handler.
1060 SlideMode.prototype.onCloseBubble_ = function() {
1061 this.bubble_.hidden = true;
1062 util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
1063 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1069 * Slideshow interval in ms.
1071 SlideMode.SLIDESHOW_INTERVAL = 5000;
1074 * First slideshow interval in ms. It should be shorter so that the user
1075 * is not guessing whether the button worked.
1077 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1080 * Empirically determined duration of the fullscreen toggle animation.
1082 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1085 * @return {boolean} True if the slideshow is on.
1088 SlideMode.prototype.isSlideshowOn_ = function() {
1089 return this.container_.hasAttribute('slideshow');
1093 * Start the slideshow.
1094 * @param {number=} opt_interval First interval in ms.
1095 * @param {Event=} opt_event Event.
1097 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1098 // Set the attribute early to prevent the toolbar from flashing when
1099 // the slideshow is being started from the mosaic view.
1100 this.container_.setAttribute('slideshow', 'playing');
1103 this.stopEditing_();
1105 // We are in the Mosaic mode. Toggle the mode but remember to return.
1106 this.leaveAfterSlideshow_ = true;
1107 this.toggleMode_(this.startSlideshow.bind(
1108 this, SlideMode.SLIDESHOW_INTERVAL, opt_event));
1112 if (opt_event) // Caused by user action, notify the Gallery.
1113 cr.dispatchSimpleEvent(this, 'useraction');
1115 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1116 if (!this.fullscreenBeforeSlideshow_) {
1117 // Wait until the zoom animation from the mosaic mode is done.
1118 setTimeout(this.toggleFullScreen_.bind(this),
1119 ImageView.ZOOM_ANIMATION_DURATION);
1120 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1121 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1124 this.resumeSlideshow_(opt_interval);
1128 * Stop the slideshow.
1129 * @param {Event=} opt_event Event.
1132 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1133 if (!this.isSlideshowOn_())
1136 if (opt_event) // Caused by user action, notify the Gallery.
1137 cr.dispatchSimpleEvent(this, 'useraction');
1139 this.pauseSlideshow_();
1140 this.container_.removeAttribute('slideshow');
1142 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1143 var fullscreen = util.isFullScreen(this.context_.appWindow);
1144 var toggleModeDelay = 0;
1145 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1146 this.toggleFullScreen_();
1147 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1149 if (this.leaveAfterSlideshow_) {
1150 this.leaveAfterSlideshow_ = false;
1151 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1156 * @return {boolean} True if the slideshow is playing (not paused).
1159 SlideMode.prototype.isSlideshowPlaying_ = function() {
1160 return this.container_.getAttribute('slideshow') === 'playing';
1164 * Pause/resume the slideshow.
1167 SlideMode.prototype.toggleSlideshowPause_ = function() {
1168 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1169 if (this.isSlideshowPlaying_()) {
1170 this.pauseSlideshow_();
1172 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1177 * @param {number=} opt_interval Slideshow interval in ms.
1180 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1181 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1183 if (this.slideShowTimeout_)
1184 clearTimeout(this.slideShowTimeout_);
1186 this.slideShowTimeout_ = setTimeout(function() {
1187 this.slideShowTimeout_ = null;
1190 opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1194 * Resume the slideshow.
1195 * @param {number=} opt_interval Slideshow interval in ms.
1198 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1199 this.container_.setAttribute('slideshow', 'playing');
1200 this.scheduleNextSlide_(opt_interval);
1204 * Pause the slideshow.
1207 SlideMode.prototype.pauseSlideshow_ = function() {
1208 this.container_.setAttribute('slideshow', 'paused');
1209 if (this.slideShowTimeout_) {
1210 clearTimeout(this.slideShowTimeout_);
1211 this.slideShowTimeout_ = null;
1216 * @return {boolean} True if the editor is active.
1218 SlideMode.prototype.isEditing = function() {
1219 return this.container_.hasAttribute('editing');
1226 SlideMode.prototype.stopEditing_ = function() {
1227 if (this.isEditing())
1228 this.toggleEditor();
1232 * Activate/deactivate editor.
1233 * @param {Event=} opt_event Event.
1235 SlideMode.prototype.toggleEditor = function(opt_event) {
1236 if (opt_event) // Caused by user action, notify the Gallery.
1237 cr.dispatchSimpleEvent(this, 'useraction');
1239 if (!this.active_) {
1240 this.toggleMode_(this.toggleEditor.bind(this));
1244 this.stopSlideshow_();
1245 if (!this.isEditing() && this.isShowingVideo_())
1246 return; // No editing for videos.
1248 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1250 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1251 if (this.context_.readonlyDirName) {
1252 this.editor_.getPrompt().showAt(
1253 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1256 this.editor_.getPrompt().hide();
1257 this.editor_.leaveModeGently();
1262 * Prints the current item.
1265 SlideMode.prototype.print_ = function() {
1266 cr.dispatchSimpleEvent(this, 'useraction');
1271 * Display the error banner.
1272 * @param {string} message Message.
1275 SlideMode.prototype.showErrorBanner_ = function(message) {
1277 this.errorBanner_.textContent = this.displayStringFunction_(message);
1279 ImageUtil.setAttribute(this.container_, 'error', !!message);
1283 * Show/hide the busy spinner.
1285 * @param {boolean} on True if show, false if hide.
1288 SlideMode.prototype.showSpinner_ = function(on) {
1289 if (this.spinnerTimer_) {
1290 clearTimeout(this.spinnerTimer_);
1291 this.spinnerTimer_ = null;
1295 this.spinnerTimer_ = setTimeout(function() {
1296 this.spinnerTimer_ = null;
1297 ImageUtil.setAttribute(this.container_, 'spinner', true);
1298 }.bind(this), 1000);
1300 ImageUtil.setAttribute(this.container_, 'spinner', false);
1305 * @return {boolean} True if the current item is a video.
1308 SlideMode.prototype.isShowingVideo_ = function() {
1309 return !!this.imageView_.getVideo();
1313 * Overlay that handles swipe gestures. Changes to the next or previous file.
1314 * @param {function(number)} callback A callback accepting the swipe direction
1315 * (1 means left, -1 right).
1317 * @implements {ImageBuffer.Overlay}
1319 function SwipeOverlay(callback) {
1320 this.callback_ = callback;
1324 * Inherit ImageBuffer.Overlay.
1326 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype;
1329 * @param {number} x X pointer position.
1330 * @param {number} y Y pointer position.
1331 * @param {boolean} touch True if dragging caused by touch.
1332 * @return {function} The closure to call on drag.
1334 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) {
1339 return function(x, y) {
1340 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) {
1343 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) {
1351 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1352 * horizontally it's considered as a swipe gesture (change the current image).
1354 SwipeOverlay.SWIPE_THRESHOLD = 100;