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 this.document_, '.content'), 'image-container');
227 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
230 * Overwrite options and info bubble.
231 * @type {!HTMLElement}
235 this.options_ = queryRequiredElement(this.bottomToolbar_, '.options');
238 * @type {!HTMLElement}
242 this.savedLabel_ = queryRequiredElement(this.options_, '.saved');
245 * @private {!PaperCheckboxElement}
248 this.overwriteOriginalCheckbox_ = /** @type {!PaperCheckboxElement} */
249 (queryRequiredElement(this.options_, '.overwrite-original'));
250 this.overwriteOriginalCheckbox_.addEventListener('change',
251 this.onOverwriteOriginalCheckboxChanged_.bind(this));
254 * @private {!FilesToast}
257 this.filesToast_ = /** @type {!FilesToast} */
258 (queryRequiredElement(document, 'files-toast'));
261 * @private {!HTMLElement}
264 this.bubble_ = queryRequiredElement(this.bottomToolbar_, '.bubble');
266 var bubbleContent = queryRequiredElement(this.bubble_, '.content');
267 // GALLERY_OVERWRITE_BUBBLE contains <br> tag inside message.
268 bubbleContent.innerHTML = strf('GALLERY_OVERWRITE_BUBBLE');
270 var bubbleClose = queryRequiredElement(this.bubble_, '.close-x');
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(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(this.topToolbar_,
327 '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(this.topToolbar_, 'button.edit');
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(
365 this.topToolbar_, 'paper-button.print');
366 this.printButton_.addEventListener('click', this.print_.bind(this));
369 * @type {!HTMLElement}
373 this.editBarSpacer_ = queryRequiredElement(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 * SlideMode extends cr.EventTarget.
485 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
488 * Handles chromevox-navigation-begin event. While user is navigating with
489 * ChromeVox, we should not hide the tools.
492 SlideMode.prototype.onChromeVoxNavigationBegin_ = function() {
493 this.dimmableUIController_.setDisabled(true);
497 * Handles chromevox-navigation-end event.
500 SlideMode.prototype.onChromeVoxNavigationEnd_ = function() {
501 this.dimmableUIController_.setDisabled(false);
505 * Handles exit-clicked event.
508 SlideMode.prototype.onExitClicked_ = function() {
509 if (this.isEditing())
514 * @return {string} Mode name.
516 SlideMode.prototype.getName = function() { return 'slide'; };
519 * @return {string} Mode title.
521 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
524 * @return {!Viewport} Viewport.
526 SlideMode.prototype.getViewport = function() { return this.viewport_; };
529 * Load items, display the selected item.
530 * @param {ImageRect} zoomFromRect Rectangle for zoom effect.
531 * @param {function()} displayCallback Called when the image is displayed.
532 * @param {function()} loadCallback Called when the image is displayed.
534 SlideMode.prototype.enter = function(
535 zoomFromRect, displayCallback, loadCallback) {
536 this.sequenceDirection_ = 0;
537 this.sequenceLength_ = 0;
539 // The latest |leave| call might have left the image animating. Remove it.
541 this.errorBanner_.clear();
543 new Promise(function(fulfill) {
544 // If the items are empty, just show the error message.
545 if (this.getItemCount_() === 0) {
546 this.displayedItem_ = null;
547 this.errorBanner_.show('GALLERY_NO_IMAGES');
552 // Remember the selection if it is empty or multiple. It will be restored
553 // in |leave| if the user did not changing the selection manually.
554 var currentSelection = this.selectionModel_.selectedIndexes;
555 if (currentSelection.length === 1)
556 this.savedSelection_ = null;
558 this.savedSelection_ = currentSelection;
560 // Ensure valid single selection.
561 // Note that the SlideMode object is not listening to selection change yet.
562 this.select(Math.max(0, this.getSelectedIndex()));
564 // Show the selected item ASAP, then complete the initialization
565 // (loading the ribbon thumbnails can take some time).
566 var selectedItem = this.getSelectedItem();
567 this.displayedItem_ = selectedItem;
569 // Load the image of the item.
571 assert(selectedItem),
573 this.imageView_.createZoomEffect(zoomFromRect) :
574 new ImageView.Effect.None(),
576 function(loadType, delay) {
579 }.bind(this)).then(function(delay) {
580 // Turn the mode active.
582 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
583 this.ribbon_.enable();
585 // Register handlers.
586 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
587 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
588 this.touchHandlers_.enabled = true;
590 // Wait 1000ms after the animation is done, then prefetch the next image.
591 this.requestPrefetch(1, delay + 1000);
593 // Call load callback.
596 }.bind(this)).catch(function(error) {
597 console.error(error.stack, error);
603 * @param {ImageRect} zoomToRect Rectangle for zoom effect.
604 * @param {function()} callback Called when the image is committed and
605 * the zoom-out animation has started.
607 SlideMode.prototype.leave = function(zoomToRect, callback) {
608 var commitDone = function() {
610 this.stopSlideshow_();
611 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
612 this.selectionModel_.removeEventListener(
613 'change', this.onSelectionBound_);
614 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
615 this.ribbon_.disable();
616 this.active_ = false;
617 if (this.savedSelection_)
618 this.selectionModel_.selectedIndexes = this.savedSelection_;
619 this.unloadImage_(zoomToRect);
623 this.viewport_.resetView();
624 if (this.getItemCount_() === 0) {
625 this.errorBanner_.clear();
628 this.commitItem_(commitDone);
631 // Disable the slide-mode only buttons when leaving.
632 this.editButton_.disabled = true;
633 this.printButton_.disabled = true;
635 // Disable touch operation.
636 this.touchHandlers_.enabled = false;
641 * Execute an action when the editor is not busy.
643 * @param {function()} action Function to execute.
645 SlideMode.prototype.executeWhenReady = function(action) {
646 this.editor_.executeWhenReady(action);
650 * @return {boolean} True if the mode has active tools (that should not fade).
652 SlideMode.prototype.hasActiveTool = function() {
653 return this.isEditing();
657 * @return {number} Item count.
660 SlideMode.prototype.getItemCount_ = function() {
661 return this.dataModel_.length;
665 * @param {number} index Index.
666 * @return {Gallery.Item} Item.
668 SlideMode.prototype.getItem = function(index) {
670 /** @type {(Gallery.Item|undefined)} */ (this.dataModel_.item(index));
671 return item === undefined ? null : item;
675 * @return {number} Selected index.
677 SlideMode.prototype.getSelectedIndex = function() {
678 return this.selectionModel_.selectedIndex;
682 * @return {ImageRect} Screen rectangle of the selected image.
684 SlideMode.prototype.getSelectedImageRect = function() {
685 if (this.getSelectedIndex() < 0)
688 return this.viewport_.getImageBoundsOnScreen();
692 * @return {Gallery.Item} Selected item.
694 SlideMode.prototype.getSelectedItem = function() {
695 return this.getItem(this.getSelectedIndex());
699 * Toggles the full screen mode.
702 SlideMode.prototype.toggleFullScreen_ = function() {
703 util.toggleFullScreen(this.context_.appWindow,
704 !util.isFullScreen(this.context_.appWindow));
708 * Selection change handler.
710 * Commits the current image and displays the newly selected image.
713 SlideMode.prototype.onSelection_ = function() {
714 if (this.selectionModel_.selectedIndexes.length === 0)
715 return; // Ignore temporary empty selection.
717 // Forget the saved selection if the user changed the selection manually.
718 if (!this.isSlideshowOn_())
719 this.savedSelection_ = null;
721 if (this.getSelectedItem() === this.displayedItem_)
722 return; // Do not reselect.
724 this.commitItem_(this.loadSelectedItem_.bind(this));
728 * Change the selection.
730 * @param {number} index New selected index.
731 * @param {number=} opt_slideHint Slide animation direction (-1|1).
733 SlideMode.prototype.select = function(index, opt_slideHint) {
734 this.slideHint_ = opt_slideHint || null;
735 this.selectionModel_.selectedIndex = index;
736 this.selectionModel_.leadIndex = index;
740 * Load the selected item.
744 SlideMode.prototype.loadSelectedItem_ = function() {
745 var slideHint = this.slideHint_;
746 this.slideHint_ = null;
748 if (this.getSelectedItem() === this.displayedItem_)
749 return; // Do not reselect.
751 var index = this.getSelectedIndex();
755 var displayedIndex = this.dataModel_.indexOf(this.displayedItem_);
757 slideHint || (displayedIndex > 0 ? index - displayedIndex : 1);
759 if (Math.abs(step) != 1) {
760 // Long leap, the sequence is broken, we have no good prefetch candidate.
761 this.sequenceDirection_ = 0;
762 this.sequenceLength_ = 0;
763 } else if (this.sequenceDirection_ === step) {
764 // Keeping going in sequence.
765 this.sequenceLength_++;
767 // Reversed the direction. Reset the counter.
768 this.sequenceDirection_ = step;
769 this.sequenceLength_ = 1;
772 this.displayedItem_ = this.getSelectedItem();
773 var selectedItem = assertInstanceof(this.getSelectedItem(), Gallery.Item);
775 function shouldPrefetch(loadType, step, sequenceLength) {
776 // Never prefetch when selecting out of sequence.
777 if (Math.abs(step) != 1)
780 // Always prefetch if the previous load was from cache.
781 if (loadType === ImageView.LoadType.CACHED_FULL)
784 // Prefetch if we have been going in the same direction for long enough.
785 return sequenceLength >= 3;
788 this.currentUniqueKey_++;
789 var selectedUniqueKey = this.currentUniqueKey_;
791 // Discard, since another load has been invoked after this one.
792 if (selectedUniqueKey != this.currentUniqueKey_)
797 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
798 function() {} /* no displayCallback */,
799 function(loadType, delay) {
800 // Discard, since another load has been invoked after this one.
801 if (selectedUniqueKey != this.currentUniqueKey_)
803 if (shouldPrefetch(loadType, step, this.sequenceLength_))
804 this.requestPrefetch(step, delay);
805 if (this.isSlideshowPlaying_())
806 this.scheduleNextSlide_();
811 * Unload the current image.
813 * @param {ImageRect=} opt_zoomToRect Rectangle for zoom effect.
816 SlideMode.prototype.unloadImage_ = function(opt_zoomToRect) {
817 this.imageView_.unload(opt_zoomToRect);
821 * Data model 'splice' event handler.
822 * @param {!Event} event Event.
826 SlideMode.prototype.onSplice_ = function(event) {
827 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
829 // Splice invalidates saved indices, drop the saved selection.
830 this.savedSelection_ = null;
832 if (event.removed.length != 1)
835 // Delay the selection to let the ribbon splice handler work first.
836 setTimeout(function() {
837 if (this.dataModel_.length === 0) {
838 // No items left. Unload the image, disable edit and print button, and
840 this.commitItem_(function() {
842 this.printButton_.disabled = true;
843 this.editButton_.disabled = true;
844 this.errorBanner_.show('GALLERY_NO_IMAGES');
849 var displayedItemNotRemvoed = event.removed.every(function(item) {
850 return item !== this.displayedItem_;
852 if (!displayedItemNotRemvoed) {
853 // There is the next item, select it. Otherwise, select the last item.
854 var nextIndex = Math.min(event.index, this.dataModel_.length - 1);
855 // To force to dispatch a selection change event, clear selection before.
856 this.selectionModel_.clear();
857 this.select(nextIndex);
863 * @param {number} direction -1 for left, 1 for right.
864 * @return {number} Next index in the given direction, with wrapping.
867 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
868 function advance(index, limit) {
869 index += (direction > 0 ? 1 : -1);
877 // If the saved selection is multiple the Slideshow should cycle through
878 // the saved selection.
879 if (this.isSlideshowOn_() &&
880 this.savedSelection_ && this.savedSelection_.length > 1) {
881 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
882 this.savedSelection_.length);
883 return this.savedSelection_[pos];
885 return advance(this.getSelectedIndex(), this.getItemCount_());
890 * Advance the selection based on the pressed key ID.
891 * @param {string} keyID Key identifier.
893 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
894 var prev = (keyID === 'Up' ||
896 keyID === 'MediaPreviousTrack');
897 this.advanceManually(prev ? -1 : 1);
901 * Advance the selection as a result of a user action (as opposed to an
902 * automatic change in the slideshow mode).
903 * @param {number} direction -1 for left, 1 for right.
905 SlideMode.prototype.advanceManually = function(direction) {
906 if (this.isSlideshowPlaying_())
907 this.pauseSlideshow_();
908 cr.dispatchSimpleEvent(this, 'useraction');
909 this.selectNext(direction);
913 * Select the next item.
914 * @param {number} direction -1 for left, 1 for right.
916 SlideMode.prototype.selectNext = function(direction) {
917 this.select(this.getNextSelectedIndex_(direction), direction);
921 * Select the first item.
923 SlideMode.prototype.selectFirst = function() {
928 * Select the last item.
930 SlideMode.prototype.selectLast = function() {
931 this.select(this.getItemCount_() - 1);
937 * Load and display an item.
939 * @param {!Gallery.Item} item Item.
940 * @param {!ImageView.Effect} effect Transition effect object.
941 * @param {function()} displayCallback Called when the image is displayed
942 * (which can happen before the image load due to caching).
943 * @param {function(number, number)} loadCallback Called when the image is fully
947 SlideMode.prototype.loadItem_ = function(
948 item, effect, displayCallback, loadCallback) {
949 this.showSpinner_(true);
951 var loadDone = this.itemLoaded_.bind(this, item, loadCallback);
953 var displayDone = function() {
954 cr.dispatchSimpleEvent(this, 'image-displayed');
958 this.editor_.openSession(
961 this.saveCurrentImage_.bind(this, item),
967 * A callback function when the editor opens a editing session for an image.
968 * @param {!Gallery.Item} item Gallery item.
969 * @param {function(number, number)} loadCallback Called when the image is fully
971 * @param {number} loadType Load type.
972 * @param {number} delay Delay.
973 * @param {*=} opt_error Error.
976 SlideMode.prototype.itemLoaded_ = function(
977 item, loadCallback, loadType, delay, opt_error) {
978 var entry = item.getEntry();
980 this.showSpinner_(false);
981 if (loadType === ImageView.LoadType.ERROR) {
982 // if we have a specific error, then display it
984 this.errorBanner_.show(/** @type {string} */ (opt_error));
986 // otherwise try to infer general error
987 this.errorBanner_.show('GALLERY_IMAGE_ERROR');
989 } else if (loadType === ImageView.LoadType.OFFLINE) {
990 this.errorBanner_.show('GALLERY_IMAGE_OFFLINE');
993 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
995 var toMillions = function(number) {
996 return Math.round(number / (1000 * 1000));
999 var metadata = item.getMetadataItem();
1001 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
1002 toMillions(metadata.size));
1005 var canvas = this.imageView_.getCanvas();
1006 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
1007 toMillions(canvas.width * canvas.height));
1009 var extIndex = entry.name.lastIndexOf('.');
1010 var ext = extIndex < 0 ? '' :
1011 entry.name.substr(extIndex + 1).toLowerCase();
1012 if (ext === 'jpeg') ext = 'jpg';
1013 ImageUtil.metrics.recordEnum(
1014 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
1016 // Enable or disable buttons for editing and printing.
1018 this.editButton_.disabled = true;
1019 this.printButton_.disabled = true;
1021 this.editButton_.disabled = false;
1022 this.printButton_.disabled = false;
1025 // Saved label is hidden by default.
1026 this.savedLabel_.hidden = true;
1028 // Disable overwrite original checkbox until settings is loaded.
1029 this.overwriteOriginalCheckbox_.disabled = true;
1030 this.overwriteOriginalCheckbox_.checked = false;
1033 keys[SlideMode.OVERWRITE_ORIGINAL_KEY] = true;
1034 chrome.storage.local.get(keys,
1036 // Users can overwrite original file only if loaded image is original
1038 if (item.isOriginal() &&
1039 item.isWritableFile(this.volumeManager_)) {
1040 this.overwriteOriginalCheckbox_.disabled = false;
1041 this.overwriteOriginalCheckbox_.checked =
1042 values[SlideMode.OVERWRITE_ORIGINAL_KEY];
1046 loadCallback(loadType, delay);
1050 * Commit changes to the current item and reset all messages/indicators.
1052 * @param {function()} callback Callback.
1055 SlideMode.prototype.commitItem_ = function(callback) {
1056 this.showSpinner_(false);
1057 this.errorBanner_.clear();
1058 this.editor_.getPrompt().hide();
1059 this.editor_.closeSession(callback);
1063 * Request a prefetch for the next image.
1065 * @param {number} direction -1 or 1.
1066 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
1067 * loading from disrupting the animation that might be still in progress.
1069 SlideMode.prototype.requestPrefetch = function(direction, delay) {
1070 if (this.getItemCount_() <= 1) return;
1072 var index = this.getNextSelectedIndex_(direction);
1073 this.imageView_.prefetch(assert(this.getItem(index)), delay);
1079 * Click handler for the entire document.
1080 * @param {!Event} event Mouse click event.
1083 SlideMode.prototype.onDocumentClick_ = function(event) {
1084 // Events created in fakeMouseClick in test util don't pass this test.
1085 if (!window.IN_TEST)
1086 event = assertInstanceof(event, MouseEvent);
1088 var targetElement = assertInstanceof(event.target, HTMLElement);
1089 // Close the bubble if clicked outside of it and if it is visible.
1090 if (!this.bubble_.contains(targetElement) &&
1091 !this.editButton_.contains(targetElement) &&
1092 !this.arrowLeft_.contains(targetElement) &&
1093 !this.arrowRight_.contains(targetElement) &&
1094 !this.bubble_.hidden) {
1095 this.bubble_.hidden = true;
1102 * @param {!Event} event Event.
1103 * @return {boolean} True if handled.
1105 SlideMode.prototype.onKeyDown = function(event) {
1106 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
1108 if (this.isSlideshowOn_()) {
1110 case 'U+001B': // Escape
1112 this.stopSlideshow_(event);
1115 case 'U+0020': // Space pauses/resumes the slideshow.
1116 case 'MediaPlayPause':
1117 this.toggleSlideshowPause_();
1124 case 'MediaNextTrack':
1125 case 'MediaPreviousTrack':
1126 this.advanceWithKeyboard(keyID);
1129 return true; // Consume all keystrokes in the slideshow mode.
1132 // Handles shortcut keys common for both modes (editing and not-editing).
1134 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
1135 if (!this.printButton_.disabled)
1139 case 'U+0045': // 'e' toggles the editor.
1140 if (!this.editButton_.disabled)
1141 this.toggleEditor(event);
1145 // Handles shortcurt keys for editing mode.
1146 if (this.isEditing()) {
1147 if (this.editor_.onKeyDown(event))
1150 if (keyID === 'U+001B') { // Escape
1151 this.toggleEditor(event);
1158 // Handles shortcut keys for not-editing mode.
1160 case 'U+001B': // Escape
1161 if (this.viewport_.isZoomed()) {
1162 this.viewport_.resetView();
1163 this.touchHandlers_.stopOperation();
1164 this.imageView_.applyViewportChange();
1181 if (this.viewport_.isZoomed()) {
1182 var delta = SlideMode.KEY_OFFSET_MAP[keyID];
1183 this.viewport_.setOffset(
1184 ~~(this.viewport_.getOffsetX() +
1185 delta[0] * this.viewport_.getZoom()),
1186 ~~(this.viewport_.getOffsetY() +
1187 delta[1] * this.viewport_.getZoom()));
1188 this.touchHandlers_.stopOperation();
1189 this.imageView_.applyViewportChange();
1191 this.advanceWithKeyboard(keyID);
1195 case 'MediaNextTrack':
1196 case 'MediaPreviousTrack':
1197 this.advanceWithKeyboard(keyID);
1200 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
1201 this.viewport_.zoomIn();
1202 this.touchHandlers_.stopOperation();
1203 this.imageView_.applyViewportChange();
1206 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
1207 this.viewport_.zoomOut();
1208 this.touchHandlers_.stopOperation();
1209 this.imageView_.applyViewportChange();
1212 case 'Ctrl-U+0030': // Ctrl+'0' zoom reset.
1213 this.viewport_.setZoom(1.0);
1214 this.touchHandlers_.stopOperation();
1215 this.imageView_.applyViewportChange();
1226 SlideMode.prototype.onResize_ = function() {
1227 this.touchHandlers_.stopOperation();
1231 * Handles resize event of viewport.
1234 SlideMode.prototype.onViewportResize_ = function() {
1235 // This method must be called after the resize of viewport.
1236 this.editor_.getBuffer().draw();
1240 * Update thumbnails.
1242 SlideMode.prototype.updateThumbnails = function() {
1243 this.ribbon_.reset();
1245 this.ribbon_.redraw();
1251 * Save the current image to a file.
1253 * @param {!Gallery.Item} item Item to save the image.
1254 * @param {function()} callback Callback.
1257 SlideMode.prototype.saveCurrentImage_ = function(item, callback) {
1258 this.showSpinner_(true);
1260 var savedPromise = this.dataModel_.saveItem(
1261 this.volumeManager_,
1263 this.imageView_.getCanvas(),
1264 this.overwriteOriginalCheckbox_.checked);
1266 savedPromise.then(function() {
1267 this.showSpinner_(false);
1268 this.flashSavedLabel_();
1270 // Record UMA for the first edit.
1271 if (this.imageView_.getContentRevision() === 1)
1272 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
1274 // Users can change overwrite original setting only if there is no undo
1275 // stack and item is original and writable.
1276 var ableToChangeOverwriteOriginalSetting = !this.editor_.canUndo() &&
1277 item.isOriginal() && item.isWritableFile(this.volumeManager_);
1278 this.overwriteOriginalCheckbox_.disabled =
1279 !ableToChangeOverwriteOriginalSetting;
1282 }.bind(this)).catch(function(error) {
1283 console.error(error.stack || error);
1285 this.showSpinner_(false);
1286 this.errorBanner_.show('GALLERY_SAVE_FAILED');
1293 * Flash 'Saved' label briefly to indicate that the image has been saved.
1296 SlideMode.prototype.flashSavedLabel_ = function() {
1297 this.savedLabel_.hidden = false;
1298 var setLabelHighlighted =
1299 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1300 setTimeout(setLabelHighlighted.bind(null, true), 0);
1301 setTimeout(setLabelHighlighted.bind(null, false), 300);
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 * Local storage key for overwrite original checkbox value.
1315 SlideMode.OVERWRITE_ORIGINAL_KEY = 'gallery-overwrite-original';
1318 * Max number that the overwrite info bubble is shown.
1321 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1324 * Handles change event of overwrite original checkbox.
1326 SlideMode.prototype.onOverwriteOriginalCheckboxChanged_ = function() {
1328 items[SlideMode.OVERWRITE_ORIGINAL_KEY] =
1329 this.overwriteOriginalCheckbox_.checked;
1330 chrome.storage.local.set(items);
1334 * Overwrite info bubble close handler.
1337 SlideMode.prototype.onCloseBubble_ = function() {
1338 this.bubble_.hidden = true;
1339 this.setOverwriteBubbleCount_(SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1345 * Slideshow interval in ms.
1347 SlideMode.SLIDESHOW_INTERVAL = 5000;
1350 * First slideshow interval in ms. It should be shorter so that the user
1351 * is not guessing whether the button worked.
1353 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1356 * Empirically determined duration of the fullscreen toggle animation.
1358 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1361 * @return {boolean} True if the slideshow is on.
1364 SlideMode.prototype.isSlideshowOn_ = function() {
1365 return this.container_.hasAttribute('slideshow');
1369 * Starts the slideshow.
1370 * @param {number=} opt_interval First interval in ms.
1371 * @param {Event=} opt_event Event.
1373 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1375 this.viewport_.resetView();
1376 this.imageView_.applyViewportChange();
1378 // Disable touch operation.
1379 this.touchHandlers_.enabled = false;
1381 // Set the attribute early to prevent the toolbar from flashing when
1382 // the slideshow is being started from the mosaic view.
1383 this.container_.setAttribute('slideshow', 'playing');
1386 this.stopEditing_();
1388 // We are in the Mosaic mode. Toggle the mode but remember to return.
1389 this.leaveAfterSlideshow_ = true;
1391 // Wait until the zoom animation from the mosaic mode is done.
1392 var startSlideshowAfterTransition = function() {
1393 setTimeout(function() {
1394 this.startSlideshow.call(this, SlideMode.SLIDESHOW_INTERVAL, opt_event);
1395 }.bind(this), ImageView.MODE_TRANSITION_DURATION);
1397 this.toggleMode_(startSlideshowAfterTransition);
1401 if (opt_event) // Caused by user action, notify the Gallery.
1402 cr.dispatchSimpleEvent(this, 'useraction');
1404 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1405 if (!this.fullscreenBeforeSlideshow_) {
1406 this.toggleFullScreen_();
1407 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1408 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1411 // This is a workaround. Mouseout event is not dispatched when window becomes
1412 // fullscreen and cursor gets out of the element
1413 // TODO(yawano): Find better implementation.
1414 this.dimmableUIController_.setCursorOutOfTools();
1416 this.resumeSlideshow_(opt_interval);
1420 * Stops the slideshow.
1421 * @param {Event=} opt_event Event.
1424 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1425 if (!this.isSlideshowOn_())
1428 if (opt_event) // Caused by user action, notify the Gallery.
1429 cr.dispatchSimpleEvent(this, 'useraction');
1431 this.pauseSlideshow_();
1432 this.container_.removeAttribute('slideshow');
1434 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1435 var fullscreen = util.isFullScreen(this.context_.appWindow);
1436 var toggleModeDelay = 0;
1437 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1438 this.toggleFullScreen_();
1439 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1441 if (this.leaveAfterSlideshow_) {
1442 this.leaveAfterSlideshow_ = false;
1443 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1446 // Re-enable touch operation.
1447 this.touchHandlers_.enabled = true;
1451 * @return {boolean} True if the slideshow is playing (not paused).
1454 SlideMode.prototype.isSlideshowPlaying_ = function() {
1455 return this.container_.getAttribute('slideshow') === 'playing';
1459 * Pauses/resumes the slideshow.
1462 SlideMode.prototype.toggleSlideshowPause_ = function() {
1463 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1464 if (this.isSlideshowPlaying_()) {
1465 this.pauseSlideshow_();
1467 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1472 * @param {number=} opt_interval Slideshow interval in ms.
1475 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1476 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1478 if (this.slideShowTimeout_)
1479 clearTimeout(this.slideShowTimeout_);
1481 this.slideShowTimeout_ = setTimeout(function() {
1482 this.slideShowTimeout_ = null;
1484 }.bind(this), opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1488 * Resumes the slideshow.
1489 * @param {number=} opt_interval Slideshow interval in ms.
1492 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1493 this.container_.setAttribute('slideshow', 'playing');
1494 this.scheduleNextSlide_(opt_interval);
1498 * Pauses the slideshow.
1501 SlideMode.prototype.pauseSlideshow_ = function() {
1502 this.container_.setAttribute('slideshow', 'paused');
1503 if (this.slideShowTimeout_) {
1504 clearTimeout(this.slideShowTimeout_);
1505 this.slideShowTimeout_ = null;
1510 * @return {boolean} True if the editor is active.
1512 SlideMode.prototype.isEditing = function() {
1513 return this.container_.hasAttribute('editing');
1520 SlideMode.prototype.stopEditing_ = function() {
1521 if (this.isEditing())
1522 this.toggleEditor();
1526 * Activate/deactivate editor.
1527 * @param {Event=} opt_event Event.
1529 SlideMode.prototype.toggleEditor = function(opt_event) {
1530 if (opt_event) // Caused by user action, notify the Gallery.
1531 cr.dispatchSimpleEvent(this, 'useraction');
1533 if (!this.active_) {
1534 this.toggleMode_(this.toggleEditor.bind(this));
1538 this.stopSlideshow_();
1540 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1541 this.editButtonToggleRipple_.activated = this.isEditing();
1543 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1545 this.viewport_.resetView();
1547 // Scale the screen so that it doesn't overlap the toolbars.
1548 this.viewport_.setScreenTop(ImageEditor.Toolbar.HEIGHT);
1549 this.viewport_.setScreenBottom(ImageEditor.Toolbar.HEIGHT);
1551 this.imageView_.applyViewportChange();
1553 // TODO(yawano): Integarate this warning message to the non writable format
1555 if (this.context_.readonlyDirName) {
1556 this.editor_.getPrompt().showAt(
1557 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1559 // If image format is not writable format, show toast to let user know
1560 // that edits will be saved to a copy.
1561 var item = this.getItem(this.getSelectedIndex());
1562 if (!item.isWritableFormat()) {
1563 item.getCopyName().then(function(copyName) {
1564 this.filesToast_.show(
1565 strf('GALLERY_NON_WRITABLE_FORMAT_WARNING', copyName));
1570 this.touchHandlers_.enabled = false;
1571 this.dimmableUIController_.setDisabled(true);
1573 // Show overwrite original bubble if it hasn't been shown for max times.
1574 this.getOverwriteBubbleCount_().then(function(count) {
1575 if (count >= SlideMode.OVERWRITE_BUBBLE_MAX_TIMES)
1578 this.setOverwriteBubbleCount_(count + 1);
1579 this.bubble_.hidden = false;
1582 this.editor_.getPrompt().hide();
1583 this.editor_.leaveModeGently();
1585 this.viewport_.setScreenTop(0);
1586 this.viewport_.setScreenBottom(0);
1587 this.imageView_.applyViewportChange();
1589 this.bubble_.hidden = true;
1591 this.touchHandlers_.enabled = true;
1592 this.dimmableUIController_.setDisabled(false);
1597 * Gets count of overwrite bubble.
1598 * @return {!Promise<number>}
1601 SlideMode.prototype.getOverwriteBubbleCount_ = function() {
1602 return new Promise(function(resolve, reject) {
1604 requests[SlideMode.OVERWRITE_BUBBLE_KEY] = 0;
1606 chrome.storage.local.get(requests, function(results) {
1607 if (!!chrome.runtime.lastError) {
1608 reject(chrome.runtime.lastError);
1612 resolve(results[SlideMode.OVERWRITE_BUBBLE_KEY]);
1618 * Sets count of overwrite bubble.
1619 * @param {number} value
1622 SlideMode.prototype.setOverwriteBubbleCount_ = function(value) {
1624 requests[SlideMode.OVERWRITE_BUBBLE_KEY] = value;
1625 chrome.storage.local.set(requests);
1629 * Prints the current item.
1632 SlideMode.prototype.print_ = function() {
1633 cr.dispatchSimpleEvent(this, 'useraction');
1638 * Shows/hides the busy spinner.
1640 * @param {boolean} on True if show, false if hide.
1643 SlideMode.prototype.showSpinner_ = function(on) {
1644 if (this.spinnerTimer_) {
1645 clearTimeout(this.spinnerTimer_);
1646 this.spinnerTimer_ = null;
1650 this.spinnerTimer_ = setTimeout(function() {
1651 this.spinnerTimer_ = null;
1652 ImageUtil.setAttribute(this.container_, 'spinner', true);
1653 }.bind(this), 1000);
1655 ImageUtil.setAttribute(this.container_, 'spinner', false);
1660 * Apply the change of viewport.
1662 SlideMode.prototype.applyViewportChange = function() {
1663 this.imageView_.applyViewportChange();
1667 * Touch handlers of the slide mode.
1668 * @param {!Element} targetElement Event source.
1669 * @param {!SlideMode} slideMode Slide mode to be operated by the handler.
1673 function TouchHandler(targetElement, slideMode) {
1680 this.targetElement_ = targetElement;
1683 * Target of touch operations.
1684 * @type {!SlideMode}
1688 this.slideMode_ = slideMode;
1691 * Flag to enable/disable touch operation.
1695 this.enabled_ = true;
1698 * Whether it is in a touch operation that is started from targetElement or
1703 this.touchStarted_ = false;
1706 * The swipe action that should happen only once in an operation is already
1714 * Event on beginning of the current gesture.
1715 * The variable is updated when the number of touch finger changed.
1716 * @type {TouchEvent}
1719 this.gestureStartEvent_ = null;
1722 * Rotation value on beginning of the current gesture.
1726 this.gestureStartRotation_ = 0;
1730 * @type {TouchEvent}
1733 this.lastEvent_ = null;
1736 * Zoom value just after last touch event.
1740 this.lastZoom_ = 1.0;
1742 targetElement.addEventListener('touchstart', this.onTouchStart_.bind(this));
1743 var onTouchEventBound = this.onTouchEvent_.bind(this);
1744 targetElement.ownerDocument.addEventListener('touchmove', onTouchEventBound);
1745 targetElement.ownerDocument.addEventListener('touchend', onTouchEventBound);
1747 targetElement.addEventListener('mousewheel', this.onMouseWheel_.bind(this));
1751 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1752 * horizontally it's considered as a swipe gesture (change the current image).
1756 TouchHandler.SWIPE_THRESHOLD = 100;
1759 * Rotation threshold in degrees.
1763 TouchHandler.ROTATION_THRESHOLD = 25;
1766 * Obtains distance between fingers.
1767 * @param {!TouchEvent} event Touch event. It should include more than two
1769 * @return {number} Distance between touch[0] and touch[1].
1771 TouchHandler.getDistance = function(event) {
1772 var touch1 = event.touches[0];
1773 var touch2 = event.touches[1];
1774 var dx = touch1.clientX - touch2.clientX;
1775 var dy = touch1.clientY - touch2.clientY;
1776 return Math.sqrt(dx * dx + dy * dy);
1780 * Obtains the degrees of the pinch twist angle.
1781 * @param {!TouchEvent} event1 Start touch event. It should include more than
1783 * @param {!TouchEvent} event2 Current touch event. It should include more than
1785 * @return {number} Degrees of the pinch twist angle.
1787 TouchHandler.getTwistAngle = function(event1, event2) {
1788 var dx1 = event1.touches[1].clientX - event1.touches[0].clientX;
1789 var dy1 = event1.touches[1].clientY - event1.touches[0].clientY;
1790 var dx2 = event2.touches[1].clientX - event2.touches[0].clientX;
1791 var dy2 = event2.touches[1].clientY - event2.touches[0].clientY;
1792 var innerProduct = dx1 * dx2 + dy1 * dy2; // |v1| * |v2| * cos(t) = x / r
1793 var outerProduct = dx1 * dy2 - dy1 * dx2; // |v1| * |v2| * sin(t) = y / r
1794 return Math.atan2(outerProduct, innerProduct) * 180 / Math.PI; // atan(y / x)
1797 TouchHandler.prototype = /** @struct */ {
1799 * @param {boolean} flag New value.
1802 this.enabled_ = flag;
1804 this.stopOperation();
1809 * Stops the current touch operation.
1811 TouchHandler.prototype.stopOperation = function() {
1812 this.touchStarted_ = false;
1814 this.gestureStartEvent_ = null;
1815 this.lastEvent_ = null;
1816 this.lastZoom_ = 1.0;
1820 * Handles touch start events.
1821 * @param {!Event} event Touch event.
1824 TouchHandler.prototype.onTouchStart_ = function(event) {
1825 event = assertInstanceof(event, TouchEvent);
1826 if (this.enabled_ && event.touches.length === 1)
1827 this.touchStarted_ = true;
1831 * Handles touch move and touch end events.
1832 * @param {!Event} event Touch event.
1835 TouchHandler.prototype.onTouchEvent_ = function(event) {
1836 event = assertInstanceof(event, TouchEvent);
1837 // Check if the current touch operation started from the target element or
1839 if (!this.touchStarted_)
1842 // Check if the current touch operation ends with the event.
1843 if (event.touches.length === 0) {
1844 this.stopOperation();
1848 // Check if a new gesture started or not.
1849 var viewport = this.slideMode_.getViewport();
1850 if (!this.lastEvent_ ||
1851 this.lastEvent_.touches.length !== event.touches.length) {
1852 if (event.touches.length === 2 ||
1853 event.touches.length === 1) {
1854 this.gestureStartEvent_ = event;
1855 this.gestureStartRotation_ = viewport.getRotation();
1856 this.lastEvent_ = event;
1857 this.lastZoom_ = viewport.getZoom();
1859 this.gestureStartEvent_ = null;
1860 this.gestureStartRotation_ = 0;
1861 this.lastEvent_ = null;
1862 this.lastZoom_ = 1.0;
1867 // Handle the gesture movement.
1868 switch (event.touches.length) {
1870 if (viewport.isZoomed()) {
1871 // Scrolling an image by swipe.
1872 var dx = event.touches[0].screenX - this.lastEvent_.touches[0].screenX;
1873 var dy = event.touches[0].screenY - this.lastEvent_.touches[0].screenY;
1875 viewport.getOffsetX() + dx, viewport.getOffsetY() + dy);
1876 this.slideMode_.applyViewportChange();
1878 // Traversing images by swipe.
1882 event.touches[0].clientX -
1883 this.gestureStartEvent_.touches[0].clientX;
1884 if (dx > TouchHandler.SWIPE_THRESHOLD) {
1885 this.slideMode_.advanceManually(-1);
1887 } else if (dx < -TouchHandler.SWIPE_THRESHOLD) {
1888 this.slideMode_.advanceManually(1);
1896 var distance1 = TouchHandler.getDistance(this.lastEvent_);
1897 var distance2 = TouchHandler.getDistance(event);
1898 if (distance1 === 0)
1900 var zoom = distance2 / distance1 * this.lastZoom_;
1901 viewport.setZoom(zoom);
1904 assert(this.gestureStartEvent_);
1905 var angle = TouchHandler.getTwistAngle(this.gestureStartEvent_, event);
1906 if (angle > TouchHandler.ROTATION_THRESHOLD)
1907 viewport.setRotation(this.gestureStartRotation_ + 1);
1908 else if (angle < -TouchHandler.ROTATION_THRESHOLD)
1909 viewport.setRotation(this.gestureStartRotation_ - 1);
1911 viewport.setRotation(this.gestureStartRotation_);
1912 this.slideMode_.applyViewportChange();
1916 // Update the last event.
1917 this.lastEvent_ = event;
1918 this.lastZoom_ = viewport.getZoom();
1922 * Handles mouse wheel events.
1923 * @param {!Event} event Wheel event.
1926 TouchHandler.prototype.onMouseWheel_ = function(event) {
1927 var event = assertInstanceof(event, MouseEvent);
1928 var viewport = this.slideMode_.getViewport();
1929 if (!this.enabled_ || !viewport.isZoomed())
1931 this.stopOperation();
1933 viewport.getOffsetX() + event.wheelDeltaX,
1934 viewport.getOffsetY() + event.wheelDeltaY);
1935 this.slideMode_.applyViewportChange();