1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * Slide mode displays a single image and has a set of controls to navigate
7 * between the images and to edit an image.
9 * @param {!HTMLElement} container Main container element.
10 * @param {!HTMLElement} content Content container element.
11 * @param {!HTMLElement} toolbar Toolbar element.
12 * @param {!ImageEditor.Prompt} prompt Prompt.
13 * @param {!ErrorBanner} errorBanner Error banner.
14 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
15 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
16 * @param {!MetadataModel} metadataModel
17 * @param {!ThumbnailModel} thumbnailModel
18 * @param {!Object} context Context.
19 * @param {!VolumeManagerWrapper} volumeManager Volume manager.
20 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
21 * @param {function(string):string} displayStringFunction String formatting
26 * @suppress {checkStructDictInheritance}
27 * @extends {cr.EventTarget}
29 function SlideMode(container, content, toolbar, prompt, errorBanner, dataModel,
30 selectionModel, metadataModel, thumbnailModel, context, volumeManager,
31 toggleMode, displayStringFunction) {
33 * @type {!HTMLElement}
37 this.container_ = container;
44 this.document_ = assert(container.ownerDocument);
47 * @type {!HTMLElement}
50 this.content = content;
53 * @type {!HTMLElement}
57 this.toolbar_ = toolbar;
60 * @type {!ImageEditor.Prompt}
64 this.prompt_ = prompt;
67 * @type {!ErrorBanner}
71 this.errorBanner_ = errorBanner;
74 * @type {!cr.ui.ArrayDataModel}
78 this.dataModel_ = dataModel;
81 * @type {!cr.ui.ListSelectionModel}
85 this.selectionModel_ = selectionModel;
92 this.context_ = context;
95 * @type {!VolumeManagerWrapper}
99 this.volumeManager_ = volumeManager;
102 * @type {function(function())}
106 this.toggleMode_ = toggleMode;
109 * @type {function(string):string}
113 this.displayStringFunction_ = displayStringFunction;
116 * @type {function(this:SlideMode)}
120 this.onSelectionBound_ = this.onSelection_.bind(this);
123 * @type {function(this:SlideMode,!Event)}
127 this.onSpliceBound_ = this.onSplice_.bind(this);
130 * Unique numeric key, incremented per each load attempt used to discard
131 * old attempts. This can happen especially when changing selection fast or
132 * Internet connection is slow.
137 this.currentUniqueKey_ = 0;
143 this.sequenceDirection_ = 0;
149 this.sequenceLength_ = 0;
152 * @type {Array.<number>}
155 this.savedSelection_ = null;
158 * @type {Gallery.Item}
161 this.displayedItem_ = null;
167 this.slideHint_ = null;
173 this.active_ = false;
179 this.leaveAfterSlideshow_ = false;
185 this.fullscreenBeforeSlideshow_ = false;
191 this.slideShowTimeout_ = null;
197 this.spinnerTimer_ = null;
199 window.addEventListener('resize', this.onResize_.bind(this));
201 // ----------------------------------------------------------------
202 // Initializes the UI.
205 * Container for displayed image.
206 * @type {!HTMLElement}
210 this.imageContainer_ = util.createChild(queryRequiredElement(
211 this.document_, '.content'), 'image-container');
212 this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
214 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
217 * Overwrite options and info bubble.
218 * @type {!HTMLElement}
222 this.options_ = util.createChild(queryRequiredElement(
223 this.toolbar_, '.filename-spacer'), 'options');
226 * @type {!HTMLElement}
230 this.savedLabel_ = util.createChild(this.options_, 'saved');
231 this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
234 * @type {!HTMLElement}
238 this.overwriteOriginalBox_ = util.createChild(
239 this.options_, 'overwrite-original');
242 * @type {!HTMLElement}
246 this.overwriteOriginal_ = util.createChild(
247 this.overwriteOriginalBox_, '', 'input');
248 this.overwriteOriginal_.type = 'checkbox';
249 this.overwriteOriginal_.id = 'overwrite-checkbox';
250 chrome.storage.local.get(SlideMode.OVERWRITE_KEY, function(values) {
251 var value = values[SlideMode.OVERWRITE_KEY];
252 // Out-of-the box default is 'true'
253 this.overwriteOriginal_.checked =
254 (value === 'false' || value === false) ? false : true;
256 this.overwriteOriginal_.addEventListener('click',
257 this.onOverwriteOriginalClick_.bind(this));
260 * @type {!HTMLElement}
263 var overwriteLabel = util.createChild(
264 this.overwriteOriginalBox_, '', 'label');
265 overwriteLabel.textContent =
266 this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
267 overwriteLabel.setAttribute('for', 'overwrite-checkbox');
270 * @type {!HTMLElement}
274 this.bubble_ = util.createChild(this.toolbar_, 'bubble');
275 this.bubble_.hidden = true;
278 * @type {!HTMLElement}
281 var bubbleContent = util.createChild(this.bubble_);
282 bubbleContent.innerHTML = this.displayStringFunction_(
283 'GALLERY_OVERWRITE_BUBBLE');
285 util.createChild(this.bubble_, 'pointer bottom', 'span');
288 * @type {!HTMLElement}
291 var bubbleClose = util.createChild(this.bubble_, 'close-x');
292 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
295 * Ribbon and related controls.
296 * @type {!HTMLElement}
300 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
303 * @type {!HTMLElement}
307 this.arrowLeft_ = util.createChild(
308 this.arrowBox_, 'arrow left tool dimmable');
309 this.arrowLeft_.addEventListener('click',
310 this.advanceManually.bind(this, -1));
311 util.createChild(this.arrowLeft_);
313 util.createChild(this.arrowBox_, 'arrow-spacer');
316 * @type {!HTMLElement}
320 this.arrowRight_ = util.createChild(
321 this.arrowBox_, 'arrow right tool dimmable');
322 this.arrowRight_.addEventListener('click',
323 this.advanceManually.bind(this, 1));
324 util.createChild(this.arrowRight_);
327 * @type {!HTMLElement}
331 this.ribbonSpacer_ = queryRequiredElement(this.toolbar_, '.ribbon-spacer');
338 this.ribbon_ = new Ribbon(
339 this.document_, this.dataModel_, this.selectionModel_, thumbnailModel);
340 this.ribbonSpacer_.appendChild(this.ribbon_);
342 util.createChild(this.container_, 'spinner');
345 * @type {!HTMLElement}
348 var slideShowButton = queryRequiredElement(this.toolbar_, 'button.slideshow');
349 slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
350 slideShowButton.addEventListener('click',
351 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
354 * @type {!HTMLElement}
357 var slideShowToolbar = util.createChild(
358 this.container_, 'tool slideshow-toolbar');
359 util.createChild(slideShowToolbar, 'slideshow-play').
360 addEventListener('click', this.toggleSlideshowPause_.bind(this));
361 util.createChild(slideShowToolbar, 'slideshow-end').
362 addEventListener('click', this.stopSlideshow_.bind(this));
366 * @type {!HTMLElement}
370 this.editButton_ = queryRequiredElement(this.toolbar_, 'button.edit');
371 this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
372 this.editButton_.disabled = true; // Disabled by default.
373 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
376 * @type {!HTMLElement}
380 this.printButton_ = queryRequiredElement(this.toolbar_, 'button.print');
381 this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
382 this.printButton_.disabled = true; // Disabled by default.
383 this.printButton_.addEventListener('click', this.print_.bind(this));
386 * @type {!HTMLElement}
390 this.editBarSpacer_ = queryRequiredElement(this.toolbar_, '.edit-bar-spacer');
393 * @type {!HTMLElement}
397 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
400 * @type {!HTMLElement}
404 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
407 * @type {!HTMLElement}
411 this.editBarModeWrapper_ = util.createChild(
412 this.editBarMode_, 'edit-modal-wrapper dimmable');
413 this.editBarModeWrapper_.hidden = true;
416 * Objects supporting image display and editing.
421 this.viewport_ = new Viewport();
428 this.imageView_ = new ImageView(
429 this.imageContainer_,
434 * @type {!ImageEditor}
438 this.editor_ = new ImageEditor(
443 root: this.container_,
444 image: this.imageContainer_,
445 toolbar: this.editBarMain_,
446 mode: this.editBarModeWrapper_
448 SlideMode.EDITOR_MODES,
449 this.displayStringFunction_,
450 this.onToolsVisibilityChanged_.bind(this));
453 * @type {!TouchHandler}
457 this.touchHandlers_ = new TouchHandler(this.imageContainer_, this);
461 * List of available editor modes.
462 * @type {!Array.<ImageEditor.Mode>}
465 SlideMode.EDITOR_MODES = [
466 new ImageEditor.Mode.InstantAutofix(),
467 new ImageEditor.Mode.Crop(),
468 new ImageEditor.Mode.Exposure(),
469 new ImageEditor.Mode.OneClick(
470 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
471 new ImageEditor.Mode.OneClick(
472 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
476 * Map of the key identifier and offset delta.
477 * @enum {!Array.<number>})
480 SlideMode.KEY_OFFSET_MAP = {
488 * SlideMode extends cr.EventTarget.
490 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
493 * @return {string} Mode name.
495 SlideMode.prototype.getName = function() { return 'slide'; };
498 * @return {string} Mode title.
500 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
503 * @return {!Viewport} Viewport.
505 SlideMode.prototype.getViewport = function() { return this.viewport_; };
508 * Load items, display the selected item.
509 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
510 * @param {function()} displayCallback Called when the image is displayed.
511 * @param {function()} loadCallback Called when the image is displayed.
513 SlideMode.prototype.enter = function(
514 zoomFromRect, displayCallback, loadCallback) {
515 this.sequenceDirection_ = 0;
516 this.sequenceLength_ = 0;
518 // The latest |leave| call might have left the image animating. Remove it.
521 new Promise(function(fulfill) {
522 // If the items are empty, just show the error message.
523 if (this.getItemCount_() === 0) {
524 this.displayedItem_ = null;
525 //TODO(hirono) Show this message in the grid mode too.
526 this.errorBanner_.show('GALLERY_NO_IMAGES');
531 // Remember the selection if it is empty or multiple. It will be restored
532 // in |leave| if the user did not changing the selection manually.
533 var currentSelection = this.selectionModel_.selectedIndexes;
534 if (currentSelection.length === 1)
535 this.savedSelection_ = null;
537 this.savedSelection_ = currentSelection;
539 // Ensure valid single selection.
540 // Note that the SlideMode object is not listening to selection change yet.
541 this.select(Math.max(0, this.getSelectedIndex()));
543 // Show the selected item ASAP, then complete the initialization
544 // (loading the ribbon thumbnails can take some time).
545 var selectedItem = this.getSelectedItem();
546 this.displayedItem_ = selectedItem;
548 // Load the image of the item.
550 assert(selectedItem),
552 this.imageView_.createZoomEffect(zoomFromRect) :
553 new ImageView.Effect.None(),
555 function(loadType, delay) {
558 }.bind(this)).then(function(delay) {
559 // Turn the mode active.
561 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
562 this.ribbon_.enable();
564 // Register handlers.
565 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
566 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
567 this.touchHandlers_.enabled = true;
569 // Wait 1000ms after the animation is done, then prefetch the next image.
570 this.requestPrefetch(1, delay + 1000);
572 // Call load callback.
575 }.bind(this)).catch(function(error) {
576 console.error(error.stack, error);
582 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
583 * @param {function()} callback Called when the image is committed and
584 * the zoom-out animation has started.
586 SlideMode.prototype.leave = function(zoomToRect, callback) {
587 var commitDone = function() {
589 this.stopSlideshow_();
590 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
591 this.selectionModel_.removeEventListener(
592 'change', this.onSelectionBound_);
593 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
594 this.ribbon_.disable();
595 this.active_ = false;
596 if (this.savedSelection_)
597 this.selectionModel_.selectedIndexes = this.savedSelection_;
598 this.unloadImage_(zoomToRect);
602 this.viewport_.resetView();
603 if (this.getItemCount_() === 0) {
604 this.errorBanner_.clear();
607 this.commitItem_(commitDone);
610 // Disable the slide-mode only buttons when leaving.
611 this.editButton_.disabled = true;
612 this.printButton_.disabled = true;
614 // Disable touch operation.
615 this.touchHandlers_.enabled = false;
620 * Execute an action when the editor is not busy.
622 * @param {function()} action Function to execute.
624 SlideMode.prototype.executeWhenReady = function(action) {
625 this.editor_.executeWhenReady(action);
629 * @return {boolean} True if the mode has active tools (that should not fade).
631 SlideMode.prototype.hasActiveTool = function() {
632 return this.isEditing();
636 * @return {number} Item count.
639 SlideMode.prototype.getItemCount_ = function() {
640 return this.dataModel_.length;
644 * @param {number} index Index.
645 * @return {Gallery.Item} Item.
647 SlideMode.prototype.getItem = function(index) {
649 /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
650 return item === undefined ? null : item;
654 * @return {number} Selected index.
656 SlideMode.prototype.getSelectedIndex = function() {
657 return this.selectionModel_.selectedIndex;
661 * @return {ImageRect} Screen rectangle of the selected image.
663 SlideMode.prototype.getSelectedImageRect = function() {
664 if (this.getSelectedIndex() < 0)
667 return this.viewport_.getImageBoundsOnScreen();
671 * @return {Gallery.Item} Selected item.
673 SlideMode.prototype.getSelectedItem = function() {
674 return this.getItem(this.getSelectedIndex());
678 * Toggles the full screen mode.
681 SlideMode.prototype.toggleFullScreen_ = function() {
682 util.toggleFullScreen(this.context_.appWindow,
683 !util.isFullScreen(this.context_.appWindow));
687 * Selection change handler.
689 * Commits the current image and displays the newly selected image.
692 SlideMode.prototype.onSelection_ = function() {
693 if (this.selectionModel_.selectedIndexes.length === 0)
694 return; // Ignore temporary empty selection.
696 // Forget the saved selection if the user changed the selection manually.
697 if (!this.isSlideshowOn_())
698 this.savedSelection_ = null;
700 if (this.getSelectedItem() === this.displayedItem_)
701 return; // Do not reselect.
703 this.commitItem_(this.loadSelectedItem_.bind(this));
707 * Handles changes in tools visibility, and if the header is dimmed, then
708 * requests disabling the draggable app region.
712 SlideMode.prototype.onToolsVisibilityChanged_ = function() {
713 var headerDimmed = queryRequiredElement(this.document_, '.header')
714 .hasAttribute('dimmed');
715 this.context_.onAppRegionChanged(!headerDimmed);
719 * Change the selection.
721 * @param {number} index New selected index.
722 * @param {number=} opt_slideHint Slide animation direction (-1|1).
724 SlideMode.prototype.select = function(index, opt_slideHint) {
725 this.slideHint_ = opt_slideHint || null;
726 this.selectionModel_.selectedIndex = index;
727 this.selectionModel_.leadIndex = index;
731 * Load the selected item.
735 SlideMode.prototype.loadSelectedItem_ = function() {
736 var slideHint = this.slideHint_;
737 this.slideHint_ = null;
739 if (this.getSelectedItem() === this.displayedItem_)
740 return; // Do not reselect.
742 var index = this.getSelectedIndex();
743 var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
745 slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);
747 if (Math.abs(step) != 1) {
748 // Long leap, the sequence is broken, we have no good prefetch candidate.
749 this.sequenceDirection_ = 0;
750 this.sequenceLength_ = 0;
751 } else if (this.sequenceDirection_ === step) {
752 // Keeping going in sequence.
753 this.sequenceLength_++;
755 // Reversed the direction. Reset the counter.
756 this.sequenceDirection_ = step;
757 this.sequenceLength_ = 1;
760 this.displayedItem_ = this.getSelectedItem();
761 var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);
763 function shouldPrefetch(loadType, step, sequenceLength) {
764 // Never prefetch when selecting out of sequence.
765 if (Math.abs(step) != 1)
768 // Always prefetch if the previous load was from cache.
769 if (loadType === ImageView.LoadType.CACHED_FULL)
772 // Prefetch if we have been going in the same direction for long enough.
773 return sequenceLength >= 3;
776 this.currentUniqueKey_++;
777 var selectedUniqueKey = this.currentUniqueKey_;
779 // Discard, since another load has been invoked after this one.
780 if (selectedUniqueKey != this.currentUniqueKey_)
785 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
786 function() {} /* no displayCallback */,
787 function(loadType, delay) {
788 // Discard, since another load has been invoked after this one.
789 if (selectedUniqueKey != this.currentUniqueKey_)
791 if (shouldPrefetch(loadType, step, this.sequenceLength_))
792 this.requestPrefetch(step, delay);
793 if (this.isSlideshowPlaying_())
794 this.scheduleNextSlide_();
799 * Unload the current image.
801 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
804 SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
805 this.imageView_.unload(opt_zoomToRect);
809 * Data model 'splice' event handler.
810 * @param {!Event} event Event.
814 SlideMode.prototype.onSplice_ = function(event) {
815 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
817 // Splice invalidates saved indices, drop the saved selection.
818 this.savedSelection_ = null;
820 if (event.removed.length != 1)
823 // Delay the selection to let the ribbon splice handler work first.
824 setTimeout(function() {
825 var displayedItemNotRemvoed = event.removed.every(function(item) {
826 return item !== this.displayedItem_;
828 if (displayedItemNotRemvoed)
831 if (event.index < this.dataModel_.length) {
832 // There is the next item, select it.
833 // The next item is now at the same index as the removed one, so we need
834 // to correct displayIndex_ so that loadSelectedItem_ does not think
835 // we are re-selecting the same item (and does right-to-left slide-in
837 nextIndex = event.index;
838 } else if (this.dataModel_.length) {
839 // Removed item is the rightmost, but there are more items.
840 nextIndex = event.index - 1; // Select the new last index.
842 // No items left. Unload the image, disable edit and print button, and
844 this.commitItem_(function() {
846 this.printButton_.disabled = true;
847 this.editButton_.disabled = true;
848 this.errorBanner_.show('GALLERY_NO_IMAGES');
852 // To force to dispatch a selection change event, clear selection before.
853 this.selectionModel_.clear();
854 this.select(nextIndex);
859 * @param {number} direction -1 for left, 1 for right.
860 * @return {number} Next index in the given direction, with wrapping.
863 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
864 function advance(index, limit) {
865 index += (direction > 0 ? 1 : -1);
873 // If the saved selection is multiple the Slideshow should cycle through
874 // the saved selection.
875 if (this.isSlideshowOn_() &&
876 this.savedSelection_ && this.savedSelection_.length > 1) {
877 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
878 this.savedSelection_.length);
879 return this.savedSelection_[pos];
881 return advance(this.getSelectedIndex(), this.getItemCount_());
886 * Advance the selection based on the pressed key ID.
887 * @param {string} keyID Key identifier.
889 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
890 var prev = (keyID === 'Up' ||
892 keyID === 'MediaPreviousTrack');
893 this.advanceManually(prev ? -1 : 1);
897 * Advance the selection as a result of a user action (as opposed to an
898 * automatic change in the slideshow mode).
899 * @param {number} direction -1 for left, 1 for right.
901 SlideMode.prototype.advanceManually = function(direction) {
902 if (this.isSlideshowPlaying_())
903 this.pauseSlideshow_();
904 cr.dispatchSimpleEvent(this, 'useraction');
905 this.selectNext(direction);
909 * Select the next item.
910 * @param {number} direction -1 for left, 1 for right.
912 SlideMode.prototype.selectNext = function(direction) {
913 this.select(this.getNextSelectedIndex_(direction), direction);
917 * Select the first item.
919 SlideMode.prototype.selectFirst = function() {
924 * Select the last item.
926 SlideMode.prototype.selectLast = function() {
927 this.select(this.getItemCount_() - 1);
933 * Load and display an item.
935 * @param {!Gallery.Item} item Item.
936 * @param {!ImageView.Effect} effect Transition effect object.
937 * @param {function()} displayCallback Called when the image is displayed
938 * (which can happen before the image load due to caching).
939 * @param {function(number, number)} loadCallback Called when the image is fully
943 SlideMode.prototype.loadItem_ = function(
944 item, effect, displayCallback, loadCallback) {
945 this.showSpinner_(true);
947 var loadDone = this.itemLoaded_.bind(this, item, loadCallback);
949 var displayDone = function() {
950 cr.dispatchSimpleEvent(this, 'image-displayed');
954 this.editor_.openSession(
957 this.saveCurrentImage_.bind(this, item),
963 * A callback function when the editor opens a editing session for an image.
964 * @param {!Gallery.Item} item Gallery item.
965 * @param {function(number, number)} loadCallback Called when the image is fully
967 * @param {number} loadType Load type.
968 * @param {number} delay Delay.
969 * @param {*=} opt_error Error.
972 SlideMode.prototype.itemLoaded_ = function(
973 item, loadCallback, loadType, delay, opt_error) {
974 var entry = item.getEntry();
976 this.showSpinner_(false);
977 if (loadType === ImageView.LoadType.ERROR) {
978 // if we have a specific error, then display it
980 this.errorBanner_.show(/** @type {string} */ (opt_error));
982 // otherwise try to infer general error
983 this.errorBanner_.show('GALLERY_IMAGE_ERROR');
985 } else if (loadType === ImageView.LoadType.OFFLINE) {
986 this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
989 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
991 var toMillions = function(number) {
992 return Math.round(number / (1000 * 1000));
995 var metadata = item.getMetadataItem();
997 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
998 toMillions(metadata.size));
1001 var canvas = this.imageView_.getCanvas();
1002 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
1003 toMillions(canvas.width * canvas.height));
1005 var extIndex = entry.name.lastIndexOf('.');
1006 var ext = extIndex < 0 ? '' :
1007 entry.name.substr(extIndex + 1).toLowerCase();
1008 if (ext === 'jpeg') ext = 'jpg';
1009 ImageUtil.metrics.recordEnum(
1010 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
1012 // Enable or disable buttons for editing and printing.
1014 this.editButton_.disabled = true;
1015 this.printButton_.disabled = true;
1017 this.editButton_.disabled = false;
1018 this.printButton_.disabled = false;
1021 // For once edited image, disallow the 'overwrite' setting change.
1022 ImageUtil.setAttribute(this.overwriteOriginalBox_, 'disabled',
1023 !this.getSelectedItem().isOriginal() || FileType.isRaw(item.getEntry()));
1026 keys[SlideMode.OVERWRITE_BUBBLE_KEY] = 0;
1027 keys[SlideMode.OVERWRITE_KEY] = true;
1028 chrome.storage.local.get(keys,
1030 var times = values[SlideMode.OVERWRITE_BUBBLE_KEY];
1031 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
1032 this.bubble_.hidden = false;
1033 if (this.isEditing()) {
1035 items[SlideMode.OVERWRITE_BUBBLE_KEY] = times + 1;
1036 chrome.storage.local.set(items);
1039 if (FileType.isRaw(item.getEntry()))
1040 this.overwriteOriginal_.checked = false;
1042 this.overwriteOriginal_.checked = values[SlideMode.OVERWRITE_KEY];
1045 loadCallback(loadType, delay);
1049 * Commit changes to the current item and reset all messages/indicators.
1051 * @param {function()} callback Callback.
1054 SlideMode.prototype.commitItem_ = function(callback) {
1055 this.showSpinner_(false);
1056 this.errorBanner_.clear();
1057 this.editor_.getPrompt().hide();
1058 this.editor_.closeSession(callback);
1062 * Request a prefetch for the next image.
1064 * @param {number} direction -1 or 1.
1065 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1066 * loading from disrupting the animation that might be still in progress.
1068 SlideMode.prototype.requestPrefetch = function(direction, delay) {
1069 if (this.getItemCount_() <= 1) return;
1071 var index = this.getNextSelectedIndex_(direction);
1072 this.imageView_.prefetch(assert(this.getItem(index)), delay);
1078 * Click handler for the image container.
1080 * @param {!Event} event Mouse click event.
1083 SlideMode.prototype.onClick_ = function(event) {
1087 * Click handler for the entire document.
1088 * @param {!Event} event Mouse click event.
1091 SlideMode.prototype.onDocumentClick_ = function(event) {
1092 // Events created in fakeMouseClick in test util don't pass this test.
1093 if (!window.IN_TEST)
1094 event = assertInstanceof(event, MouseEvent);
1096 var targetElement = assertInstanceof(event.target, HTMLElement);
1097 // Close the bubble if clicked outside of it and if it is visible.
1098 if (!this.bubble_.contains(targetElement) &&
1099 !this.editButton_.contains(targetElement) &&
1100 !this.arrowLeft_.contains(targetElement) &&
1101 !this.arrowRight_.contains(targetElement) &&
1102 !this.bubble_.hidden) {
1103 this.bubble_.hidden = true;
1110 * @param {!Event} event Event.
1111 * @return {boolean} True if handled.
1113 SlideMode.prototype.onKeyDown = function(event) {
1114 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
1116 if (this.isSlideshowOn_()) {
1118 case 'U+001B': // Escape exits the slideshow.
1120 this.stopSlideshow_(event);
1123 case 'U+0020': // Space pauses/resumes the slideshow.
1124 case 'MediaPlayPause':
1125 this.toggleSlideshowPause_();
1132 case 'MediaNextTrack':
1133 case 'MediaPreviousTrack':
1134 this.advanceWithKeyboard(keyID);
1137 return true; // Consume all keystrokes in the slideshow mode.
1140 if (this.isEditing() && this.editor_.onKeyDown(event))
1144 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
1145 if (!this.printButton_.disabled)
1149 case 'U+0045': // 'e' toggles the editor.
1150 if (!this.editButton_.disabled)
1151 this.toggleEditor(event);
1154 case 'U+001B': // Escape
1155 if (this.isEditing()) {
1156 this.toggleEditor(event);
1157 } else if (this.viewport_.isZoomed()) {
1158 this.viewport_.resetView();
1159 this.touchHandlers_.stopOperation();
1160 this.imageView_.applyViewportChange();
1162 return false; // Not handled.
1176 if (!this.isEditing() && this.viewport_.isZoomed()) {
1177 var delta = SlideMode.KEY_OFFSET_MAP[keyID];
1178 this.viewport_.setOffset(
1179 ~~(this.viewport_.getOffsetX() +
1180 delta[0] * this.viewport_.getZoom()),
1181 ~~(this.viewport_.getOffsetY() +
1182 delta[1] * this.viewport_.getZoom()));
1183 this.touchHandlers_.stopOperation();
1184 this.imageView_.applyViewportChange();
1186 this.advanceWithKeyboard(keyID);
1189 case 'MediaNextTrack':
1190 case 'MediaPreviousTrack':
1191 this.advanceWithKeyboard(keyID);
1194 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
1195 if (!this.isEditing()) {
1196 this.viewport_.zoomIn();
1197 this.touchHandlers_.stopOperation();
1198 this.imageView_.applyViewportChange();
1202 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
1203 if (!this.isEditing()) {
1204 this.viewport_.zoomOut();
1205 this.touchHandlers_.stopOperation();
1206 this.imageView_.applyViewportChange();
1210 case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1211 if (!this.isEditing()) {
1212 this.viewport_.setZoom(1.0);
1213 this.touchHandlers_.stopOperation();
1214 this.imageView_.applyViewportChange();
1226 SlideMode.prototype.onResize_ = function() {
1227 this.viewport_.setScreenSize(
1228 this.container_.clientWidth, this.container_.clientHeight);
1229 this.touchHandlers_.stopOperation();
1230 this.editor_.getBuffer().draw();
1234 * Update thumbnails.
1236 SlideMode.prototype.updateThumbnails = function() {
1237 this.ribbon_.reset();
1239 this.ribbon_.redraw();
1245 * Save the current image to a file.
1247 * @param {!Gallery.Item} item Item to save the image.
1248 * @param {function()} callback Callback.
1251 SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
1252 this.showSpinner_(true);
1254 var savedPromise = this.dataModel_.saveItem(
1255 this.volumeManager_,
1257 this.imageView_.getCanvas(),
1258 this.shouldOverwriteOriginal_());
1260 savedPromise.then(function() {
1261 this.showSpinner_(false);
1262 this.flashSavedLabel_();
1264 // Allow changing the 'Overwrite original' setting only if the user
1265 // used Undo to restore the original image AND it is not a copy.
1266 // Otherwise lock the setting in its current state.
1267 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal() &&
1268 !FileType.isRaw(item.getEntry());
1269 ImageUtil.setAttribute(
1270 this.overwriteOriginalBox_, 'disabled', !mayChangeOverwrite);
1272 // Record UMA for the first edit.
1273 if (this.imageView_.getContentRevision() === 1)
1274 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
1277 }.bind(this)).catch(function(error) {
1278 console.error(error.stack || error);
1280 this.showSpinner_(false);
1281 this.errorBanner_.show('GALLERY_SAVE_FAILED');
1288 * Flash 'Saved' label briefly to indicate that the image has been saved.
1291 SlideMode.prototype.flashSavedLabel_ = function() {
1292 var setLabelHighlighted =
1293 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1294 setTimeout(setLabelHighlighted.bind(null, true), 0);
1295 setTimeout(setLabelHighlighted.bind(null, false), 300);
1299 * Local storage key for the 'Overwrite original' setting.
1302 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
1305 * Local storage key for the number of times that
1306 * the overwrite info bubble has been displayed.
1309 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1312 * Max number that the overwrite info bubble is shown.
1315 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1318 * @return {boolean} True if 'Overwrite original' is set.
1321 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
1322 return this.overwriteOriginal_.checked;
1326 * 'Overwrite original' checkbox handler.
1327 * @param {!Event} event Event.
1330 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
1332 items[SlideMode.OVERWRITE_KEY] = event.target.checked;
1333 chrome.storage.local.set(items);
1337 * Overwrite info bubble close handler.
1340 SlideMode.prototype.onCloseBubble_ = function() {
1341 this.bubble_.hidden = true;
1343 items[SlideMode.OVERWRITE_BUBBLE_KEY] =
1344 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES;
1345 chrome.storage.local.set(items);
1351 * Slideshow interval in ms.
1353 SlideMode.SLIDESHOW_INTERVAL = 5000;
1356 * First slideshow interval in ms. It should be shorter so that the user
1357 * is not guessing whether the button worked.
1359 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1362 * Empirically determined duration of the fullscreen toggle animation.
1364 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1367 * @return {boolean} True if the slideshow is on.
1370 SlideMode.prototype.isSlideshowOn_ = function() {
1371 return this.container_.hasAttribute('slideshow');
1375 * Starts the slideshow.
1376 * @param {number=} opt_interval First interval in ms.
1377 * @param {Event=} opt_event Event.
1379 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1381 this.viewport_.resetView();
1382 this.imageView_.applyViewportChange();
1384 // Disable touch operation.
1385 this.touchHandlers_.enabled = false;
1387 // Set the attribute early to prevent the toolbar from flashing when
1388 // the slideshow is being started from the mosaic view.
1389 this.container_.setAttribute('slideshow', 'playing');
1392 this.stopEditing_();
1394 // We are in the Mosaic mode. Toggle the mode but remember to return.
1395 this.leaveAfterSlideshow_ = true;
1397 // Wait until the zoom animation from the mosaic mode is done.
1398 var startSlideshowAfterTransition = function() {
1399 setTimeout(function() {
1400 this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
1401 }.bind(this), ImageView.MODE_TRANSITION_DURATION);
1403 this.toggleMode_(startSlideshowAfterTransition);
1407 if (opt_event) // Caused by user action, notify the Gallery.
1408 cr.dispatchSimpleEvent(this, 'useraction');
1410 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1411 if (!this.fullscreenBeforeSlideshow_) {
1412 this.toggleFullScreen_();
1413 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1414 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1417 this.resumeSlideshow_(opt_interval);
1421 * Stops the slideshow.
1422 * @param {Event=} opt_event Event.
1425 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1426 if (!this.isSlideshowOn_())
1429 if (opt_event) // Caused by user action, notify the Gallery.
1430 cr.dispatchSimpleEvent(this, 'useraction');
1432 this.pauseSlideshow_();
1433 this.container_.removeAttribute('slideshow');
1435 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1436 var fullscreen = util.isFullScreen(this.context_.appWindow);
1437 var toggleModeDelay = 0;
1438 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1439 this.toggleFullScreen_();
1440 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1442 if (this.leaveAfterSlideshow_) {
1443 this.leaveAfterSlideshow_ = false;
1444 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1447 // Re-enable touch operation.
1448 this.touchHandlers_.enabled = true;
1452 * @return {boolean} True if the slideshow is playing (not paused).
1455 SlideMode.prototype.isSlideshowPlaying_ = function() {
1456 return this.container_.getAttribute('slideshow') === 'playing';
1460 * Pauses/resumes the slideshow.
1463 SlideMode.prototype.toggleSlideshowPause_ = function() {
1464 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1465 if (this.isSlideshowPlaying_()) {
1466 this.pauseSlideshow_();
1468 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1473 * @param {number=} opt_interval Slideshow interval in ms.
1476 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1477 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1479 if (this.slideShowTimeout_)
1480 clearTimeout(this.slideShowTimeout_);
1482 this.slideShowTimeout_ = setTimeout(function() {
1483 this.slideShowTimeout_ = null;
1485 }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1489 * Resumes the slideshow.
1490 * @param {number=} opt_interval Slideshow interval in ms.
1493 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1494 this.container_.setAttribute('slideshow', 'playing');
1495 this.scheduleNextSlide_(opt_interval);
1499 * Pauses the slideshow.
1502 SlideMode.prototype.pauseSlideshow_ = function() {
1503 this.container_.setAttribute('slideshow', 'paused');
1504 if (this.slideShowTimeout_) {
1505 clearTimeout(this.slideShowTimeout_);
1506 this.slideShowTimeout_ = null;
1511 * @return {boolean} True if the editor is active.
1513 SlideMode.prototype.isEditing = function() {
1514 return this.container_.hasAttribute('editing');
1521 SlideMode.prototype.stopEditing_ = function() {
1522 if (this.isEditing())
1523 this.toggleEditor();
1527 * Activate/deactivate editor.
1528 * @param {Event=} opt_event Event.
1530 SlideMode.prototype.toggleEditor = function(opt_event) {
1531 if (opt_event) // Caused by user action, notify the Gallery.
1532 cr.dispatchSimpleEvent(this, 'useraction');
1534 if (!this.active_) {
1535 this.toggleMode_(this.toggleEditor.bind(this));
1539 this.stopSlideshow_();
1541 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1543 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1545 this.viewport_.resetView();
1546 this.imageView_.applyViewportChange();
1547 if (this.context_.readonlyDirName) {
1548 this.editor_.getPrompt().showAt(
1549 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1551 this.touchHandlers_.enabled = false;
1553 this.editor_.getPrompt().hide();
1554 this.editor_.leaveModeGently();
1555 this.touchHandlers_.enabled = true;
1560 * Prints the current item.
1563 SlideMode.prototype.print_ = function() {
1564 cr.dispatchSimpleEvent(this, 'useraction');
1569 * Shows/hides the busy spinner.
1571 * @param {boolean} on True if show, false if hide.
1574 SlideMode.prototype.showSpinner_ = function(on) {
1575 if (this.spinnerTimer_) {
1576 clearTimeout(this.spinnerTimer_);
1577 this.spinnerTimer_ = null;
1581 this.spinnerTimer_ = setTimeout(function() {
1582 this.spinnerTimer_ = null;
1583 ImageUtil.setAttribute(this.container_, 'spinner', true);
1584 }.bind(this), 1000);
1586 ImageUtil.setAttribute(this.container_, 'spinner', false);
1591 * Apply the change of viewport.
1593 SlideMode.prototype.applyViewportChange = function() {
1594 this.imageView_.applyViewportChange();
1598 * Touch handlers of the slide mode.
1599 * @param {!Element} targetElement Event source.
1600 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1604 function TouchHandler(targetElement, slideMode) {
1611 this.targetElement_ = targetElement;
1614 * Target of touch operations.
1615 * @type {!SlideMode}
1619 this.slideMode_ = slideMode;
1622 * Flag to enable/disable touch operation.
1626 this.enabled_ = true;
1629 * Whether it is in a touch operation that is started from targetElement or
1634 this.touchStarted_ = false;
1637 * The swipe action that should happen only once in an operation is already
1645 * Event on beginning of the current gesture.
1646 * The variable is updated when the number of touch finger changed.
1647 * @type {TouchEvent}
1650 this.gestureStartEvent_ = null;
1653 * Rotation value on beginning of the current gesture.
1657 this.gestureStartRotation_ = 0;
1661 * @type {TouchEvent}
1664 this.lastEvent_ = null;
1667 * Zoom value just after last touch event.
1671 this.lastZoom_ = 1.0;
1673 targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
1674 var onTouchEventBound = this.onTouchEvent_.bind(this);
1675 targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
1676 targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
1678 targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
1682 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1683 * horizontally it's considered as a swipe gesture (change the current image).
1687 TouchHandler.SWIPE_THRESHOLD = 100;
1690 * Rotation threshold in degrees.
1694 TouchHandler.ROTATION_THRESHOLD = 25;
1697 * Obtains distance between fingers.
1698 * @param {!TouchEvent} event Touch event. It should include more than two
1700 * @return {number} Distance between touch[0] and touch[1].
1702 TouchHandler.getDistance = function(event) {
1703 var touch1 = event.touches[0];
1704 var touch2 = event.touches[1];
1705 var dx = touch1.clientX - touch2.clientX;
1706 var dy = touch1.clientY - touch2.clientY;
1707 return Math.sqrt(dx * dx + dy * dy);
1711 * Obtains the degrees of the pinch twist angle.
1712 * @param {!TouchEvent} event1 Start touch event. It should include more than
1714 * @param {!TouchEvent} event2 Current touch event. It should include more than
1716 * @return {number} Degrees of the pinch twist angle.
1718 TouchHandler.getTwistAngle = function(event1, event2) {
1719 var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
1720 var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
1721 var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
1722 var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
1723 var innerProduct = dx1 * dx2 + dy1 * dy2; // |v1| * |v2| * cos(t) = x / r
1724 var outerProduct = dx1 * dy2 - dy1 * dx2; // |v1| * |v2| * sin(t) = y / r
1725 return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI; // atan(y / x)
1728 TouchHandler.prototype = /** @struct */ {
1730 * @param {boolean} flag New value.
1733 this.enabled_ = flag;
1735 this.stopOperation();
1740 * Stops the current touch operation.
1742 TouchHandler.prototype.stopOperation = function() {
1743 this.touchStarted_ = false;
1745 this.gestureStartEvent_ = null;
1746 this.lastEvent_ = null;
1747 this.lastZoom_ = 1.0;
1751 * Handles touch start events.
1752 * @param {!Event} event Touch event.
1755 TouchHandler.prototype.onTouchStart_ = function(event) {
1756 event = assertInstanceof(event, TouchEvent);
1757 if (this.enabled_ && event.touches.length === 1)
1758 this.touchStarted_ = true;
1762 * Handles touch move and touch end events.
1763 * @param {!Event} event Touch event.
1766 TouchHandler.prototype.onTouchEvent_ = function(event) {
1767 event = assertInstanceof(event, TouchEvent);
1768 // Check if the current touch operation started from the target element or
1770 if (!this.touchStarted_)
1773 // Check if the current touch operation ends with the event.
1774 if (event.touches.length === 0) {
1775 this.stopOperation();
1779 // Check if a new gesture started or not.
1780 var viewport = this.slideMode_.getViewport();
1781 if (!this.lastEvent_ ||
1782 this.lastEvent_.touches.length !== event.touches.length) {
1783 if (event.touches.length === 2 ||
1784 event.touches.length === 1) {
1785 this.gestureStartEvent_ = event;
1786 this.gestureStartRotation_ = viewport.getRotation();
1787 this.lastEvent_ = event;
1788 this.lastZoom_ = viewport.getZoom();
1790 this.gestureStartEvent_ = null;
1791 this.gestureStartRotation_ = 0;
1792 this.lastEvent_ = null;
1793 this.lastZoom_ = 1.0;
1798 // Handle the gesture movement.
1799 switch (event.touches.length) {
1801 if (viewport.isZoomed()) {
1802 // Scrolling an image by swipe.
1803 var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
1804 var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
1806 viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
1807 this.slideMode_.applyViewportChange();
1809 // Traversing images by swipe.
1813 event.touches[0].clientX -
1814 this.gestureStartEvent_.touches[0].clientX;
1815 if (dx > TouchHandler.SWIPE_THRESHOLD) {
1816 this.slideMode_.advanceManually(-1);
1818 } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
1819 this.slideMode_.advanceManually(1);
1827 var distance1 = TouchHandler.getDistance(this.lastEvent_);
1828 var distance2 = TouchHandler.getDistance(event);
1829 if (distance1 === 0)
1831 var zoom = distance2 / distance1 * this.lastZoom_;
1832 viewport.setZoom(zoom);
1835 assert(this.gestureStartEvent_);
1836 var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
1837 if (angle > TouchHandler.ROTATION_THRESHOLD)
1838 viewport.setRotation(this.gestureStartRotation_ + 1);
1839 else if (angle < -TouchHandler.ROTATION_THRESHOLD)
1840 viewport.setRotation(this.gestureStartRotation_ - 1);
1842 viewport.setRotation(this.gestureStartRotation_);
1843 this.slideMode_.applyViewportChange();
1847 // Update the last event.
1848 this.lastEvent_ = event;
1849 this.lastZoom_ = viewport.getZoom();
1853 * Handles mouse wheel events.
1854 * @param {!Event} event Wheel event.
1857 TouchHandler.prototype.onMouseWheel_ = function(event) {
1858 var event = assertInstanceof(event, MouseEvent);
1859 var viewport = this.slideMode_.getViewport();
1860 if (!this.enabled_ || !viewport.isZoomed())
1862 this.stopOperation();
1864 viewport.getOffsetX() + event.wheelDeltaX,
1865 viewport.getOffsetY() + event.wheelDeltaY);
1866 this.slideMode_.applyViewportChange();