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} topToolbar Top toolbar element.
12 * @param {!HTMLElement} bottomToolbar Toolbar element.
13 * @param {!ImageEditor.Prompt} prompt Prompt.
14 * @param {!ErrorBanner} errorBanner Error banner.
15 * @param {!cr.ui.ArrayDataModel} dataModel Data model.
16 * @param {!cr.ui.ListSelectionModel} selectionModel Selection model.
17 * @param {!MetadataModel} metadataModel
18 * @param {!ThumbnailModel} thumbnailModel
19 * @param {!Object} context Context.
20 * @param {!VolumeManagerWrapper} volumeManager Volume manager.
21 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
22 * @param {function(string):string} displayStringFunction String formatting
24 * @param {!DimmableUIController} dimmableUIController Dimmable UI controller.
27 * @extends {cr.EventTarget}
29 function SlideMode(container
, content
, topToolbar
, bottomToolbar
, prompt
,
30 errorBanner
, dataModel
, selectionModel
, metadataModel
, thumbnailModel
,
31 context
, volumeManager
, toggleMode
, displayStringFunction
,
32 dimmableUIController
) {
34 * @type {!HTMLElement}
38 this.container_
= container
;
45 this.document_
= assert(container
.ownerDocument
);
48 * @type {!HTMLElement}
51 this.content
= content
;
54 * @type {!HTMLElement}
58 this.topToolbar_
= topToolbar
;
61 * @type {!HTMLElement}
65 this.bottomToolbar_
= bottomToolbar
;
68 * @type {!ImageEditor.Prompt}
72 this.prompt_
= prompt
;
75 * @type {!ErrorBanner}
79 this.errorBanner_
= errorBanner
;
82 * @type {!cr.ui.ArrayDataModel}
86 this.dataModel_
= dataModel
;
89 * @type {!cr.ui.ListSelectionModel}
93 this.selectionModel_
= selectionModel
;
100 this.context_
= context
;
103 * @type {!VolumeManagerWrapper}
107 this.volumeManager_
= volumeManager
;
110 * @type {function(function())}
114 this.toggleMode_
= toggleMode
;
117 * @type {function(string):string}
121 this.displayStringFunction_
= displayStringFunction
;
124 * @private {!DimmableUIController}
127 this.dimmableUIController_
= dimmableUIController
;
130 * @type {function(this:SlideMode)}
134 this.onSelectionBound_
= this.onSelection_
.bind(this);
137 * @type {function(this:SlideMode,!Event)}
141 this.onSpliceBound_
= this.onSplice_
.bind(this);
144 * Unique numeric key, incremented per each load attempt used to discard
145 * old attempts. This can happen especially when changing selection fast or
146 * Internet connection is slow.
151 this.currentUniqueKey_
= 0;
157 this.sequenceDirection_
= 0;
163 this.sequenceLength_
= 0;
166 * @type {Array<number>}
169 this.savedSelection_
= null;
172 * @type {Gallery.Item}
175 this.displayedItem_
= null;
181 this.slideHint_
= null;
187 this.active_
= false;
193 this.leaveAfterSlideshow_
= false;
199 this.fullscreenBeforeSlideshow_
= false;
205 this.slideShowTimeout_
= null;
211 this.spinnerTimer_
= null;
213 window
.addEventListener('resize', this.onResize_
.bind(this));
215 // ----------------------------------------------------------------
216 // Initializes the UI.
219 * Container for displayed image.
220 * @type {!HTMLElement}
224 this.imageContainer_
= util
.createChild(queryRequiredElement(
225 '.content', this.document_
), 'image-container');
227 this.document_
.addEventListener('click', this.onDocumentClick_
.bind(this));
230 * Overwrite options and info bubble.
231 * @type {!HTMLElement}
235 this.options_
= queryRequiredElement('.options', this.bottomToolbar_
);
238 * @type {!HTMLElement}
242 this.savedLabel_
= queryRequiredElement('.saved', this.options_
);
245 * @private {!PaperCheckboxElement}
248 this.overwriteOriginalCheckbox_
= /** @type {!PaperCheckboxElement} */
249 (queryRequiredElement('.overwrite-original', this.options_
));
250 this.overwriteOriginalCheckbox_
.addEventListener('change',
251 this.onOverwriteOriginalCheckboxChanged_
.bind(this));
254 * @private {!FilesToast}
257 this.filesToast_
= /** @type {!FilesToast} */
258 (queryRequiredElement('files-toast'));
261 * @private {!HTMLElement}
264 this.bubble_
= queryRequiredElement('.bubble', this.bottomToolbar_
);
266 var bubbleContent
= queryRequiredElement('.content', this.bubble_
);
267 // GALLERY_OVERWRITE_BUBBLE contains <br> tag inside message.
268 bubbleContent
.innerHTML
= strf('GALLERY_OVERWRITE_BUBBLE');
270 var bubbleClose
= queryRequiredElement('.close-x', this.bubble_
);
271 bubbleClose
.addEventListener('click', this.onCloseBubble_
.bind(this));
274 * Ribbon and related controls.
275 * @type {!HTMLElement}
279 this.arrowBox_
= util
.createChild(this.container_
, 'arrow-box');
282 * @type {!HTMLElement}
286 this.arrowLeft_
= util
.createChild(
287 this.arrowBox_
, 'arrow left tool dimmable');
288 this.arrowLeft_
.addEventListener('click',
289 this.advanceManually
.bind(this, -1));
290 util
.createChild(this.arrowLeft_
);
293 * @type {!HTMLElement}
297 this.arrowRight_
= util
.createChild(
298 this.arrowBox_
, 'arrow right tool dimmable');
299 this.arrowRight_
.addEventListener('click',
300 this.advanceManually
.bind(this, 1));
301 util
.createChild(this.arrowRight_
);
304 * @type {!HTMLElement}
308 this.ribbonSpacer_
= queryRequiredElement('.ribbon-spacer',
309 this.bottomToolbar_
);
316 this.ribbon_
= new Ribbon(this.document_
, window
, this.dataModel_
,
317 this.selectionModel_
, thumbnailModel
);
318 this.ribbonSpacer_
.appendChild(this.ribbon_
);
320 util
.createChild(this.container_
, 'spinner');
323 * @type {!HTMLElement}
326 var slideShowButton
= queryRequiredElement('paper-button.slideshow',
328 slideShowButton
.addEventListener('click',
329 this.startSlideshow
.bind(this, SlideMode
.SLIDESHOW_INTERVAL_FIRST
));
332 * @type {!HTMLElement}
335 var slideShowToolbar
= util
.createChild(
336 this.container_
, 'tool slideshow-toolbar');
337 util
.createChild(slideShowToolbar
, 'slideshow-play').
338 addEventListener('click', this.toggleSlideshowPause_
.bind(this));
339 util
.createChild(slideShowToolbar
, 'slideshow-end').
340 addEventListener('click', this.stopSlideshow_
.bind(this));
344 * @type {!HTMLElement}
348 this.editButton_
= queryRequiredElement('button.edit', this.topToolbar_
);
349 GalleryUtil
.decorateMouseFocusHandling(this.editButton_
);
350 this.editButton_
.addEventListener('click', this.toggleEditor
.bind(this));
353 * @private {!FilesToggleRipple}
356 this.editButtonToggleRipple_
= /** @type {!FilesToggleRipple} */
357 (assert(this.editButton_
.querySelector('files-toggle-ripple')));
360 * @type {!HTMLElement}
364 this.printButton_
= queryRequiredElement('paper-button.print',
366 this.printButton_
.addEventListener('click', this.print_
.bind(this));
369 * @type {!HTMLElement}
373 this.editBarSpacer_
= queryRequiredElement('.edit-bar-spacer',
374 this.bottomToolbar_
);
377 * @type {!HTMLElement}
381 this.editBarMain_
= util
.createChild(this.editBarSpacer_
, 'edit-main');
384 * @type {!HTMLElement}
388 this.editBarMode_
= util
.createChild(this.container_
, 'edit-modal');
391 * @type {!HTMLElement}
395 this.editBarModeWrapper_
= util
.createChild(
396 this.editBarMode_
, 'edit-modal-wrapper dimmable');
397 this.editBarModeWrapper_
.hidden
= true;
400 * Objects supporting image display and editing.
405 this.viewport_
= new Viewport(window
);
406 this.viewport_
.addEventListener('resize', this.onViewportResize_
.bind(this));
413 this.imageView_
= new ImageView(
414 this.imageContainer_
,
419 * @type {!ImageEditor}
423 this.editor_
= new ImageEditor(
428 root
: this.container_
,
429 image
: this.imageContainer_
,
430 toolbar
: this.editBarMain_
,
431 mode
: this.editBarModeWrapper_
433 SlideMode
.EDITOR_MODES
,
434 this.displayStringFunction_
);
435 this.editor_
.addEventListener('exit-clicked', this.onExitClicked_
.bind(this));
438 * @type {!TouchHandler}
442 this.touchHandlers_
= new TouchHandler(this.imageContainer_
, this);
445 * @private {!ChromeVoxStateWatcher}
448 this.chromeVoxStateWatcher_
= new ChromeVoxStateWatcher();
449 this.chromeVoxStateWatcher_
.addEventListener('chromevox-navigation-begin',
450 this.onChromeVoxNavigationBegin_
.bind(this));
451 this.chromeVoxStateWatcher_
.addEventListener('chromevox-navigation-end',
452 this.onChromeVoxNavigationEnd_
.bind(this));
456 * List of available editor modes.
457 * @type {!Array<ImageEditor.Mode>}
460 SlideMode
.EDITOR_MODES
= [
461 new ImageEditor
.Mode
.InstantAutofix(),
462 new ImageEditor
.Mode
.Crop(),
463 new ImageEditor
.Mode
.Exposure(),
464 new ImageEditor
.Mode
.OneClick(
465 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command
.Rotate(-1)),
466 new ImageEditor
.Mode
.OneClick(
467 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command
.Rotate(1))
471 * Map of the key identifier and offset delta.
472 * @enum {!Array<number>})
475 SlideMode
.KEY_OFFSET_MAP
= {
483 * Returns editor warning message if it should be shown.
484 * @param {!Gallery.Item} item
485 * @param {string} readonlyDirName Name of read only volume. Pass empty string
486 * if volume is writable.
487 * @param {!DirectoryEntry} fallbackSaveDirectory
488 * @return {!Promise<?string>} Warning message. null if no warning message
491 SlideMode
.getEditorWarningMessage = function(
492 item
, readonlyDirName
, fallbackSaveDirectory
) {
493 var isReadOnlyVolume
= !!readonlyDirName
;
494 var isWritableFormat
= item
.isWritableFormat();
496 if (isReadOnlyVolume
&& !isWritableFormat
) {
497 return item
.getCopyName(fallbackSaveDirectory
).then(function(copyName
) {
498 return strf('GALLERY_READONLY_AND_NON_WRITABLE_FORMAT_WARNING',
499 readonlyDirName
, copyName
);
501 } else if (isReadOnlyVolume
) {
502 return Promise
.resolve(/** @type {?string} */
503 (strf('GALLERY_READONLY_WARNING', readonlyDirName
)));
504 } else if (!isWritableFormat
) {
505 var entry
= item
.getEntry();
506 return new Promise(entry
.getParent
.bind(entry
)).then(function(parentDir
) {
507 return item
.getCopyName(parentDir
);
508 }).then(function(copyName
) {
509 return strf('GALLERY_NON_WRITABLE_FORMAT_WARNING', copyName
);
512 return Promise
.resolve(/** @type {?string} */ (null));
517 * SlideMode extends cr.EventTarget.
519 SlideMode
.prototype.__proto__
= cr
.EventTarget
.prototype;
522 * Handles chromevox-navigation-begin event. While user is navigating with
523 * ChromeVox, we should not hide the tools.
526 SlideMode
.prototype.onChromeVoxNavigationBegin_ = function() {
527 this.dimmableUIController_
.setDisabled(true);
531 * Handles chromevox-navigation-end event.
534 SlideMode
.prototype.onChromeVoxNavigationEnd_ = function() {
535 this.dimmableUIController_
.setDisabled(false);
539 * Handles exit-clicked event.
542 SlideMode
.prototype.onExitClicked_ = function() {
543 if (this.isEditing())
548 * @return {string} Mode name.
550 SlideMode
.prototype.getName = function() { return 'slide'; };
553 * @return {string} Mode title.
555 SlideMode
.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
558 * @return {!Viewport} Viewport.
560 SlideMode
.prototype.getViewport = function() { return this.viewport_
; };
563 * Load items, display the selected item.
564 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
565 * @param {function()} displayCallback Called when the image is displayed.
566 * @param {function()} loadCallback Called when the image is displayed.
568 SlideMode
.prototype.enter = function(
569 zoomFromRect
, displayCallback
, loadCallback
) {
570 this.sequenceDirection_
= 0;
571 this.sequenceLength_
= 0;
573 // The latest |leave| call might have left the image animating. Remove it.
575 this.errorBanner_
.clear();
577 new Promise(function(fulfill
) {
578 // If the items are empty, just show the error message.
579 if (this.getItemCount_() === 0) {
580 this.displayedItem_
= null;
581 this.errorBanner_
.show('GALLERY_NO_IMAGES');
586 // Remember the selection if it is empty or multiple. It will be restored
587 // in |leave| if the user did not changing the selection manually.
588 var currentSelection
= this.selectionModel_
.selectedIndexes
;
589 if (currentSelection
.length
=== 1)
590 this.savedSelection_
= null;
592 this.savedSelection_
= currentSelection
;
594 // Ensure valid single selection.
595 // Note that the SlideMode object is not listening to selection change yet.
596 this.select(Math
.max(0, this.getSelectedIndex()));
598 // Show the selected item ASAP, then complete the initialization
599 // (loading the ribbon thumbnails can take some time).
600 var selectedItem
= this.getSelectedItem();
601 this.displayedItem_
= selectedItem
;
603 // Load the image of the item.
605 assert(selectedItem
),
607 this.imageView_
.createZoomEffect(zoomFromRect
) :
608 new ImageView
.Effect
.None(),
610 function(loadType
, delay
) {
613 }.bind(this)).then(function(delay
) {
614 // Turn the mode active.
616 ImageUtil
.setAttribute(this.arrowBox_
, 'active', this.getItemCount_() > 1);
617 this.ribbon_
.enable();
619 // Register handlers.
620 this.selectionModel_
.addEventListener('change', this.onSelectionBound_
);
621 this.dataModel_
.addEventListener('splice', this.onSpliceBound_
);
622 this.touchHandlers_
.enabled
= true;
624 // Wait 1000ms after the animation is done, then prefetch the next image.
625 this.requestPrefetch(1, delay
+ 1000);
627 // Call load callback.
630 }.bind(this)).catch(function(error
) {
631 console
.error(error
.stack
, error
);
637 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
638 * @param {function()} callback Called when the image is committed and
639 * the zoom-out animation has started.
641 SlideMode
.prototype.leave = function(zoomToRect
, callback
) {
642 var commitDone = function() {
644 this.stopSlideshow_();
645 ImageUtil
.setAttribute(this.arrowBox_
, 'active', false);
646 this.selectionModel_
.removeEventListener(
647 'change', this.onSelectionBound_
);
648 this.dataModel_
.removeEventListener('splice', this.onSpliceBound_
);
649 this.ribbon_
.disable();
650 this.active_
= false;
651 if (this.savedSelection_
)
652 this.selectionModel_
.selectedIndexes
= this.savedSelection_
;
653 this.unloadImage_(zoomToRect
);
657 this.viewport_
.resetView();
658 if (this.getItemCount_() === 0) {
659 this.errorBanner_
.clear();
662 this.commitItem_(commitDone
);
665 // Disable the slide-mode only buttons when leaving.
666 this.editButton_
.disabled
= true;
667 this.printButton_
.disabled
= true;
669 // Disable touch operation.
670 this.touchHandlers_
.enabled
= false;
675 * Execute an action when the editor is not busy.
677 * @param {function()} action Function to execute.
679 SlideMode
.prototype.executeWhenReady = function(action
) {
680 this.editor_
.executeWhenReady(action
);
684 * @return {boolean} True if the mode has active tools (that should not fade).
686 SlideMode
.prototype.hasActiveTool = function() {
687 return this.isEditing();
691 * @return {number} Item count.
694 SlideMode
.prototype.getItemCount_ = function() {
695 return this.dataModel_
.length
;
699 * @param {number} index Index.
700 * @return {Gallery.Item} Item.
702 SlideMode
.prototype.getItem = function(index
) {
704 /** @type {(Gallery.Item|undefined)} */ (this.dataModel_
.item(index
));
705 return item
=== undefined ? null : item
;
709 * @return {number} Selected index.
711 SlideMode
.prototype.getSelectedIndex = function() {
712 return this.selectionModel_
.selectedIndex
;
716 * @return {ImageRect} Screen rectangle of the selected image.
718 SlideMode
.prototype.getSelectedImageRect = function() {
719 if (this.getSelectedIndex() < 0)
722 return this.viewport_
.getImageBoundsOnScreen();
726 * @return {Gallery.Item} Selected item.
728 SlideMode
.prototype.getSelectedItem = function() {
729 return this.getItem(this.getSelectedIndex());
733 * Toggles the full screen mode.
736 SlideMode
.prototype.toggleFullScreen_ = function() {
737 util
.toggleFullScreen(this.context_
.appWindow
,
738 !util
.isFullScreen(this.context_
.appWindow
));
742 * Selection change handler.
744 * Commits the current image and displays the newly selected image.
747 SlideMode
.prototype.onSelection_ = function() {
748 if (this.selectionModel_
.selectedIndexes
.length
=== 0)
749 return; // Ignore temporary empty selection.
751 // Forget the saved selection if the user changed the selection manually.
752 if (!this.isSlideshowOn_())
753 this.savedSelection_
= null;
755 if (this.getSelectedItem() === this.displayedItem_
)
756 return; // Do not reselect.
758 this.commitItem_(this.loadSelectedItem_
.bind(this));
762 * Change the selection.
764 * @param {number} index New selected index.
765 * @param {number=} opt_slideHint Slide animation direction (-1|1).
767 SlideMode
.prototype.select = function(index
, opt_slideHint
) {
768 this.slideHint_
= opt_slideHint
|| null;
769 this.selectionModel_
.selectedIndex
= index
;
770 this.selectionModel_
.leadIndex
= index
;
774 * Load the selected item.
778 SlideMode
.prototype.loadSelectedItem_ = function() {
779 var slideHint
= this.slideHint_
;
780 this.slideHint_
= null;
782 if (this.getSelectedItem() === this.displayedItem_
)
783 return; // Do not reselect.
785 var index
= this.getSelectedIndex();
789 var displayedIndex
= this.dataModel_
.indexOf(this.displayedItem_
);
791 slideHint
|| (displayedIndex
> 0 ? index
- displayedIndex
: 1);
793 if (Math
.abs(step
) != 1) {
794 // Long leap, the sequence is broken, we have no good prefetch candidate.
795 this.sequenceDirection_
= 0;
796 this.sequenceLength_
= 0;
797 } else if (this.sequenceDirection_
=== step
) {
798 // Keeping going in sequence.
799 this.sequenceLength_
++;
801 // Reversed the direction. Reset the counter.
802 this.sequenceDirection_
= step
;
803 this.sequenceLength_
= 1;
806 this.displayedItem_
= this.getSelectedItem();
807 var selectedItem
= assertInstanceof(this.getSelectedItem(), Gallery
.Item
);
809 function shouldPrefetch(loadType
, step
, sequenceLength
) {
810 // Never prefetch when selecting out of sequence.
811 if (Math
.abs(step
) != 1)
814 // Always prefetch if the previous load was from cache.
815 if (loadType
=== ImageView
.LoadType
.CACHED_FULL
)
818 // Prefetch if we have been going in the same direction for long enough.
819 return sequenceLength
>= 3;
822 this.currentUniqueKey_
++;
823 var selectedUniqueKey
= this.currentUniqueKey_
;
825 // Discard, since another load has been invoked after this one.
826 if (selectedUniqueKey
!= this.currentUniqueKey_
)
831 new ImageView
.Effect
.Slide(step
, this.isSlideshowPlaying_()),
832 function() {} /* no displayCallback */,
833 function(loadType
, delay
) {
834 // Discard, since another load has been invoked after this one.
835 if (selectedUniqueKey
!= this.currentUniqueKey_
)
837 if (shouldPrefetch(loadType
, step
, this.sequenceLength_
))
838 this.requestPrefetch(step
, delay
);
839 if (this.isSlideshowPlaying_())
840 this.scheduleNextSlide_();
845 * Unload the current image.
847 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
850 SlideMode
.prototype.unloadImage_ = function(opt_zoomToRect
) {
851 this.imageView_
.unload(opt_zoomToRect
);
855 * Data model 'splice' event handler.
856 * @param {!Event} event Event.
860 SlideMode
.prototype.onSplice_ = function(event
) {
861 ImageUtil
.setAttribute(this.arrowBox_
, 'active', this.getItemCount_() > 1);
863 // Splice invalidates saved indices, drop the saved selection.
864 this.savedSelection_
= null;
866 if (event
.removed
.length
!= 1)
869 // Delay the selection to let the ribbon splice handler work first.
870 setTimeout(function() {
871 if (this.dataModel_
.length
=== 0) {
872 // No items left. Unload the image, disable edit and print button, and
874 this.commitItem_(function() {
876 this.printButton_
.disabled
= true;
877 this.editButton_
.disabled
= true;
878 this.errorBanner_
.show('GALLERY_NO_IMAGES');
883 var displayedItemNotRemvoed
= event
.removed
.every(function(item
) {
884 return item
!== this.displayedItem_
;
886 if (!displayedItemNotRemvoed
) {
887 // There is the next item, select it. Otherwise, select the last item.
888 var nextIndex
= Math
.min(event
.index
, this.dataModel_
.length
- 1);
889 // To force to dispatch a selection change event, clear selection before.
890 this.selectionModel_
.clear();
891 this.select(nextIndex
);
897 * @param {number} direction -1 for left, 1 for right.
898 * @return {number} Next index in the given direction, with wrapping.
901 SlideMode
.prototype.getNextSelectedIndex_ = function(direction
) {
902 function advance(index
, limit
) {
903 index
+= (direction
> 0 ? 1 : -1);
911 // If the saved selection is multiple the Slideshow should cycle through
912 // the saved selection.
913 if (this.isSlideshowOn_() &&
914 this.savedSelection_
&& this.savedSelection_
.length
> 1) {
915 var pos
= advance(this.savedSelection_
.indexOf(this.getSelectedIndex()),
916 this.savedSelection_
.length
);
917 return this.savedSelection_
[pos
];
919 return advance(this.getSelectedIndex(), this.getItemCount_());
924 * Advance the selection based on the pressed key ID.
925 * @param {string} keyID Key identifier.
927 SlideMode
.prototype.advanceWithKeyboard = function(keyID
) {
928 var prev
= (keyID
=== 'Up' ||
930 keyID
=== 'MediaPreviousTrack');
931 this.advanceManually(prev
? -1 : 1);
935 * Advance the selection as a result of a user action (as opposed to an
936 * automatic change in the slideshow mode).
937 * @param {number} direction -1 for left, 1 for right.
939 SlideMode
.prototype.advanceManually = function(direction
) {
940 if (this.isSlideshowPlaying_())
941 this.pauseSlideshow_();
942 cr
.dispatchSimpleEvent(this, 'useraction');
943 this.selectNext(direction
);
947 * Select the next item.
948 * @param {number} direction -1 for left, 1 for right.
950 SlideMode
.prototype.selectNext = function(direction
) {
951 this.select(this.getNextSelectedIndex_(direction
), direction
);
955 * Select the first item.
957 SlideMode
.prototype.selectFirst = function() {
962 * Select the last item.
964 SlideMode
.prototype.selectLast = function() {
965 this.select(this.getItemCount_() - 1);
971 * Load and display an item.
973 * @param {!Gallery.Item} item Item.
974 * @param {!ImageView.Effect} effect Transition effect object.
975 * @param {function()} displayCallback Called when the image is displayed
976 * (which can happen before the image load due to caching).
977 * @param {function(number, number)} loadCallback Called when the image is fully
981 SlideMode
.prototype.loadItem_ = function(
982 item
, effect
, displayCallback
, loadCallback
) {
983 this.showSpinner_(true);
985 var loadDone
= this.itemLoaded_
.bind(this, item
, loadCallback
);
987 var displayDone = function() {
988 cr
.dispatchSimpleEvent(this, 'image-displayed');
992 this.editor_
.openSession(
995 this.saveCurrentImage_
.bind(this, item
),
1001 * A callback function when the editor opens a editing session for an image.
1002 * @param {!Gallery.Item} item Gallery item.
1003 * @param {function(number, number)} loadCallback Called when the image is fully
1005 * @param {number} loadType Load type.
1006 * @param {number} delay Delay.
1007 * @param {*=} opt_error Error.
1010 SlideMode
.prototype.itemLoaded_ = function(
1011 item
, loadCallback
, loadType
, delay
, opt_error
) {
1012 var entry
= item
.getEntry();
1014 this.showSpinner_(false);
1015 if (loadType
=== ImageView
.LoadType
.ERROR
) {
1016 // if we have a specific error, then display it
1018 this.errorBanner_
.show(/** @type {string} */ (opt_error
));
1020 // otherwise try to infer general error
1021 this.errorBanner_
.show('GALLERY_IMAGE_ERROR');
1023 } else if (loadType
=== ImageView
.LoadType
.OFFLINE
) {
1024 this.errorBanner_
.show('GALLERY_IMAGE_OFFLINE');
1027 ImageUtil
.metrics
.recordUserAction(ImageUtil
.getMetricName('View'));
1029 var toMillions = function(number
) {
1030 return Math
.round(number
/ (1000 * 1000));
1033 var metadata
= item
.getMetadataItem();
1035 ImageUtil
.metrics
.recordSmallCount(ImageUtil
.getMetricName('Size.MB'),
1036 toMillions(metadata
.size
));
1039 var canvas
= this.imageView_
.getCanvas();
1040 ImageUtil
.metrics
.recordSmallCount(ImageUtil
.getMetricName('Size.MPix'),
1041 toMillions(canvas
.width
* canvas
.height
));
1043 var extIndex
= entry
.name
.lastIndexOf('.');
1044 var ext
= extIndex
< 0 ? '' :
1045 entry
.name
.substr(extIndex
+ 1).toLowerCase();
1046 if (ext
=== 'jpeg') ext
= 'jpg';
1047 ImageUtil
.metrics
.recordEnum(
1048 ImageUtil
.getMetricName('FileType'), ext
, ImageUtil
.FILE_TYPES
);
1050 // Enable or disable buttons for editing and printing.
1052 this.editButton_
.disabled
= true;
1053 this.printButton_
.disabled
= true;
1055 this.editButton_
.disabled
= false;
1056 this.printButton_
.disabled
= false;
1059 // Saved label is hidden by default.
1060 this.savedLabel_
.hidden
= true;
1062 // Disable overwrite original checkbox until settings is loaded.
1063 this.overwriteOriginalCheckbox_
.disabled
= true;
1064 this.overwriteOriginalCheckbox_
.checked
= false;
1067 keys
[SlideMode
.OVERWRITE_ORIGINAL_KEY
] = true;
1068 chrome
.storage
.local
.get(keys
,
1070 // Users can overwrite original file only if loaded image is original
1072 if (item
.isOriginal() &&
1073 item
.isWritableFile(this.volumeManager_
)) {
1074 this.overwriteOriginalCheckbox_
.disabled
= false;
1075 this.overwriteOriginalCheckbox_
.checked
=
1076 values
[SlideMode
.OVERWRITE_ORIGINAL_KEY
];
1080 loadCallback(loadType
, delay
);
1084 * Commit changes to the current item and reset all messages/indicators.
1086 * @param {function()} callback Callback.
1089 SlideMode
.prototype.commitItem_ = function(callback
) {
1090 this.showSpinner_(false);
1091 this.errorBanner_
.clear();
1092 this.editor_
.getPrompt().hide();
1093 this.editor_
.closeSession(callback
);
1097 * Request a prefetch for the next image.
1099 * @param {number} direction -1 or 1.
1100 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1101 * loading from disrupting the animation that might be still in progress.
1103 SlideMode
.prototype.requestPrefetch = function(direction
, delay
) {
1104 if (this.getItemCount_() <= 1) return;
1106 var index
= this.getNextSelectedIndex_(direction
);
1107 this.imageView_
.prefetch(assert(this.getItem(index
)), delay
);
1113 * Click handler for the entire document.
1114 * @param {!Event} event Mouse click event.
1117 SlideMode
.prototype.onDocumentClick_ = function(event
) {
1118 // Events created in fakeMouseClick in test util don't pass this test.
1119 if (!window
.IN_TEST
)
1120 event
= assertInstanceof(event
, MouseEvent
);
1122 var targetElement
= assertInstanceof(event
.target
, HTMLElement
);
1123 // Close the bubble if clicked outside of it and if it is visible.
1124 if (!this.bubble_
.contains(targetElement
) &&
1125 !this.editButton_
.contains(targetElement
) &&
1126 !this.arrowLeft_
.contains(targetElement
) &&
1127 !this.arrowRight_
.contains(targetElement
) &&
1128 !this.bubble_
.hidden
) {
1129 this.bubble_
.hidden
= true;
1136 * @param {!Event} event Event.
1137 * @return {boolean} True if handled.
1139 SlideMode
.prototype.onKeyDown = function(event
) {
1140 var keyID
= util
.getKeyModifiers(event
) + event
.keyIdentifier
;
1142 if (this.isSlideshowOn_()) {
1144 case 'U+001B': // Escape
1146 this.stopSlideshow_(event
);
1149 case 'U+0020': // Space pauses/resumes the slideshow.
1150 case 'MediaPlayPause':
1151 this.toggleSlideshowPause_();
1158 case 'MediaNextTrack':
1159 case 'MediaPreviousTrack':
1160 this.advanceWithKeyboard(keyID
);
1163 return true; // Consume all keystrokes in the slideshow mode.
1166 // Handles shortcut keys common for both modes (editing and not-editing).
1168 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
1169 if (!this.printButton_
.disabled
)
1173 case 'U+0045': // 'e' toggles the editor.
1174 if (!this.editButton_
.disabled
)
1175 this.toggleEditor(event
);
1179 // Handles shortcurt keys for editing mode.
1180 if (this.isEditing()) {
1181 if (this.editor_
.onKeyDown(event
))
1184 if (keyID
=== 'U+001B') { // Escape
1185 this.toggleEditor(event
);
1192 // Handles shortcut keys for not-editing mode.
1194 case 'U+001B': // Escape
1195 if (this.viewport_
.isZoomed()) {
1196 this.viewport_
.resetView();
1197 this.touchHandlers_
.stopOperation();
1198 this.imageView_
.applyViewportChange();
1215 if (this.viewport_
.isZoomed()) {
1216 var delta
= SlideMode
.KEY_OFFSET_MAP
[keyID
];
1217 this.viewport_
.setOffset(
1218 ~~(this.viewport_
.getOffsetX() +
1219 delta
[0] * this.viewport_
.getZoom()),
1220 ~~(this.viewport_
.getOffsetY() +
1221 delta
[1] * this.viewport_
.getZoom()));
1222 this.touchHandlers_
.stopOperation();
1223 this.imageView_
.applyViewportChange();
1225 this.advanceWithKeyboard(keyID
);
1229 case 'MediaNextTrack':
1230 case 'MediaPreviousTrack':
1231 this.advanceWithKeyboard(keyID
);
1234 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
1235 this.viewport_
.zoomIn();
1236 this.touchHandlers_
.stopOperation();
1237 this.imageView_
.applyViewportChange();
1240 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
1241 this.viewport_
.zoomOut();
1242 this.touchHandlers_
.stopOperation();
1243 this.imageView_
.applyViewportChange();
1246 case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1247 this.viewport_
.setZoom(1.0);
1248 this.touchHandlers_
.stopOperation();
1249 this.imageView_
.applyViewportChange();
1260 SlideMode
.prototype.onResize_ = function() {
1261 this.touchHandlers_
.stopOperation();
1265 * Handles resize event of viewport.
1268 SlideMode
.prototype.onViewportResize_ = function() {
1269 // This method must be called after the resize of viewport.
1270 this.editor_
.getBuffer().draw();
1274 * Update thumbnails.
1276 SlideMode
.prototype.updateThumbnails = function() {
1277 this.ribbon_
.reset();
1279 this.ribbon_
.redraw();
1285 * Save the current image to a file.
1287 * @param {!Gallery.Item} item Item to save the image.
1288 * @param {function()} callback Callback.
1291 SlideMode
.prototype.saveCurrentImage_ = function(item
, callback
) {
1292 this.showSpinner_(true);
1294 var savedPromise
= this.dataModel_
.saveItem(
1295 this.volumeManager_
,
1297 this.imageView_
.getCanvas(),
1298 this.overwriteOriginalCheckbox_
.checked
);
1300 savedPromise
.then(function() {
1301 this.showSpinner_(false);
1302 this.flashSavedLabel_();
1304 // Record UMA for the first edit.
1305 if (this.imageView_
.getContentRevision() === 1)
1306 ImageUtil
.metrics
.recordUserAction(ImageUtil
.getMetricName('Edit'));
1308 // Users can change overwrite original setting only if there is no undo
1309 // stack and item is original and writable.
1310 var ableToChangeOverwriteOriginalSetting
= !this.editor_
.canUndo() &&
1311 item
.isOriginal() && item
.isWritableFile(this.volumeManager_
);
1312 this.overwriteOriginalCheckbox_
.disabled
=
1313 !ableToChangeOverwriteOriginalSetting
;
1316 }.bind(this)).catch(function(error
) {
1317 console
.error(error
.stack
|| error
);
1319 this.showSpinner_(false);
1320 this.errorBanner_
.show('GALLERY_SAVE_FAILED');
1327 * Flash 'Saved' label briefly to indicate that the image has been saved.
1330 SlideMode
.prototype.flashSavedLabel_ = function() {
1331 this.savedLabel_
.hidden
= false;
1332 var setLabelHighlighted
=
1333 ImageUtil
.setAttribute
.bind(null, this.savedLabel_
, 'highlighted');
1334 setTimeout(setLabelHighlighted
.bind(null, true), 0);
1335 setTimeout(setLabelHighlighted
.bind(null, false), 300);
1339 * Local storage key for the number of times that
1340 * the overwrite info bubble has been displayed.
1343 SlideMode
.OVERWRITE_BUBBLE_KEY
= 'gallery-overwrite-bubble';
1346 * Local storage key for overwrite original checkbox value.
1349 SlideMode
.OVERWRITE_ORIGINAL_KEY
= 'gallery-overwrite-original';
1352 * Max number that the overwrite info bubble is shown.
1355 SlideMode
.OVERWRITE_BUBBLE_MAX_TIMES
= 5;
1358 * Handles change event of overwrite original checkbox.
1360 SlideMode
.prototype.onOverwriteOriginalCheckboxChanged_ = function() {
1362 items
[SlideMode
.OVERWRITE_ORIGINAL_KEY
] =
1363 this.overwriteOriginalCheckbox_
.checked
;
1364 chrome
.storage
.local
.set(items
);
1368 * Overwrite info bubble close handler.
1371 SlideMode
.prototype.onCloseBubble_ = function() {
1372 this.bubble_
.hidden
= true;
1373 this.setOverwriteBubbleCount_(SlideMode
.OVERWRITE_BUBBLE_MAX_TIMES
);
1379 * Slideshow interval in ms.
1381 SlideMode
.SLIDESHOW_INTERVAL
= 5000;
1384 * First slideshow interval in ms. It should be shorter so that the user
1385 * is not guessing whether the button worked.
1387 SlideMode
.SLIDESHOW_INTERVAL_FIRST
= 1000;
1390 * Empirically determined duration of the fullscreen toggle animation.
1392 SlideMode
.FULLSCREEN_TOGGLE_DELAY
= 500;
1395 * @return {boolean} True if the slideshow is on.
1398 SlideMode
.prototype.isSlideshowOn_ = function() {
1399 return this.container_
.hasAttribute('slideshow');
1403 * Starts the slideshow.
1404 * @param {number=} opt_interval First interval in ms.
1405 * @param {Event=} opt_event Event.
1407 SlideMode
.prototype.startSlideshow = function(opt_interval
, opt_event
) {
1409 this.viewport_
.resetView();
1410 this.imageView_
.applyViewportChange();
1412 // Disable touch operation.
1413 this.touchHandlers_
.enabled
= false;
1415 // Set the attribute early to prevent the toolbar from flashing when
1416 // the slideshow is being started from the mosaic view.
1417 this.container_
.setAttribute('slideshow', 'playing');
1420 this.stopEditing_();
1422 // We are in the Mosaic mode. Toggle the mode but remember to return.
1423 this.leaveAfterSlideshow_
= true;
1425 // Wait until the zoom animation from the mosaic mode is done.
1426 var startSlideshowAfterTransition = function() {
1427 setTimeout(function() {
1428 this.startSlideshow
.call(this, SlideMode
.SLIDESHOW_INTERVAL
, opt_event
);
1429 }.bind(this), ImageView
.MODE_TRANSITION_DURATION
);
1431 this.toggleMode_(startSlideshowAfterTransition
);
1435 if (opt_event
) // Caused by user action, notify the Gallery.
1436 cr
.dispatchSimpleEvent(this, 'useraction');
1438 this.fullscreenBeforeSlideshow_
= util
.isFullScreen(this.context_
.appWindow
);
1439 if (!this.fullscreenBeforeSlideshow_
) {
1440 this.toggleFullScreen_();
1441 opt_interval
= (opt_interval
|| SlideMode
.SLIDESHOW_INTERVAL
) +
1442 SlideMode
.FULLSCREEN_TOGGLE_DELAY
;
1445 // This is a workaround. Mouseout event is not dispatched when window becomes
1446 // fullscreen and cursor gets out of the element
1447 // TODO(yawano): Find better implementation.
1448 this.dimmableUIController_
.setCursorOutOfTools();
1450 this.resumeSlideshow_(opt_interval
);
1454 * Stops the slideshow.
1455 * @param {Event=} opt_event Event.
1458 SlideMode
.prototype.stopSlideshow_ = function(opt_event
) {
1459 if (!this.isSlideshowOn_())
1462 if (opt_event
) // Caused by user action, notify the Gallery.
1463 cr
.dispatchSimpleEvent(this, 'useraction');
1465 this.pauseSlideshow_();
1466 this.container_
.removeAttribute('slideshow');
1468 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1469 var fullscreen
= util
.isFullScreen(this.context_
.appWindow
);
1470 var toggleModeDelay
= 0;
1471 if (!this.fullscreenBeforeSlideshow_
&& fullscreen
) {
1472 this.toggleFullScreen_();
1473 toggleModeDelay
= SlideMode
.FULLSCREEN_TOGGLE_DELAY
;
1475 if (this.leaveAfterSlideshow_
) {
1476 this.leaveAfterSlideshow_
= false;
1477 setTimeout(this.toggleMode_
.bind(this), toggleModeDelay
);
1480 // Re-enable touch operation.
1481 this.touchHandlers_
.enabled
= true;
1485 * @return {boolean} True if the slideshow is playing (not paused).
1488 SlideMode
.prototype.isSlideshowPlaying_ = function() {
1489 return this.container_
.getAttribute('slideshow') === 'playing';
1493 * Pauses/resumes the slideshow.
1496 SlideMode
.prototype.toggleSlideshowPause_ = function() {
1497 cr
.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1498 if (this.isSlideshowPlaying_()) {
1499 this.pauseSlideshow_();
1501 this.resumeSlideshow_(SlideMode
.SLIDESHOW_INTERVAL_FIRST
);
1506 * @param {number=} opt_interval Slideshow interval in ms.
1509 SlideMode
.prototype.scheduleNextSlide_ = function(opt_interval
) {
1510 console
.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1512 if (this.slideShowTimeout_
)
1513 clearTimeout(this.slideShowTimeout_
);
1515 this.slideShowTimeout_
= setTimeout(function() {
1516 this.slideShowTimeout_
= null;
1518 }.bind(this), opt_interval
|| SlideMode
.SLIDESHOW_INTERVAL
);
1522 * Resumes the slideshow.
1523 * @param {number=} opt_interval Slideshow interval in ms.
1526 SlideMode
.prototype.resumeSlideshow_ = function(opt_interval
) {
1527 this.container_
.setAttribute('slideshow', 'playing');
1528 this.scheduleNextSlide_(opt_interval
);
1532 * Pauses the slideshow.
1535 SlideMode
.prototype.pauseSlideshow_ = function() {
1536 this.container_
.setAttribute('slideshow', 'paused');
1537 if (this.slideShowTimeout_
) {
1538 clearTimeout(this.slideShowTimeout_
);
1539 this.slideShowTimeout_
= null;
1544 * @return {boolean} True if the editor is active.
1546 SlideMode
.prototype.isEditing = function() {
1547 return this.container_
.hasAttribute('editing');
1554 SlideMode
.prototype.stopEditing_ = function() {
1555 if (this.isEditing())
1556 this.toggleEditor();
1560 * Activate/deactivate editor.
1561 * @param {Event=} opt_event Event.
1563 SlideMode
.prototype.toggleEditor = function(opt_event
) {
1564 if (opt_event
) // Caused by user action, notify the Gallery.
1565 cr
.dispatchSimpleEvent(this, 'useraction');
1567 if (!this.active_
) {
1568 this.toggleMode_(this.toggleEditor
.bind(this));
1572 this.stopSlideshow_();
1574 ImageUtil
.setAttribute(this.container_
, 'editing', !this.isEditing());
1575 this.editButtonToggleRipple_
.activated
= this.isEditing();
1577 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1579 this.viewport_
.resetView();
1581 // Scale the screen so that it doesn't overlap the toolbars.
1582 this.viewport_
.setScreenTop(ImageEditor
.Toolbar
.HEIGHT
);
1583 this.viewport_
.setScreenBottom(ImageEditor
.Toolbar
.HEIGHT
);
1585 this.imageView_
.applyViewportChange();
1587 this.touchHandlers_
.enabled
= false;
1588 this.dimmableUIController_
.setDisabled(true);
1590 // Show editor warning message.
1591 SlideMode
.getEditorWarningMessage(
1592 assert(this.getItem(this.getSelectedIndex())),
1593 this.context_
.readonlyDirName
,
1594 assert(this.dataModel_
.fallbackSaveDirectory
)
1595 ).then(function(warningMessage
) {
1596 if (!warningMessage
)
1599 this.filesToast_
.show(warningMessage
);
1602 // Show overwrite original bubble if it hasn't been shown for max times.
1603 this.getOverwriteBubbleCount_().then(function(count
) {
1604 if (count
>= SlideMode
.OVERWRITE_BUBBLE_MAX_TIMES
)
1607 this.setOverwriteBubbleCount_(count
+ 1);
1608 this.bubble_
.hidden
= false;
1611 this.editor_
.getPrompt().hide();
1612 this.editor_
.leaveModeGently();
1614 this.viewport_
.setScreenTop(0);
1615 this.viewport_
.setScreenBottom(0);
1616 this.imageView_
.applyViewportChange();
1618 this.bubble_
.hidden
= true;
1620 this.touchHandlers_
.enabled
= true;
1621 this.dimmableUIController_
.setDisabled(false);
1626 * Gets count of overwrite bubble.
1627 * @return {!Promise<number>}
1630 SlideMode
.prototype.getOverwriteBubbleCount_ = function() {
1631 return new Promise(function(resolve
, reject
) {
1633 requests
[SlideMode
.OVERWRITE_BUBBLE_KEY
] = 0;
1635 chrome
.storage
.local
.get(requests
, function(results
) {
1636 if (!!chrome
.runtime
.lastError
) {
1637 reject(chrome
.runtime
.lastError
);
1641 resolve(results
[SlideMode
.OVERWRITE_BUBBLE_KEY
]);
1647 * Sets count of overwrite bubble.
1648 * @param {number} value
1651 SlideMode
.prototype.setOverwriteBubbleCount_ = function(value
) {
1653 requests
[SlideMode
.OVERWRITE_BUBBLE_KEY
] = value
;
1654 chrome
.storage
.local
.set(requests
);
1658 * Prints the current item.
1661 SlideMode
.prototype.print_ = function() {
1662 cr
.dispatchSimpleEvent(this, 'useraction');
1667 * Shows/hides the busy spinner.
1669 * @param {boolean} on True if show, false if hide.
1672 SlideMode
.prototype.showSpinner_ = function(on
) {
1673 if (this.spinnerTimer_
) {
1674 clearTimeout(this.spinnerTimer_
);
1675 this.spinnerTimer_
= null;
1679 this.spinnerTimer_
= setTimeout(function() {
1680 this.spinnerTimer_
= null;
1681 ImageUtil
.setAttribute(this.container_
, 'spinner', true);
1682 }.bind(this), 1000);
1684 ImageUtil
.setAttribute(this.container_
, 'spinner', false);
1689 * Apply the change of viewport.
1691 SlideMode
.prototype.applyViewportChange = function() {
1692 this.imageView_
.applyViewportChange();
1696 * Touch handlers of the slide mode.
1697 * @param {!Element} targetElement Event source.
1698 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1702 function TouchHandler(targetElement
, slideMode
) {
1709 this.targetElement_
= targetElement
;
1712 * Target of touch operations.
1713 * @type {!SlideMode}
1717 this.slideMode_
= slideMode
;
1720 * Flag to enable/disable touch operation.
1724 this.enabled_
= true;
1727 * Whether it is in a touch operation that is started from targetElement or
1732 this.touchStarted_
= false;
1735 * The swipe action that should happen only once in an operation is already
1743 * Event on beginning of the current gesture.
1744 * The variable is updated when the number of touch finger changed.
1745 * @type {TouchEvent}
1748 this.gestureStartEvent_
= null;
1751 * Rotation value on beginning of the current gesture.
1755 this.gestureStartRotation_
= 0;
1759 * @type {TouchEvent}
1762 this.lastEvent_
= null;
1765 * Zoom value just after last touch event.
1769 this.lastZoom_
= 1.0;
1771 targetElement
.addEventListener('touchstart', this.onTouchStart_
.bind(this));
1772 var onTouchEventBound
= this.onTouchEvent_
.bind(this);
1773 targetElement
.ownerDocument
.addEventListener('touchmove', onTouchEventBound
);
1774 targetElement
.ownerDocument
.addEventListener('touchend', onTouchEventBound
);
1776 targetElement
.addEventListener('mousewheel', this.onMouseWheel_
.bind(this));
1780 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1781 * horizontally it's considered as a swipe gesture (change the current image).
1785 TouchHandler
.SWIPE_THRESHOLD
= 100;
1788 * Rotation threshold in degrees.
1792 TouchHandler
.ROTATION_THRESHOLD
= 25;
1795 * Obtains distance between fingers.
1796 * @param {!TouchEvent} event Touch event. It should include more than two
1798 * @return {number} Distance between touch[0] and touch[1].
1800 TouchHandler
.getDistance = function(event
) {
1801 var touch1
= event
.touches
[0];
1802 var touch2
= event
.touches
[1];
1803 var dx
= touch1
.clientX
- touch2
.clientX
;
1804 var dy
= touch1
.clientY
- touch2
.clientY
;
1805 return Math
.sqrt(dx
* dx
+ dy
* dy
);
1809 * Obtains the degrees of the pinch twist angle.
1810 * @param {!TouchEvent} event1 Start touch event. It should include more than
1812 * @param {!TouchEvent} event2 Current touch event. It should include more than
1814 * @return {number} Degrees of the pinch twist angle.
1816 TouchHandler
.getTwistAngle = function(event1
, event2
) {
1817 var dx1
= event1
.touches
[1].clientX
- event1
.touches
[0].clientX
;
1818 var dy1
= event1
.touches
[1].clientY
- event1
.touches
[0].clientY
;
1819 var dx2
= event2
.touches
[1].clientX
- event2
.touches
[0].clientX
;
1820 var dy2
= event2
.touches
[1].clientY
- event2
.touches
[0].clientY
;
1821 var innerProduct
= dx1
* dx2
+ dy1
* dy2
; // |v1| * |v2| * cos(t) = x / r
1822 var outerProduct
= dx1
* dy2
- dy1
* dx2
; // |v1| * |v2| * sin(t) = y / r
1823 return Math
.atan2(outerProduct
, innerProduct
) * 180 / Math
.PI
; // atan(y / x)
1826 TouchHandler
.prototype = /** @struct */ {
1828 * @param {boolean} flag New value.
1831 this.enabled_
= flag
;
1833 this.stopOperation();
1838 * Stops the current touch operation.
1840 TouchHandler
.prototype.stopOperation = function() {
1841 this.touchStarted_
= false;
1843 this.gestureStartEvent_
= null;
1844 this.lastEvent_
= null;
1845 this.lastZoom_
= 1.0;
1849 * Handles touch start events.
1850 * @param {!Event} event Touch event.
1853 TouchHandler
.prototype.onTouchStart_ = function(event
) {
1854 event
= assertInstanceof(event
, TouchEvent
);
1855 if (this.enabled_
&& event
.touches
.length
=== 1)
1856 this.touchStarted_
= true;
1860 * Handles touch move and touch end events.
1861 * @param {!Event} event Touch event.
1864 TouchHandler
.prototype.onTouchEvent_ = function(event
) {
1865 event
= assertInstanceof(event
, TouchEvent
);
1866 // Check if the current touch operation started from the target element or
1868 if (!this.touchStarted_
)
1871 // Check if the current touch operation ends with the event.
1872 if (event
.touches
.length
=== 0) {
1873 this.stopOperation();
1877 // Check if a new gesture started or not.
1878 var viewport
= this.slideMode_
.getViewport();
1879 if (!this.lastEvent_
||
1880 this.lastEvent_
.touches
.length
!== event
.touches
.length
) {
1881 if (event
.touches
.length
=== 2 ||
1882 event
.touches
.length
=== 1) {
1883 this.gestureStartEvent_
= event
;
1884 this.gestureStartRotation_
= viewport
.getRotation();
1885 this.lastEvent_
= event
;
1886 this.lastZoom_
= viewport
.getZoom();
1888 this.gestureStartEvent_
= null;
1889 this.gestureStartRotation_
= 0;
1890 this.lastEvent_
= null;
1891 this.lastZoom_
= 1.0;
1896 // Handle the gesture movement.
1897 switch (event
.touches
.length
) {
1899 if (viewport
.isZoomed()) {
1900 // Scrolling an image by swipe.
1901 var dx
= event
.touches
[0].screenX
- this.lastEvent_
.touches
[0].screenX
;
1902 var dy
= event
.touches
[0].screenY
- this.lastEvent_
.touches
[0].screenY
;
1904 viewport
.getOffsetX() + dx
, viewport
.getOffsetY() + dy
);
1905 this.slideMode_
.applyViewportChange();
1907 // Traversing images by swipe.
1911 event
.touches
[0].clientX
-
1912 this.gestureStartEvent_
.touches
[0].clientX
;
1913 if (dx
> TouchHandler
.SWIPE_THRESHOLD
) {
1914 this.slideMode_
.advanceManually(-1);
1916 } else if (dx
< -TouchHandler
.SWIPE_THRESHOLD
) {
1917 this.slideMode_
.advanceManually(1);
1925 var distance1
= TouchHandler
.getDistance(this.lastEvent_
);
1926 var distance2
= TouchHandler
.getDistance(event
);
1927 if (distance1
=== 0)
1929 var zoom
= distance2
/ distance1
* this.lastZoom_
;
1930 viewport
.setZoom(zoom
);
1933 assert(this.gestureStartEvent_
);
1934 var angle
= TouchHandler
.getTwistAngle(this.gestureStartEvent_
, event
);
1935 if (angle
> TouchHandler
.ROTATION_THRESHOLD
)
1936 viewport
.setRotation(this.gestureStartRotation_
+ 1);
1937 else if (angle
< -TouchHandler
.ROTATION_THRESHOLD
)
1938 viewport
.setRotation(this.gestureStartRotation_
- 1);
1940 viewport
.setRotation(this.gestureStartRotation_
);
1941 this.slideMode_
.applyViewportChange();
1945 // Update the last event.
1946 this.lastEvent_
= event
;
1947 this.lastZoom_
= viewport
.getZoom();
1951 * Handles mouse wheel events.
1952 * @param {!Event} event Wheel event.
1955 TouchHandler
.prototype.onMouseWheel_ = function(event
) {
1956 var event
= assertInstanceof(event
, MouseEvent
);
1957 var viewport
= this.slideMode_
.getViewport();
1958 if (!this.enabled_
|| !viewport
.isZoomed())
1960 this.stopOperation();
1962 viewport
.getOffsetX() + event
.wheelDeltaX
,
1963 viewport
.getOffsetY() + event
.wheelDeltaY
);
1964 this.slideMode_
.applyViewportChange();