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.
8 * Slide mode displays a single image and has a set of controls to navigate
9 * between the images and to edit an image.
11 * @param {Element} container Main container element.
12 * @param {Element} content Content container element.
13 * @param {Element} toolbar Toolbar element.
14 * @param {ImageEditor.Prompt} prompt Prompt.
15 * @param {cr.ui.ArrayDataModel} dataModel Data model.
16 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
17 * @param {Object} context Context.
18 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
19 * @param {function(string):string} displayStringFunction String formatting
23 function SlideMode(container, content, toolbar, prompt,
24 dataModel, selectionModel, context,
25 toggleMode, displayStringFunction) {
26 this.container_ = container;
27 this.document_ = container.ownerDocument;
28 this.content = content;
29 this.toolbar_ = toolbar;
30 this.prompt_ = prompt;
31 this.dataModel_ = dataModel;
32 this.selectionModel_ = selectionModel;
33 this.context_ = context;
34 this.metadataCache_ = context.metadataCache;
35 this.toggleMode_ = toggleMode;
36 this.displayStringFunction_ = displayStringFunction;
38 this.onSelectionBound_ = this.onSelection_.bind(this);
39 this.onSpliceBound_ = this.onSplice_.bind(this);
40 this.onContentBound_ = this.onContentChange_.bind(this);
42 // Unique numeric key, incremented per each load attempt used to discard
43 // old attempts. This can happen especially when changing selection fast or
44 // Internet connection is slow.
45 this.currentUniqueKey_ = 0;
47 this.initListeners_();
52 * SlideMode extends cr.EventTarget.
54 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
57 * List of available editor modes.
58 * @type {Array.<ImageEditor.Mode>}
60 SlideMode.editorModes = [
61 new ImageEditor.Mode.InstantAutofix(),
62 new ImageEditor.Mode.Crop(),
63 new ImageEditor.Mode.Exposure(),
64 new ImageEditor.Mode.OneClick(
65 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
66 new ImageEditor.Mode.OneClick(
67 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
71 * @return {string} Mode name.
73 SlideMode.prototype.getName = function() { return 'slide'; };
76 * @return {string} Mode title.
78 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE'; };
81 * Initialize the listeners.
84 SlideMode.prototype.initListeners_ = function() {
85 window.addEventListener('resize', this.onResize_.bind(this));
92 SlideMode.prototype.initDom_ = function() {
93 // Container for displayed image.
94 this.imageContainer_ = util.createChild(
95 this.document_.querySelector('.content'), 'image-container');
96 this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
98 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
100 // Overwrite options and info bubble.
101 this.options_ = util.createChild(
102 this.toolbar_.querySelector('.filename-spacer'), 'options');
104 this.savedLabel_ = util.createChild(this.options_, 'saved');
105 this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
107 var overwriteOriginalBox =
108 util.createChild(this.options_, 'overwrite-original');
110 this.overwriteOriginal_ = util.createChild(
111 overwriteOriginalBox, 'common white', 'input');
112 this.overwriteOriginal_.type = 'checkbox';
113 this.overwriteOriginal_.id = 'overwrite-checkbox';
114 util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) {
115 // Out-of-the box default is 'true'
116 this.overwriteOriginal_.checked =
117 (typeof value !== 'string' || value === 'true');
119 this.overwriteOriginal_.addEventListener('click',
120 this.onOverwriteOriginalClick_.bind(this));
122 var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
123 overwriteLabel.textContent =
124 this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
125 overwriteLabel.setAttribute('for', 'overwrite-checkbox');
127 this.bubble_ = util.createChild(this.toolbar_, 'bubble');
128 this.bubble_.hidden = true;
130 var bubbleContent = util.createChild(this.bubble_);
131 bubbleContent.innerHTML = this.displayStringFunction_(
132 'GALLERY_OVERWRITE_BUBBLE');
134 util.createChild(this.bubble_, 'pointer bottom', 'span');
136 var bubbleClose = util.createChild(this.bubble_, 'close-x');
137 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
139 // Ribbon and related controls.
140 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
143 util.createChild(this.arrowBox_, 'arrow left tool dimmable');
144 this.arrowLeft_.addEventListener('click',
145 this.advanceManually.bind(this, -1));
146 util.createChild(this.arrowLeft_);
148 util.createChild(this.arrowBox_, 'arrow-spacer');
151 util.createChild(this.arrowBox_, 'arrow right tool dimmable');
152 this.arrowRight_.addEventListener('click',
153 this.advanceManually.bind(this, 1));
154 util.createChild(this.arrowRight_);
156 this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer');
157 this.ribbon_ = new Ribbon(
158 this.document_, this.dataModel_, this.selectionModel_);
159 this.ribbonSpacer_.appendChild(this.ribbon_);
162 var errorWrapper = util.createChild(this.container_, 'prompt-wrapper');
163 errorWrapper.setAttribute('pos', 'center');
165 this.errorBanner_ = util.createChild(errorWrapper, 'error-banner');
167 util.createChild(this.container_, 'spinner');
169 var slideShowButton = util.createChild(this.toolbar_,
170 'button slideshow', 'button');
171 slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
172 slideShowButton.addEventListener('click',
173 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
175 var slideShowToolbar =
176 util.createChild(this.container_, 'tool slideshow-toolbar');
177 util.createChild(slideShowToolbar, 'slideshow-play').
178 addEventListener('click', this.toggleSlideshowPause_.bind(this));
179 util.createChild(slideShowToolbar, 'slideshow-end').
180 addEventListener('click', this.stopSlideshow_.bind(this));
184 this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button');
185 this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
186 this.editButton_.setAttribute('disabled', ''); // Disabled by default.
187 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
189 this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button');
190 this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
191 this.printButton_.setAttribute('disabled', ''); // Disabled by default.
192 this.printButton_.addEventListener('click', this.print_.bind(this));
194 this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer');
195 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
197 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
198 this.editBarModeWrapper_ = util.createChild(
199 this.editBarMode_, 'edit-modal-wrapper');
200 this.editBarModeWrapper_.hidden = true;
202 // Objects supporting image display and editing.
203 this.viewport_ = new Viewport();
205 this.imageView_ = new ImageView(
206 this.imageContainer_,
209 this.editor_ = new ImageEditor(
214 root: this.container_,
215 image: this.imageContainer_,
216 toolbar: this.editBarMain_,
217 mode: this.editBarModeWrapper_
219 SlideMode.editorModes,
220 this.displayStringFunction_,
221 this.onToolsVisibilityChanged_.bind(this));
223 this.editor_.getBuffer().addOverlay(
224 new SwipeOverlay(this.advanceManually.bind(this)));
228 * Load items, display the selected item.
229 * @param {Rect} zoomFromRect Rectangle for zoom effect.
230 * @param {function} displayCallback Called when the image is displayed.
231 * @param {function} loadCallback Called when the image is displayed.
233 SlideMode.prototype.enter = function(
234 zoomFromRect, displayCallback, loadCallback) {
235 this.sequenceDirection_ = 0;
236 this.sequenceLength_ = 0;
238 var loadDone = function(loadType, delay) {
241 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
242 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
243 this.dataModel_.addEventListener('content', this.onContentBound_);
245 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
246 this.ribbon_.enable();
248 // Wait 1000ms after the animation is done, then prefetch the next image.
249 this.requestPrefetch(1, delay + 1000);
251 if (loadCallback) loadCallback();
254 // The latest |leave| call might have left the image animating. Remove it.
257 new Promise(function(fulfill) {
258 // If the items are empty, just show the error message.
259 if (this.getItemCount_() === 0) {
260 this.displayedIndex_ = -1;
261 //TODO(hirono) Show this message in the grid mode too.
262 this.showErrorBanner_('GALLERY_NO_IMAGES');
267 // Remember the selection if it is empty or multiple. It will be restored
268 // in |leave| if the user did not changing the selection manually.
269 var currentSelection = this.selectionModel_.selectedIndexes;
270 if (currentSelection.length === 1)
271 this.savedSelection_ = null;
273 this.savedSelection_ = currentSelection;
275 // Ensure valid single selection.
276 // Note that the SlideMode object is not listening to selection change yet.
277 this.select(Math.max(0, this.getSelectedIndex()));
278 this.displayedIndex_ = this.getSelectedIndex();
280 // Show the selected item ASAP, then complete the initialization
281 // (loading the ribbon thumbnails can take some time).
282 var selectedItem = this.getSelectedItem();
284 // Load the image of the item.
287 zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect),
289 function(loadType, delay) {
292 }.bind(this)).then(function(delay) {
293 // Turn the mode active.
295 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
296 this.ribbon_.enable();
298 // Register handlers.
299 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
300 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
301 this.dataModel_.addEventListener('content', this.onContentBound_);
303 // Wait 1000ms after the animation is done, then prefetch the next image.
304 this.requestPrefetch(1, delay + 1000);
306 // Call load callback.
309 }.bind(this)).catch(function(error) {
310 console.error(error.stack, error);
316 * @param {Rect} zoomToRect Rectangle for zoom effect.
317 * @param {function} callback Called when the image is committed and
318 * the zoom-out animation has started.
320 SlideMode.prototype.leave = function(zoomToRect, callback) {
321 var commitDone = function() {
323 this.stopSlideshow_();
324 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
325 this.selectionModel_.removeEventListener(
326 'change', this.onSelectionBound_);
327 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
328 this.dataModel_.removeEventListener('content', this.onContentBound_);
329 this.ribbon_.disable();
330 this.active_ = false;
331 if (this.savedSelection_)
332 this.selectionModel_.selectedIndexes = this.savedSelection_;
333 this.unloadImage_(zoomToRect);
337 this.viewport_.setZoomIndex(0);
338 if (this.getItemCount_() === 0) {
339 this.showErrorBanner_(false);
342 this.commitItem_(commitDone);
345 // Disable the slide-mode only buttons when leaving.
346 this.editButton_.setAttribute('disabled', '');
347 this.printButton_.setAttribute('disabled', '');
352 * Execute an action when the editor is not busy.
354 * @param {function} action Function to execute.
356 SlideMode.prototype.executeWhenReady = function(action) {
357 this.editor_.executeWhenReady(action);
361 * @return {boolean} True if the mode has active tools (that should not fade).
363 SlideMode.prototype.hasActiveTool = function() {
364 return this.isEditing();
368 * @return {number} Item count.
371 SlideMode.prototype.getItemCount_ = function() {
372 return this.dataModel_.length;
376 * @param {number} index Index.
377 * @return {Gallery.Item} Item.
379 SlideMode.prototype.getItem = function(index) {
380 return this.dataModel_.item(index);
384 * @return {Gallery.Item} Selected index.
386 SlideMode.prototype.getSelectedIndex = function() {
387 return this.selectionModel_.selectedIndex;
391 * @return {Rect} Screen rectangle of the selected image.
393 SlideMode.prototype.getSelectedImageRect = function() {
394 if (this.getSelectedIndex() < 0)
397 return this.viewport_.getImageBoundsOnScreen();
401 * @return {Gallery.Item} Selected item.
403 SlideMode.prototype.getSelectedItem = function() {
404 return this.getItem(this.getSelectedIndex());
408 * Toggles the full screen mode.
411 SlideMode.prototype.toggleFullScreen_ = function() {
412 util.toggleFullScreen(this.context_.appWindow,
413 !util.isFullScreen(this.context_.appWindow));
417 * Selection change handler.
419 * Commits the current image and displays the newly selected image.
422 SlideMode.prototype.onSelection_ = function() {
423 if (this.selectionModel_.selectedIndexes.length === 0)
424 return; // Temporary empty selection.
426 // Forget the saved selection if the user changed the selection manually.
427 if (!this.isSlideshowOn_())
428 this.savedSelection_ = null;
430 if (this.getSelectedIndex() === this.displayedIndex_)
431 return; // Do not reselect.
433 this.commitItem_(this.loadSelectedItem_.bind(this));
437 * Handles changes in tools visibility, and if the header is dimmed, then
438 * requests disabling the draggable app region.
442 SlideMode.prototype.onToolsVisibilityChanged_ = function() {
444 this.document_.querySelector('.header').hasAttribute('dimmed');
445 this.context_.onAppRegionChanged(!headerDimmed);
449 * Change the selection.
451 * @param {number} index New selected index.
452 * @param {number=} opt_slideHint Slide animation direction (-1|1).
454 SlideMode.prototype.select = function(index, opt_slideHint) {
455 this.slideHint_ = opt_slideHint;
456 this.selectionModel_.selectedIndex = index;
457 this.selectionModel_.leadIndex = index;
461 * Load the selected item.
465 SlideMode.prototype.loadSelectedItem_ = function() {
466 var slideHint = this.slideHint_;
467 this.slideHint_ = undefined;
469 var index = this.getSelectedIndex();
470 if (index === this.displayedIndex_)
471 return; // Do not reselect.
473 var step = slideHint || (index - this.displayedIndex_);
475 if (Math.abs(step) != 1) {
476 // Long leap, the sequence is broken, we have no good prefetch candidate.
477 this.sequenceDirection_ = 0;
478 this.sequenceLength_ = 0;
479 } else if (this.sequenceDirection_ === step) {
480 // Keeping going in sequence.
481 this.sequenceLength_++;
483 // Reversed the direction. Reset the counter.
484 this.sequenceDirection_ = step;
485 this.sequenceLength_ = 1;
488 this.displayedIndex_ = index;
489 var selectedItem = this.getSelectedItem();
491 if (this.sequenceLength_ <= 1) {
492 // We have just broke the sequence. Touch the current image so that it stays
493 // in the cache longer.
494 this.imageView_.prefetch(selectedItem);
497 function shouldPrefetch(loadType, step, sequenceLength) {
498 // Never prefetch when selecting out of sequence.
499 if (Math.abs(step) != 1)
502 // Always prefetch if the previous load was from cache.
503 if (loadType === ImageView.LOAD_TYPE_CACHED_FULL)
506 // Prefetch if we have been going in the same direction for long enough.
507 return sequenceLength >= 3;
510 this.currentUniqueKey_++;
511 var selectedUniqueKey = this.currentUniqueKey_;
513 // Discard, since another load has been invoked after this one.
514 if (selectedUniqueKey != this.currentUniqueKey_)
519 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
520 function() {} /* no displayCallback */,
521 function(loadType, delay) {
522 // Discard, since another load has been invoked after this one.
523 if (selectedUniqueKey != this.currentUniqueKey_)
525 if (shouldPrefetch(loadType, step, this.sequenceLength_))
526 this.requestPrefetch(step, delay);
527 if (this.isSlideshowPlaying_())
528 this.scheduleNextSlide_();
533 * Unload the current image.
535 * @param {Rect} zoomToRect Rectangle for zoom effect.
538 SlideMode.prototype.unloadImage_ = function(zoomToRect) {
539 this.imageView_.unload(zoomToRect);
543 * Data model 'splice' event handler.
544 * @param {Event} event Event.
547 SlideMode.prototype.onSplice_ = function(event) {
548 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
550 // Splice invalidates saved indices, drop the saved selection.
551 this.savedSelection_ = null;
553 if (event.removed.length != 1)
556 // Delay the selection to let the ribbon splice handler work first.
557 setTimeout(function() {
558 if (event.index < this.dataModel_.length) {
559 // There is the next item, select it.
560 // The next item is now at the same index as the removed one, so we need
561 // to correct displayIndex_ so that loadSelectedItem_ does not think
562 // we are re-selecting the same item (and does right-to-left slide-in
564 this.displayedIndex_ = event.index - 1;
565 this.select(event.index);
566 } else if (this.dataModel_.length) {
567 // Removed item is the rightmost, but there are more items.
568 this.select(event.index - 1); // Select the new last index.
570 // No items left. Unload the image and show the banner.
571 this.commitItem_(function() {
573 this.showErrorBanner_('GALLERY_NO_IMAGES');
580 * @param {number} direction -1 for left, 1 for right.
581 * @return {number} Next index in the given direction, with wrapping.
584 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
585 function advance(index, limit) {
586 index += (direction > 0 ? 1 : -1);
594 // If the saved selection is multiple the Slideshow should cycle through
595 // the saved selection.
596 if (this.isSlideshowOn_() &&
597 this.savedSelection_ && this.savedSelection_.length > 1) {
598 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
599 this.savedSelection_.length);
600 return this.savedSelection_[pos];
602 return advance(this.getSelectedIndex(), this.getItemCount_());
607 * Advance the selection based on the pressed key ID.
608 * @param {string} keyID Key identifier.
610 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
611 var prev = (keyID === 'Up' ||
613 keyID === 'MediaPreviousTrack');
614 this.advanceManually(prev ? -1 : 1);
618 * Advance the selection as a result of a user action (as opposed to an
619 * automatic change in the slideshow mode).
620 * @param {number} direction -1 for left, 1 for right.
622 SlideMode.prototype.advanceManually = function(direction) {
623 if (this.isSlideshowPlaying_())
624 this.pauseSlideshow_();
625 cr.dispatchSimpleEvent(this, 'useraction');
626 this.selectNext(direction);
630 * Select the next item.
631 * @param {number} direction -1 for left, 1 for right.
633 SlideMode.prototype.selectNext = function(direction) {
634 this.select(this.getNextSelectedIndex_(direction), direction);
638 * Select the first item.
640 SlideMode.prototype.selectFirst = function() {
645 * Select the last item.
647 SlideMode.prototype.selectLast = function() {
648 this.select(this.getItemCount_() - 1);
654 * Load and display an item.
656 * @param {Gallery.Item} item Item.
657 * @param {Object} effect Transition effect object.
658 * @param {function} displayCallback Called when the image is displayed
659 * (which can happen before the image load due to caching).
660 * @param {function} loadCallback Called when the image is fully loaded.
663 SlideMode.prototype.loadItem_ = function(
664 item, effect, displayCallback, loadCallback) {
665 var entry = item.getEntry();
666 var metadata = item.getMetadata();
667 this.showSpinner_(true);
669 var loadDone = function(loadType, delay, error) {
670 this.showSpinner_(false);
671 if (loadType === ImageView.LOAD_TYPE_ERROR) {
672 // if we have a specific error, then display it
674 this.showErrorBanner_(error);
676 // otherwise try to infer general error
677 this.showErrorBanner_('GALLERY_IMAGE_ERROR');
679 } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) {
680 this.showErrorBanner_('GALLERY_IMAGE_OFFLINE');
683 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
685 var toMillions = function(number) {
686 return Math.round(number / (1000 * 1000));
689 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
690 toMillions(metadata.filesystem.size));
692 var canvas = this.imageView_.getCanvas();
693 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
694 toMillions(canvas.width * canvas.height));
696 var extIndex = entry.name.lastIndexOf('.');
697 var ext = extIndex < 0 ? '' :
698 entry.name.substr(extIndex + 1).toLowerCase();
699 if (ext === 'jpeg') ext = 'jpg';
700 ImageUtil.metrics.recordEnum(
701 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
703 // Enable or disable buttons for editing and printing.
705 this.editButton_.setAttribute('disabled', '');
706 this.printButton_.setAttribute('disabled', '');
708 this.editButton_.removeAttribute('disabled');
709 this.printButton_.removeAttribute('disabled');
712 // For once edited image, disallow the 'overwrite' setting change.
713 ImageUtil.setAttribute(this.options_, 'saved',
714 !this.getSelectedItem().isOriginal());
716 util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
718 var times = typeof value === 'string' ? parseInt(value, 10) : 0;
719 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
720 this.bubble_.hidden = false;
721 if (this.isEditing()) {
722 util.platform.setPreference(
723 SlideMode.OVERWRITE_BUBBLE_KEY, times + 1);
728 loadCallback(loadType, delay);
731 var displayDone = function() {
732 cr.dispatchSimpleEvent(this, 'image-displayed');
736 this.editor_.openSession(
737 item, effect, this.saveCurrentImage_.bind(this), displayDone, loadDone);
741 * Commit changes to the current item and reset all messages/indicators.
743 * @param {function} callback Callback.
746 SlideMode.prototype.commitItem_ = function(callback) {
747 this.showSpinner_(false);
748 this.showErrorBanner_(false);
749 this.editor_.getPrompt().hide();
750 this.editor_.closeSession(callback);
754 * Request a prefetch for the next image.
756 * @param {number} direction -1 or 1.
757 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
758 * loading from disrupting the animation that might be still in progress.
760 SlideMode.prototype.requestPrefetch = function(direction, delay) {
761 if (this.getItemCount_() <= 1) return;
763 var index = this.getNextSelectedIndex_(direction);
764 this.imageView_.prefetch(this.getItem(index), delay);
770 * Unload handler, to be called from the top frame.
771 * @param {boolean} exiting True if the app is exiting.
773 SlideMode.prototype.onUnload = function(exiting) {
777 * Click handler for the image container.
779 * @param {Event} event Mouse click event.
782 SlideMode.prototype.onClick_ = function(event) {
786 * Click handler for the entire document.
787 * @param {Event} e Mouse click event.
790 SlideMode.prototype.onDocumentClick_ = function(e) {
791 // Close the bubble if clicked outside of it and if it is visible.
792 if (!this.bubble_.contains(e.target) &&
793 !this.editButton_.contains(e.target) &&
794 !this.arrowLeft_.contains(e.target) &&
795 !this.arrowRight_.contains(e.target) &&
796 !this.bubble_.hidden) {
797 this.bubble_.hidden = true;
804 * @param {Event} event Event.
805 * @return {boolean} True if handled.
807 SlideMode.prototype.onKeyDown = function(event) {
808 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
810 if (this.isSlideshowOn_()) {
812 case 'U+001B': // Escape exits the slideshow.
814 this.stopSlideshow_(event);
817 case 'U+0020': // Space pauses/resumes the slideshow.
818 case 'MediaPlayPause':
819 this.toggleSlideshowPause_();
826 case 'MediaNextTrack':
827 case 'MediaPreviousTrack':
828 this.advanceWithKeyboard(keyID);
831 return true; // Consume all keystrokes in the slideshow mode.
834 if (this.isEditing() && this.editor_.onKeyDown(event))
838 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
839 if (!this.printButton_.hasAttribute('disabled'))
843 case 'U+0045': // 'e' toggles the editor.
844 if (!this.editButton_.hasAttribute('disabled'))
845 this.toggleEditor(event);
848 case 'U+001B': // Escape
849 if (!this.isEditing())
850 return false; // Not handled.
851 this.toggleEditor(event);
864 case 'MediaNextTrack':
865 case 'MediaPreviousTrack':
866 this.advanceWithKeyboard(keyID);
869 case 'Ctrl-U+00BB': // Ctrl+'=' zoom in.
870 if (!this.isEditing()) {
871 this.viewport_.setZoomIndex(this.viewport_.getZoomIndex() + 1);
872 this.imageView_.applyViewportChange();
876 case 'Ctrl-U+00BD': // Ctrl+'-' zoom out.
877 if (!this.isEditing()) {
878 this.viewport_.setZoomIndex(this.viewport_.getZoomIndex() - 1);
879 this.imageView_.applyViewportChange();
891 SlideMode.prototype.onResize_ = function() {
892 this.viewport_.setScreenSize(
893 this.container_.clientWidth, this.container_.clientHeight);
894 this.editor_.getBuffer().draw();
900 SlideMode.prototype.updateThumbnails = function() {
901 this.ribbon_.reset();
903 this.ribbon_.redraw();
909 * Save the current image to a file.
911 * @param {function} callback Callback.
914 SlideMode.prototype.saveCurrentImage_ = function(callback) {
915 this.showSpinner_(true);
917 var item = this.getSelectedItem();
918 var savedPromise = this.dataModel_.saveItem(
920 this.imageView_.getCanvas(),
921 this.shouldOverwriteOriginal_());
923 savedPromise.catch(function(error) {
924 // TODO(hirono): Implement write error handling.
925 // Until then pretend that the save succeeded.
926 console.error(error.stack || error);
928 this.showSpinner_(false);
929 this.flashSavedLabel_();
931 // Allow changing the 'Overwrite original' setting only if the user
932 // used Undo to restore the original image AND it is not a copy.
933 // Otherwise lock the setting in its current state.
934 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
935 ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
937 // Record UMA for the first edit.
938 if (this.imageView_.getContentRevision() === 1)
939 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
942 cr.dispatchSimpleEvent(this, 'image-saved');
943 }.bind(this)).catch(function(error) {
944 console.error(error.stack || error);
949 * Update caches when the selected item has been renamed.
950 * @param {Event} event Event.
953 SlideMode.prototype.onContentChange_ = function(event) {
954 var newEntry = event.item.getEntry();
955 if (!util.isSameEntry(newEntry, event.oldEntry))
956 this.imageView_.changeEntry(newEntry);
960 * Flash 'Saved' label briefly to indicate that the image has been saved.
963 SlideMode.prototype.flashSavedLabel_ = function() {
964 var setLabelHighlighted =
965 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
966 setTimeout(setLabelHighlighted.bind(null, true), 0);
967 setTimeout(setLabelHighlighted.bind(null, false), 300);
971 * Local storage key for the 'Overwrite original' setting.
974 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
977 * Local storage key for the number of times that
978 * the overwrite info bubble has been displayed.
981 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
984 * Max number that the overwrite info bubble is shown.
987 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
990 * @return {boolean} True if 'Overwrite original' is set.
993 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
994 return this.overwriteOriginal_.checked;
998 * 'Overwrite original' checkbox handler.
999 * @param {Event} event Event.
1002 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
1003 util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked);
1007 * Overwrite info bubble close handler.
1010 SlideMode.prototype.onCloseBubble_ = function() {
1011 this.bubble_.hidden = true;
1012 util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
1013 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1019 * Slideshow interval in ms.
1021 SlideMode.SLIDESHOW_INTERVAL = 5000;
1024 * First slideshow interval in ms. It should be shorter so that the user
1025 * is not guessing whether the button worked.
1027 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1030 * Empirically determined duration of the fullscreen toggle animation.
1032 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1035 * @return {boolean} True if the slideshow is on.
1038 SlideMode.prototype.isSlideshowOn_ = function() {
1039 return this.container_.hasAttribute('slideshow');
1043 * Start the slideshow.
1044 * @param {number=} opt_interval First interval in ms.
1045 * @param {Event=} opt_event Event.
1047 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1049 this.viewport_.setZoomIndex(0);
1050 this.imageView_.applyViewportChange();
1052 // Set the attribute early to prevent the toolbar from flashing when
1053 // the slideshow is being started from the mosaic view.
1054 this.container_.setAttribute('slideshow', 'playing');
1057 this.stopEditing_();
1059 // We are in the Mosaic mode. Toggle the mode but remember to return.
1060 this.leaveAfterSlideshow_ = true;
1061 this.toggleMode_(this.startSlideshow.bind(
1062 this, SlideMode.SLIDESHOW_INTERVAL, opt_event));
1066 if (opt_event) // Caused by user action, notify the Gallery.
1067 cr.dispatchSimpleEvent(this, 'useraction');
1069 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1070 if (!this.fullscreenBeforeSlideshow_) {
1071 // Wait until the zoom animation from the mosaic mode is done.
1072 setTimeout(this.toggleFullScreen_.bind(this),
1073 ImageView.ZOOM_ANIMATION_DURATION);
1074 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1075 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1078 this.resumeSlideshow_(opt_interval);
1082 * Stop the slideshow.
1083 * @param {Event=} opt_event Event.
1086 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1087 if (!this.isSlideshowOn_())
1090 if (opt_event) // Caused by user action, notify the Gallery.
1091 cr.dispatchSimpleEvent(this, 'useraction');
1093 this.pauseSlideshow_();
1094 this.container_.removeAttribute('slideshow');
1096 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1097 var fullscreen = util.isFullScreen(this.context_.appWindow);
1098 var toggleModeDelay = 0;
1099 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1100 this.toggleFullScreen_();
1101 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1103 if (this.leaveAfterSlideshow_) {
1104 this.leaveAfterSlideshow_ = false;
1105 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1110 * @return {boolean} True if the slideshow is playing (not paused).
1113 SlideMode.prototype.isSlideshowPlaying_ = function() {
1114 return this.container_.getAttribute('slideshow') === 'playing';
1118 * Pause/resume the slideshow.
1121 SlideMode.prototype.toggleSlideshowPause_ = function() {
1122 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1123 if (this.isSlideshowPlaying_()) {
1124 this.pauseSlideshow_();
1126 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1131 * @param {number=} opt_interval Slideshow interval in ms.
1134 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1135 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1137 if (this.slideShowTimeout_)
1138 clearTimeout(this.slideShowTimeout_);
1140 this.slideShowTimeout_ = setTimeout(function() {
1141 this.slideShowTimeout_ = null;
1144 opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1148 * Resume the slideshow.
1149 * @param {number=} opt_interval Slideshow interval in ms.
1152 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1153 this.container_.setAttribute('slideshow', 'playing');
1154 this.scheduleNextSlide_(opt_interval);
1158 * Pause the slideshow.
1161 SlideMode.prototype.pauseSlideshow_ = function() {
1162 this.container_.setAttribute('slideshow', 'paused');
1163 if (this.slideShowTimeout_) {
1164 clearTimeout(this.slideShowTimeout_);
1165 this.slideShowTimeout_ = null;
1170 * @return {boolean} True if the editor is active.
1172 SlideMode.prototype.isEditing = function() {
1173 return this.container_.hasAttribute('editing');
1180 SlideMode.prototype.stopEditing_ = function() {
1181 if (this.isEditing())
1182 this.toggleEditor();
1186 * Activate/deactivate editor.
1187 * @param {Event=} opt_event Event.
1189 SlideMode.prototype.toggleEditor = function(opt_event) {
1190 if (opt_event) // Caused by user action, notify the Gallery.
1191 cr.dispatchSimpleEvent(this, 'useraction');
1193 if (!this.active_) {
1194 this.toggleMode_(this.toggleEditor.bind(this));
1198 this.stopSlideshow_();
1200 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1202 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1204 this.viewport_.setZoomIndex(0);
1205 this.imageView_.applyViewportChange();
1206 if (this.context_.readonlyDirName) {
1207 this.editor_.getPrompt().showAt(
1208 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
1211 this.editor_.getPrompt().hide();
1212 this.editor_.leaveModeGently();
1217 * Prints the current item.
1220 SlideMode.prototype.print_ = function() {
1221 cr.dispatchSimpleEvent(this, 'useraction');
1226 * Display the error banner.
1227 * @param {string} message Message.
1230 SlideMode.prototype.showErrorBanner_ = function(message) {
1232 this.errorBanner_.textContent = this.displayStringFunction_(message);
1234 ImageUtil.setAttribute(this.container_, 'error', !!message);
1238 * Show/hide the busy spinner.
1240 * @param {boolean} on True if show, false if hide.
1243 SlideMode.prototype.showSpinner_ = function(on) {
1244 if (this.spinnerTimer_) {
1245 clearTimeout(this.spinnerTimer_);
1246 this.spinnerTimer_ = null;
1250 this.spinnerTimer_ = setTimeout(function() {
1251 this.spinnerTimer_ = null;
1252 ImageUtil.setAttribute(this.container_, 'spinner', true);
1253 }.bind(this), 1000);
1255 ImageUtil.setAttribute(this.container_, 'spinner', false);
1260 * Overlay that handles swipe gestures. Changes to the next or previous file.
1261 * @param {function(number)} callback A callback accepting the swipe direction
1262 * (1 means left, -1 right).
1264 * @implements {ImageBuffer.Overlay}
1266 function SwipeOverlay(callback) {
1267 this.callback_ = callback;
1271 * Inherit ImageBuffer.Overlay.
1273 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype;
1276 * @param {number} x X pointer position.
1277 * @param {number} y Y pointer position.
1278 * @param {boolean} touch True if dragging caused by touch.
1279 * @return {function} The closure to call on drag.
1281 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) {
1286 return function(x, y) {
1287 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) {
1290 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) {
1298 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1299 * horizontally it's considered as a swipe gesture (change the current image).
1301 SwipeOverlay.SWIPE_THRESHOLD = 100;