Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(336)

Side by Side Diff: chrome/browser/resources/file_manager/js/photo/slide_mode.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8 * Slide mode displays a single image and has a set of controls to navigate
9 * between the images and to edit an image.
10 *
11 * TODO(kaznacheev): Introduce a parameter object.
12 *
13 * @param {Element} container Main container element.
14 * @param {Element} content Content container element.
15 * @param {Element} toolbar Toolbar element.
16 * @param {ImageEditor.Prompt} prompt Prompt.
17 * @param {cr.ui.ArrayDataModel} dataModel Data model.
18 * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
19 * @param {Object} context Context.
20 * @param {function(function())} toggleMode Function to toggle the Gallery mode.
21 * @param {function(string):string} displayStringFunction String formatting
22 * function.
23 * @constructor
24 */
25 function SlideMode(container, content, toolbar, prompt,
26 dataModel, selectionModel, context,
27 toggleMode, displayStringFunction) {
28 this.container_ = container;
29 this.document_ = container.ownerDocument;
30 this.content = content;
31 this.toolbar_ = toolbar;
32 this.prompt_ = prompt;
33 this.dataModel_ = dataModel;
34 this.selectionModel_ = selectionModel;
35 this.context_ = context;
36 this.metadataCache_ = context.metadataCache;
37 this.toggleMode_ = toggleMode;
38 this.displayStringFunction_ = displayStringFunction;
39
40 this.onSelectionBound_ = this.onSelection_.bind(this);
41 this.onSpliceBound_ = this.onSplice_.bind(this);
42 this.onContentBound_ = this.onContentChange_.bind(this);
43
44 // Unique numeric key, incremented per each load attempt used to discard
45 // old attempts. This can happen especially when changing selection fast or
46 // Internet connection is slow.
47 this.currentUniqueKey_ = 0;
48
49 this.initListeners_();
50 this.initDom_();
51 }
52
53 /**
54 * SlideMode extends cr.EventTarget.
55 */
56 SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
57
58 /**
59 * List of available editor modes.
60 * @type {Array.<ImageEditor.Mode>}
61 */
62 SlideMode.editorModes = [
63 new ImageEditor.Mode.InstantAutofix(),
64 new ImageEditor.Mode.Crop(),
65 new ImageEditor.Mode.Exposure(),
66 new ImageEditor.Mode.OneClick(
67 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
68 new ImageEditor.Mode.OneClick(
69 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
70 ];
71
72 /**
73 * @return {string} Mode name.
74 */
75 SlideMode.prototype.getName = function() { return 'slide' };
76
77 /**
78 * @return {string} Mode title.
79 */
80 SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' };
81
82 /**
83 * Initialize the listeners.
84 * @private
85 */
86 SlideMode.prototype.initListeners_ = function() {
87 window.addEventListener('resize', this.onResize_.bind(this), false);
88 };
89
90 /**
91 * Initialize the UI.
92 * @private
93 */
94 SlideMode.prototype.initDom_ = function() {
95 // Container for displayed image or video.
96 this.imageContainer_ = util.createChild(
97 this.document_.querySelector('.content'), 'image-container');
98 this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
99
100 this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
101
102 // Overwrite options and info bubble.
103 this.options_ = util.createChild(
104 this.toolbar_.querySelector('.filename-spacer'), 'options');
105
106 this.savedLabel_ = util.createChild(this.options_, 'saved');
107 this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
108
109 var overwriteOriginalBox =
110 util.createChild(this.options_, 'overwrite-original');
111
112 this.overwriteOriginal_ = util.createChild(
113 overwriteOriginalBox, 'common white', 'input');
114 this.overwriteOriginal_.type = 'checkbox';
115 this.overwriteOriginal_.id = 'overwrite-checkbox';
116 util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) {
117 // Out-of-the box default is 'true'
118 this.overwriteOriginal_.checked =
119 (typeof value != 'string' || value == 'true');
120 }.bind(this));
121 this.overwriteOriginal_.addEventListener('click',
122 this.onOverwriteOriginalClick_.bind(this));
123
124 var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
125 overwriteLabel.textContent =
126 this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
127 overwriteLabel.setAttribute('for', 'overwrite-checkbox');
128
129 this.bubble_ = util.createChild(this.toolbar_, 'bubble');
130 this.bubble_.hidden = true;
131
132 var bubbleContent = util.createChild(this.bubble_);
133 bubbleContent.innerHTML = this.displayStringFunction_(
134 'GALLERY_OVERWRITE_BUBBLE');
135
136 util.createChild(this.bubble_, 'pointer bottom', 'span');
137
138 var bubbleClose = util.createChild(this.bubble_, 'close-x');
139 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
140
141 // Video player controls.
142 this.mediaSpacer_ =
143 util.createChild(this.container_, 'video-controls-spacer');
144 this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool');
145 this.mediaControls_ = new VideoControls(
146 this.mediaToolbar_,
147 this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'),
148 this.displayStringFunction_.bind(this),
149 this.toggleFullScreen_.bind(this),
150 this.container_);
151
152 // Ribbon and related controls.
153 this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
154
155 this.arrowLeft_ =
156 util.createChild(this.arrowBox_, 'arrow left tool dimmable');
157 this.arrowLeft_.addEventListener('click',
158 this.advanceManually.bind(this, -1));
159 util.createChild(this.arrowLeft_);
160
161 util.createChild(this.arrowBox_, 'arrow-spacer');
162
163 this.arrowRight_ =
164 util.createChild(this.arrowBox_, 'arrow right tool dimmable');
165 this.arrowRight_.addEventListener('click',
166 this.advanceManually.bind(this, 1));
167 util.createChild(this.arrowRight_);
168
169 this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer');
170 this.ribbon_ = new Ribbon(this.document_,
171 this.metadataCache_, this.dataModel_, this.selectionModel_);
172 this.ribbonSpacer_.appendChild(this.ribbon_);
173
174 // Error indicator.
175 var errorWrapper = util.createChild(this.container_, 'prompt-wrapper');
176 errorWrapper.setAttribute('pos', 'center');
177
178 this.errorBanner_ = util.createChild(errorWrapper, 'error-banner');
179
180 util.createChild(this.container_, 'spinner');
181
182 var slideShowButton = util.createChild(this.toolbar_,
183 'button slideshow', 'button');
184 slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
185 slideShowButton.addEventListener('click',
186 this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
187
188 var slideShowToolbar =
189 util.createChild(this.container_, 'tool slideshow-toolbar');
190 util.createChild(slideShowToolbar, 'slideshow-play').
191 addEventListener('click', this.toggleSlideshowPause_.bind(this));
192 util.createChild(slideShowToolbar, 'slideshow-end').
193 addEventListener('click', this.stopSlideshow_.bind(this));
194
195 // Editor.
196
197 this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button');
198 this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
199 this.editButton_.setAttribute('disabled', ''); // Disabled by default.
200 this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
201
202 this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button');
203 this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
204 this.printButton_.setAttribute('disabled', ''); // Disabled by default.
205 this.printButton_.addEventListener('click', this.print_.bind(this));
206
207 this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer');
208 this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
209
210 this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
211 this.editBarModeWrapper_ = util.createChild(
212 this.editBarMode_, 'edit-modal-wrapper');
213 this.editBarModeWrapper_.hidden = true;
214
215 // Objects supporting image display and editing.
216 this.viewport_ = new Viewport();
217
218 this.imageView_ = new ImageView(
219 this.imageContainer_,
220 this.viewport_,
221 this.metadataCache_);
222
223 this.editor_ = new ImageEditor(
224 this.viewport_,
225 this.imageView_,
226 this.prompt_,
227 {
228 root: this.container_,
229 image: this.imageContainer_,
230 toolbar: this.editBarMain_,
231 mode: this.editBarModeWrapper_
232 },
233 SlideMode.editorModes,
234 this.displayStringFunction_);
235
236 this.editor_.getBuffer().addOverlay(
237 new SwipeOverlay(this.advanceManually.bind(this)));
238 };
239
240 /**
241 * Load items, display the selected item.
242 * @param {Rect} zoomFromRect Rectangle for zoom effect.
243 * @param {function} displayCallback Called when the image is displayed.
244 * @param {function} loadCallback Called when the image is displayed.
245 */
246 SlideMode.prototype.enter = function(
247 zoomFromRect, displayCallback, loadCallback) {
248 this.sequenceDirection_ = 0;
249 this.sequenceLength_ = 0;
250
251 var loadDone = function(loadType, delay) {
252 this.active_ = true;
253
254 this.selectionModel_.addEventListener('change', this.onSelectionBound_);
255 this.dataModel_.addEventListener('splice', this.onSpliceBound_);
256 this.dataModel_.addEventListener('content', this.onContentBound_);
257
258 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
259 this.ribbon_.enable();
260
261 // Wait 1000ms after the animation is done, then prefetch the next image.
262 this.requestPrefetch(1, delay + 1000);
263
264 if (loadCallback) loadCallback();
265 }.bind(this);
266
267 // The latest |leave| call might have left the image animating. Remove it.
268 this.unloadImage_();
269
270 if (this.getItemCount_() == 0) {
271 this.displayedIndex_ = -1;
272 //TODO(kaznacheev) Show this message in the grid mode too.
273 this.showErrorBanner_('GALLERY_NO_IMAGES');
274 loadDone();
275 } else {
276 // Remember the selection if it is empty or multiple. It will be restored
277 // in |leave| if the user did not changing the selection manually.
278 var currentSelection = this.selectionModel_.selectedIndexes;
279 if (currentSelection.length == 1)
280 this.savedSelection_ = null;
281 else
282 this.savedSelection_ = currentSelection;
283
284 // Ensure valid single selection.
285 // Note that the SlideMode object is not listening to selection change yet.
286 this.select(Math.max(0, this.getSelectedIndex()));
287 this.displayedIndex_ = this.getSelectedIndex();
288
289 var selectedItem = this.getSelectedItem();
290 var selectedUrl = selectedItem.getUrl();
291 // Show the selected item ASAP, then complete the initialization
292 // (loading the ribbon thumbnails can take some time).
293 this.metadataCache_.get(selectedUrl, Gallery.METADATA_TYPE,
294 function(metadata) {
295 this.loadItem_(selectedUrl, metadata,
296 zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect),
297 displayCallback, loadDone);
298 }.bind(this));
299
300 }
301 };
302
303 /**
304 * Leave the mode.
305 * @param {Rect} zoomToRect Rectangle for zoom effect.
306 * @param {function} callback Called when the image is committed and
307 * the zoom-out animation has started.
308 */
309 SlideMode.prototype.leave = function(zoomToRect, callback) {
310 var commitDone = function() {
311 this.stopEditing_();
312 this.stopSlideshow_();
313 ImageUtil.setAttribute(this.arrowBox_, 'active', false);
314 this.selectionModel_.removeEventListener(
315 'change', this.onSelectionBound_);
316 this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
317 this.dataModel_.removeEventListener('content', this.onContentBound_);
318 this.ribbon_.disable();
319 this.active_ = false;
320 if (this.savedSelection_)
321 this.selectionModel_.selectedIndexes = this.savedSelection_;
322 this.unloadImage_(zoomToRect);
323 callback();
324 }.bind(this);
325
326 if (this.getItemCount_() == 0) {
327 this.showErrorBanner_(false);
328 commitDone();
329 } else {
330 this.commitItem_(commitDone);
331 }
332
333 // Disable the slide-mode only buttons when leaving.
334 this.editButton_.setAttribute('disabled', '');
335 this.printButton_.setAttribute('disabled', '');
336 };
337
338
339 /**
340 * Execute an action when the editor is not busy.
341 *
342 * @param {function} action Function to execute.
343 */
344 SlideMode.prototype.executeWhenReady = function(action) {
345 this.editor_.executeWhenReady(action);
346 };
347
348 /**
349 * @return {boolean} True if the mode has active tools (that should not fade).
350 */
351 SlideMode.prototype.hasActiveTool = function() {
352 return this.isEditing();
353 };
354
355 /**
356 * @return {number} Item count.
357 * @private
358 */
359 SlideMode.prototype.getItemCount_ = function() {
360 return this.dataModel_.length;
361 };
362
363 /**
364 * @param {number} index Index.
365 * @return {Gallery.Item} Item.
366 */
367 SlideMode.prototype.getItem = function(index) {
368 return this.dataModel_.item(index);
369 };
370
371 /**
372 * @return {Gallery.Item} Selected index.
373 */
374 SlideMode.prototype.getSelectedIndex = function() {
375 return this.selectionModel_.selectedIndex;
376 };
377
378 /**
379 * @return {Rect} Screen rectangle of the selected image.
380 */
381 SlideMode.prototype.getSelectedImageRect = function() {
382 if (this.getSelectedIndex() < 0)
383 return null;
384 else
385 return this.viewport_.getScreenClipped();
386 };
387
388 /**
389 * @return {Gallery.Item} Selected item.
390 */
391 SlideMode.prototype.getSelectedItem = function() {
392 return this.getItem(this.getSelectedIndex());
393 };
394
395 /**
396 * Toggles the full screen mode.
397 * @private
398 */
399 SlideMode.prototype.toggleFullScreen_ = function() {
400 util.toggleFullScreen(this.context_.appWindow,
401 !util.isFullScreen(this.context_.appWindow));
402 };
403
404 /**
405 * Selection change handler.
406 *
407 * Commits the current image and displays the newly selected image.
408 * @private
409 */
410 SlideMode.prototype.onSelection_ = function() {
411 if (this.selectionModel_.selectedIndexes.length == 0)
412 return; // Temporary empty selection.
413
414 // Forget the saved selection if the user changed the selection manually.
415 if (!this.isSlideshowOn_())
416 this.savedSelection_ = null;
417
418 if (this.getSelectedIndex() == this.displayedIndex_)
419 return; // Do not reselect.
420
421 this.commitItem_(this.loadSelectedItem_.bind(this));
422 };
423
424 /**
425 * Change the selection.
426 *
427 * @param {number} index New selected index.
428 * @param {number=} opt_slideHint Slide animation direction (-1|1).
429 */
430 SlideMode.prototype.select = function(index, opt_slideHint) {
431 this.slideHint_ = opt_slideHint;
432 this.selectionModel_.selectedIndex = index;
433 this.selectionModel_.leadIndex = index;
434 };
435
436 /**
437 * Load the selected item.
438 *
439 * @private
440 */
441 SlideMode.prototype.loadSelectedItem_ = function() {
442 var slideHint = this.slideHint_;
443 this.slideHint_ = undefined;
444
445 var index = this.getSelectedIndex();
446 if (index == this.displayedIndex_)
447 return; // Do not reselect.
448
449 var step = slideHint || (index - this.displayedIndex_);
450
451 if (Math.abs(step) != 1) {
452 // Long leap, the sequence is broken, we have no good prefetch candidate.
453 this.sequenceDirection_ = 0;
454 this.sequenceLength_ = 0;
455 } else if (this.sequenceDirection_ == step) {
456 // Keeping going in sequence.
457 this.sequenceLength_++;
458 } else {
459 // Reversed the direction. Reset the counter.
460 this.sequenceDirection_ = step;
461 this.sequenceLength_ = 1;
462 }
463
464 if (this.sequenceLength_ <= 1) {
465 // We have just broke the sequence. Touch the current image so that it stays
466 // in the cache longer.
467 this.imageView_.prefetch(this.imageView_.contentID_);
468 }
469
470 this.displayedIndex_ = index;
471
472 function shouldPrefetch(loadType, step, sequenceLength) {
473 // Never prefetch when selecting out of sequence.
474 if (Math.abs(step) != 1)
475 return false;
476
477 // Never prefetch after a video load (decoding the next image can freeze
478 // the UI for a second or two).
479 if (loadType == ImageView.LOAD_TYPE_VIDEO_FILE)
480 return false;
481
482 // Always prefetch if the previous load was from cache.
483 if (loadType == ImageView.LOAD_TYPE_CACHED_FULL)
484 return true;
485
486 // Prefetch if we have been going in the same direction for long enough.
487 return sequenceLength >= 3;
488 }
489
490 var selectedItem = this.getSelectedItem();
491 this.currentUniqueKey_++;
492 var selectedUniqueKey = this.currentUniqueKey_;
493 var onMetadata = function(metadata) {
494 // Discard, since another load has been invoked after this one.
495 if (selectedUniqueKey != this.currentUniqueKey_) return;
496 this.loadItem_(selectedItem.getUrl(), metadata,
497 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
498 function() {} /* no displayCallback */,
499 function(loadType, delay) {
500 // Discard, since another load has been invoked after this one.
501 if (selectedUniqueKey != this.currentUniqueKey_) return;
502 if (shouldPrefetch(loadType, step, this.sequenceLength_)) {
503 this.requestPrefetch(step, delay);
504 }
505 if (this.isSlideshowPlaying_())
506 this.scheduleNextSlide_();
507 }.bind(this));
508 }.bind(this);
509 this.metadataCache_.get(
510 selectedItem.getUrl(), Gallery.METADATA_TYPE, onMetadata);
511 };
512
513 /**
514 * Unload the current image.
515 *
516 * @param {Rect} zoomToRect Rectangle for zoom effect.
517 * @private
518 */
519 SlideMode.prototype.unloadImage_ = function(zoomToRect) {
520 this.imageView_.unload(zoomToRect);
521 this.container_.removeAttribute('video');
522 };
523
524 /**
525 * Data model 'splice' event handler.
526 * @param {Event} event Event.
527 * @private
528 */
529 SlideMode.prototype.onSplice_ = function(event) {
530 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
531
532 // Splice invalidates saved indices, drop the saved selection.
533 this.savedSelection_ = null;
534
535 if (event.removed.length != 1)
536 return;
537
538 // Delay the selection to let the ribbon splice handler work first.
539 setTimeout(function() {
540 if (event.index < this.dataModel_.length) {
541 // There is the next item, select it.
542 // The next item is now at the same index as the removed one, so we need
543 // to correct displayIndex_ so that loadSelectedItem_ does not think
544 // we are re-selecting the same item (and does right-to-left slide-in
545 // animation).
546 this.displayedIndex_ = event.index - 1;
547 this.select(event.index);
548 } else if (this.dataModel_.length) {
549 // Removed item is the rightmost, but there are more items.
550 this.select(event.index - 1); // Select the new last index.
551 } else {
552 // No items left. Unload the image and show the banner.
553 this.commitItem_(function() {
554 this.unloadImage_();
555 this.showErrorBanner_('GALLERY_NO_IMAGES');
556 }.bind(this));
557 }
558 }.bind(this), 0);
559 };
560
561 /**
562 * @param {number} direction -1 for left, 1 for right.
563 * @return {number} Next index in the given direction, with wrapping.
564 * @private
565 */
566 SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
567 function advance(index, limit) {
568 index += (direction > 0 ? 1 : -1);
569 if (index < 0)
570 return limit - 1;
571 if (index == limit)
572 return 0;
573 return index;
574 }
575
576 // If the saved selection is multiple the Slideshow should cycle through
577 // the saved selection.
578 if (this.isSlideshowOn_() &&
579 this.savedSelection_ && this.savedSelection_.length > 1) {
580 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
581 this.savedSelection_.length);
582 return this.savedSelection_[pos];
583 } else {
584 return advance(this.getSelectedIndex(), this.getItemCount_());
585 }
586 };
587
588 /**
589 * Advance the selection based on the pressed key ID.
590 * @param {string} keyID Key identifier.
591 */
592 SlideMode.prototype.advanceWithKeyboard = function(keyID) {
593 this.advanceManually(keyID == 'Up' || keyID == 'Left' ? -1 : 1);
594 };
595
596 /**
597 * Advance the selection as a result of a user action (as opposed to an
598 * automatic change in the slideshow mode).
599 * @param {number} direction -1 for left, 1 for right.
600 */
601 SlideMode.prototype.advanceManually = function(direction) {
602 if (this.isSlideshowPlaying_()) {
603 this.pauseSlideshow_();
604 cr.dispatchSimpleEvent(this, 'useraction');
605 }
606 this.selectNext(direction);
607 };
608
609 /**
610 * Select the next item.
611 * @param {number} direction -1 for left, 1 for right.
612 */
613 SlideMode.prototype.selectNext = function(direction) {
614 this.select(this.getNextSelectedIndex_(direction), direction);
615 };
616
617 /**
618 * Select the first item.
619 */
620 SlideMode.prototype.selectFirst = function() {
621 this.select(0);
622 };
623
624 /**
625 * Select the last item.
626 */
627 SlideMode.prototype.selectLast = function() {
628 this.select(this.getItemCount_() - 1);
629 };
630
631 // Loading/unloading
632
633 /**
634 * Load and display an item.
635 *
636 * @param {string} url Item url.
637 * @param {Object} metadata Item metadata.
638 * @param {Object} effect Transition effect object.
639 * @param {function} displayCallback Called when the image is displayed
640 * (which can happen before the image load due to caching).
641 * @param {function} loadCallback Called when the image is fully loaded.
642 * @private
643 */
644 SlideMode.prototype.loadItem_ = function(
645 url, metadata, effect, displayCallback, loadCallback) {
646 this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata);
647
648 this.showSpinner_(true);
649
650 var loadDone = function(loadType, delay, error) {
651 var video = this.isShowingVideo_();
652 ImageUtil.setAttribute(this.container_, 'video', video);
653
654 this.showSpinner_(false);
655 if (loadType == ImageView.LOAD_TYPE_ERROR) {
656 // if we have a specific error, then display it
657 if (error) {
658 this.showErrorBanner_('GALLERY_' + error);
659 } else {
660 // otherwise try to infer general error
661 this.showErrorBanner_(
662 video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR');
663 }
664 } else if (loadType == ImageView.LOAD_TYPE_OFFLINE) {
665 this.showErrorBanner_(
666 video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE');
667 }
668
669 if (video) {
670 // The editor toolbar does not make sense for video, hide it.
671 this.stopEditing_();
672 this.mediaControls_.attachMedia(this.imageView_.getVideo());
673
674 // TODO(kaznacheev): Add metrics for video playback.
675 } else {
676 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
677
678 var toMillions = function(number) {
679 return Math.round(number / (1000 * 1000));
680 };
681
682 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
683 toMillions(metadata.filesystem.size));
684
685 var canvas = this.imageView_.getCanvas();
686 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
687 toMillions(canvas.width * canvas.height));
688
689 var extIndex = url.lastIndexOf('.');
690 var ext = extIndex < 0 ? '' : url.substr(extIndex + 1).toLowerCase();
691 if (ext == 'jpeg') ext = 'jpg';
692 ImageUtil.metrics.recordEnum(
693 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
694 }
695
696 // Enable or disable buttons for editing and printing.
697 if (video || error) {
698 this.editButton_.setAttribute('disabled', '');
699 this.printButton_.setAttribute('disabled', '');
700 } else {
701 this.editButton_.removeAttribute('disabled');
702 this.printButton_.removeAttribute('disabled');
703 }
704
705 // For once edited image, disallow the 'overwrite' setting change.
706 ImageUtil.setAttribute(this.options_, 'saved',
707 !this.getSelectedItem().isOriginal());
708
709 util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
710 function(value) {
711 var times = typeof value == 'string' ? parseInt(value, 10) : 0;
712 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
713 this.bubble_.hidden = false;
714 if (this.isEditing()) {
715 util.platform.setPreference(
716 SlideMode.OVERWRITE_BUBBLE_KEY, times + 1);
717 }
718 }
719 }.bind(this));
720
721 loadCallback(loadType, delay);
722 }.bind(this);
723
724 var displayDone = function() {
725 cr.dispatchSimpleEvent(this, 'image-displayed');
726 displayCallback();
727 }.bind(this);
728
729 this.editor_.openSession(url, metadata, effect,
730 this.saveCurrentImage_.bind(this), displayDone, loadDone);
731 };
732
733 /**
734 * Commit changes to the current item and reset all messages/indicators.
735 *
736 * @param {function} callback Callback.
737 * @private
738 */
739 SlideMode.prototype.commitItem_ = function(callback) {
740 this.showSpinner_(false);
741 this.showErrorBanner_(false);
742 this.editor_.getPrompt().hide();
743
744 // Detach any media attached to the controls.
745 if (this.mediaControls_.getMedia())
746 this.mediaControls_.detachMedia();
747
748 // If showing the video, then pause it. Note, that it may not be attached
749 // to the media controls yet.
750 if (this.isShowingVideo_()) {
751 this.imageView_.getVideo().pause();
752 // Force stop downloading, if uncached on Drive.
753 this.imageView_.getVideo().src = '';
754 this.imageView_.getVideo().load();
755 }
756
757 this.editor_.closeSession(callback);
758 };
759
760 /**
761 * Request a prefetch for the next image.
762 *
763 * @param {number} direction -1 or 1.
764 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
765 * loading from disrupting the animation that might be still in progress.
766 */
767 SlideMode.prototype.requestPrefetch = function(direction, delay) {
768 if (this.getItemCount_() <= 1) return;
769
770 var index = this.getNextSelectedIndex_(direction);
771 var nextItemUrl = this.getItem(index).getUrl();
772 this.imageView_.prefetch(nextItemUrl, delay);
773 };
774
775 // Event handlers.
776
777 /**
778 * Unload handler, to be called from the top frame.
779 * @param {boolean} exiting True if the app is exiting.
780 */
781 SlideMode.prototype.onUnload = function(exiting) {
782 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) {
783 this.mediaControls_.savePosition(exiting);
784 }
785 };
786
787 /**
788 * beforeunload handler, to be called from the top frame.
789 * @return {string} Message to show if there are unsaved changes.
790 */
791 SlideMode.prototype.onBeforeUnload = function() {
792 if (this.editor_.isBusy())
793 return this.displayStringFunction_('GALLERY_UNSAVED_CHANGES');
794 return null;
795 };
796
797 /**
798 * Click handler for the image container.
799 *
800 * @param {Event} event Mouse click event.
801 * @private
802 */
803 SlideMode.prototype.onClick_ = function(event) {
804 if (!this.isShowingVideo_() || !this.mediaControls_.getMedia())
805 return;
806 if (event.ctrlKey) {
807 this.mediaControls_.toggleLoopedModeWithFeedback(true);
808 if (!this.mediaControls_.isPlaying())
809 this.mediaControls_.togglePlayStateWithFeedback();
810 } else {
811 this.mediaControls_.togglePlayStateWithFeedback();
812 }
813 };
814
815 /**
816 * Click handler for the entire document.
817 * @param {Event} e Mouse click event.
818 * @private
819 */
820 SlideMode.prototype.onDocumentClick_ = function(e) {
821 // Close the bubble if clicked outside of it and if it is visible.
822 if (!this.bubble_.contains(e.target) &&
823 !this.editButton_.contains(e.target) &&
824 !this.arrowLeft_.contains(e.target) &&
825 !this.arrowRight_.contains(e.target) &&
826 !this.bubble_.hidden) {
827 this.bubble_.hidden = true;
828 }
829 };
830
831 /**
832 * Keydown handler.
833 *
834 * @param {Event} event Event.
835 * @return {boolean} True if handled.
836 */
837 SlideMode.prototype.onKeyDown = function(event) {
838 var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
839
840 if (this.isSlideshowOn_()) {
841 switch (keyID) {
842 case 'U+001B': // Escape exits the slideshow.
843 this.stopSlideshow_(event);
844 break;
845
846 case 'U+0020': // Space pauses/resumes the slideshow.
847 this.toggleSlideshowPause_();
848 break;
849
850 case 'Up':
851 case 'Down':
852 case 'Left':
853 case 'Right':
854 this.advanceWithKeyboard(keyID);
855 break;
856 }
857 return true; // Consume all keystrokes in the slideshow mode.
858 }
859
860 if (this.isEditing() && this.editor_.onKeyDown(event))
861 return true;
862
863 switch (keyID) {
864 case 'U+0020': // Space toggles the video playback.
865 if (this.isShowingVideo_() && this.mediaControls_.getMedia())
866 this.mediaControls_.togglePlayStateWithFeedback();
867 break;
868
869 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
870 if (!this.printButton_.hasAttribute('disabled'))
871 this.print_();
872 break;
873
874 case 'U+0045': // 'e' toggles the editor.
875 if (!this.editButton_.hasAttribute('disabled'))
876 this.toggleEditor(event);
877 break;
878
879 case 'U+001B': // Escape
880 if (!this.isEditing())
881 return false; // Not handled.
882 this.toggleEditor(event);
883 break;
884
885 case 'Home':
886 this.selectFirst();
887 break;
888 case 'End':
889 this.selectLast();
890 break;
891 case 'Up':
892 case 'Down':
893 case 'Left':
894 case 'Right':
895 this.advanceWithKeyboard(keyID);
896 break;
897
898 default: return false;
899 }
900
901 return true;
902 };
903
904 /**
905 * Resize handler.
906 * @private
907 */
908 SlideMode.prototype.onResize_ = function() {
909 this.viewport_.sizeByFrameAndFit(this.container_);
910 this.viewport_.repaint();
911 };
912
913 /**
914 * Update thumbnails.
915 */
916 SlideMode.prototype.updateThumbnails = function() {
917 this.ribbon_.reset();
918 if (this.active_)
919 this.ribbon_.redraw();
920 };
921
922 // Saving
923
924 /**
925 * Save the current image to a file.
926 *
927 * @param {function} callback Callback.
928 * @private
929 */
930 SlideMode.prototype.saveCurrentImage_ = function(callback) {
931 var item = this.getSelectedItem();
932 var oldUrl = item.getUrl();
933 var canvas = this.imageView_.getCanvas();
934
935 this.showSpinner_(true);
936 var metadataEncoder = ImageEncoder.encodeMetadata(
937 this.selectedImageMetadata_.media, canvas, 1 /* quality */);
938
939 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata(
940 metadataEncoder.getMetadata(), this.selectedImageMetadata_);
941
942 item.saveToFile(
943 this.context_.saveDirEntry,
944 this.shouldOverwriteOriginal_(),
945 canvas,
946 metadataEncoder,
947 function(success) {
948 // TODO(kaznacheev): Implement write error handling.
949 // Until then pretend that the save succeeded.
950 this.showSpinner_(false);
951 this.flashSavedLabel_();
952
953 var e = new Event('content');
954 e.item = item;
955 e.oldUrl = oldUrl;
956 e.metadata = this.selectedImageMetadata_;
957 this.dataModel_.dispatchEvent(e);
958
959 // Allow changing the 'Overwrite original' setting only if the user
960 // used Undo to restore the original image AND it is not a copy.
961 // Otherwise lock the setting in its current state.
962 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
963 ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
964
965 if (this.imageView_.getContentRevision() == 1) { // First edit.
966 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
967 }
968
969 if (oldUrl != item.getUrl()) {
970 this.dataModel_.splice(
971 this.getSelectedIndex(), 0, new Gallery.Item(oldUrl));
972 // The ribbon will ignore the splice above and redraw after the
973 // select call below (while being obscured by the Editor toolbar,
974 // so there is no need for nice animation here).
975 // SlideMode will ignore the selection change as the displayed item
976 // index has not changed.
977 this.select(++this.displayedIndex_);
978 }
979 callback();
980 cr.dispatchSimpleEvent(this, 'image-saved');
981 }.bind(this));
982 };
983
984 /**
985 * Update caches when the selected item url has changed.
986 *
987 * @param {Event} event Event.
988 * @private
989 */
990 SlideMode.prototype.onContentChange_ = function(event) {
991 var newUrl = event.item.getUrl();
992 if (newUrl != event.oldUrl) {
993 this.imageView_.changeUrl(newUrl);
994 }
995 this.metadataCache_.clear(event.oldUrl, Gallery.METADATA_TYPE);
996 };
997
998 /**
999 * Flash 'Saved' label briefly to indicate that the image has been saved.
1000 * @private
1001 */
1002 SlideMode.prototype.flashSavedLabel_ = function() {
1003 var setLabelHighlighted =
1004 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
1005 setTimeout(setLabelHighlighted.bind(null, true), 0);
1006 setTimeout(setLabelHighlighted.bind(null, false), 300);
1007 };
1008
1009 /**
1010 * Local storage key for the 'Overwrite original' setting.
1011 * @type {string}
1012 */
1013 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
1014
1015 /**
1016 * Local storage key for the number of times that
1017 * the overwrite info bubble has been displayed.
1018 * @type {string}
1019 */
1020 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
1021
1022 /**
1023 * Max number that the overwrite info bubble is shown.
1024 * @type {number}
1025 */
1026 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
1027
1028 /**
1029 * @return {boolean} True if 'Overwrite original' is set.
1030 * @private
1031 */
1032 SlideMode.prototype.shouldOverwriteOriginal_ = function() {
1033 return this.overwriteOriginal_.checked;
1034 };
1035
1036 /**
1037 * 'Overwrite original' checkbox handler.
1038 * @param {Event} event Event.
1039 * @private
1040 */
1041 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
1042 util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked);
1043 };
1044
1045 /**
1046 * Overwrite info bubble close handler.
1047 * @private
1048 */
1049 SlideMode.prototype.onCloseBubble_ = function() {
1050 this.bubble_.hidden = true;
1051 util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
1052 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
1053 };
1054
1055 // Slideshow
1056
1057 /**
1058 * Slideshow interval in ms.
1059 */
1060 SlideMode.SLIDESHOW_INTERVAL = 5000;
1061
1062 /**
1063 * First slideshow interval in ms. It should be shorter so that the user
1064 * is not guessing whether the button worked.
1065 */
1066 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
1067
1068 /**
1069 * Empirically determined duration of the fullscreen toggle animation.
1070 */
1071 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
1072
1073 /**
1074 * @return {boolean} True if the slideshow is on.
1075 * @private
1076 */
1077 SlideMode.prototype.isSlideshowOn_ = function() {
1078 return this.container_.hasAttribute('slideshow');
1079 };
1080
1081 /**
1082 * Start the slideshow.
1083 * @param {number=} opt_interval First interval in ms.
1084 * @param {Event=} opt_event Event.
1085 */
1086 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
1087 // Set the attribute early to prevent the toolbar from flashing when
1088 // the slideshow is being started from the mosaic view.
1089 this.container_.setAttribute('slideshow', 'playing');
1090
1091 if (this.active_) {
1092 this.stopEditing_();
1093 } else {
1094 // We are in the Mosaic mode. Toggle the mode but remember to return.
1095 this.leaveAfterSlideshow_ = true;
1096 this.toggleMode_(this.startSlideshow.bind(
1097 this, SlideMode.SLIDESHOW_INTERVAL, opt_event));
1098 return;
1099 }
1100
1101 if (opt_event) // Caused by user action, notify the Gallery.
1102 cr.dispatchSimpleEvent(this, 'useraction');
1103
1104 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
1105 if (!this.fullscreenBeforeSlideshow_) {
1106 // Wait until the zoom animation from the mosaic mode is done.
1107 setTimeout(this.toggleFullScreen_.bind(this),
1108 ImageView.ZOOM_ANIMATION_DURATION);
1109 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
1110 SlideMode.FULLSCREEN_TOGGLE_DELAY;
1111 }
1112
1113 this.resumeSlideshow_(opt_interval);
1114 };
1115
1116 /**
1117 * Stop the slideshow.
1118 * @param {Event=} opt_event Event.
1119 * @private
1120 */
1121 SlideMode.prototype.stopSlideshow_ = function(opt_event) {
1122 if (!this.isSlideshowOn_())
1123 return;
1124
1125 if (opt_event) // Caused by user action, notify the Gallery.
1126 cr.dispatchSimpleEvent(this, 'useraction');
1127
1128 this.pauseSlideshow_();
1129 this.container_.removeAttribute('slideshow');
1130
1131 // Do not restore fullscreen if we exited fullscreen while in slideshow.
1132 var fullscreen = util.isFullScreen(this.context_.appWindow);
1133 var toggleModeDelay = 0;
1134 if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
1135 this.toggleFullScreen_();
1136 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
1137 }
1138 if (this.leaveAfterSlideshow_) {
1139 this.leaveAfterSlideshow_ = false;
1140 setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
1141 }
1142 };
1143
1144 /**
1145 * @return {boolean} True if the slideshow is playing (not paused).
1146 * @private
1147 */
1148 SlideMode.prototype.isSlideshowPlaying_ = function() {
1149 return this.container_.getAttribute('slideshow') == 'playing';
1150 };
1151
1152 /**
1153 * Pause/resume the slideshow.
1154 * @private
1155 */
1156 SlideMode.prototype.toggleSlideshowPause_ = function() {
1157 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
1158 if (this.isSlideshowPlaying_()) {
1159 this.pauseSlideshow_();
1160 } else {
1161 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
1162 }
1163 };
1164
1165 /**
1166 * @param {number=} opt_interval Slideshow interval in ms.
1167 * @private
1168 */
1169 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
1170 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
1171
1172 if (this.slideShowTimeout_)
1173 clearTimeout(this.slideShowTimeout_);
1174
1175 this.slideShowTimeout_ = setTimeout(function() {
1176 this.slideShowTimeout_ = null;
1177 this.selectNext(1);
1178 }.bind(this),
1179 opt_interval || SlideMode.SLIDESHOW_INTERVAL);
1180 };
1181
1182 /**
1183 * Resume the slideshow.
1184 * @param {number=} opt_interval Slideshow interval in ms.
1185 * @private
1186 */
1187 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
1188 this.container_.setAttribute('slideshow', 'playing');
1189 this.scheduleNextSlide_(opt_interval);
1190 };
1191
1192 /**
1193 * Pause the slideshow.
1194 * @private
1195 */
1196 SlideMode.prototype.pauseSlideshow_ = function() {
1197 this.container_.setAttribute('slideshow', 'paused');
1198 if (this.slideShowTimeout_) {
1199 clearTimeout(this.slideShowTimeout_);
1200 this.slideShowTimeout_ = null;
1201 }
1202 };
1203
1204 /**
1205 * @return {boolean} True if the editor is active.
1206 */
1207 SlideMode.prototype.isEditing = function() {
1208 return this.container_.hasAttribute('editing');
1209 };
1210
1211 /**
1212 * Stop editing.
1213 * @private
1214 */
1215 SlideMode.prototype.stopEditing_ = function() {
1216 if (this.isEditing())
1217 this.toggleEditor();
1218 };
1219
1220 /**
1221 * Activate/deactivate editor.
1222 * @param {Event=} opt_event Event.
1223 */
1224 SlideMode.prototype.toggleEditor = function(opt_event) {
1225 if (opt_event) // Caused by user action, notify the Gallery.
1226 cr.dispatchSimpleEvent(this, 'useraction');
1227
1228 if (!this.active_) {
1229 this.toggleMode_(this.toggleEditor.bind(this));
1230 return;
1231 }
1232
1233 this.stopSlideshow_();
1234 if (!this.isEditing() && this.isShowingVideo_())
1235 return; // No editing for videos.
1236
1237 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
1238
1239 if (this.isEditing()) { // isEditing has just been flipped to a new value.
1240 if (this.context_.readonlyDirName) {
1241 this.editor_.getPrompt().showAt(
1242 'top', 'readonly_warning', 0, this.context_.readonlyDirName);
1243 }
1244 } else {
1245 this.editor_.getPrompt().hide();
1246 this.editor_.leaveModeGently();
1247 }
1248 };
1249
1250 /**
1251 * Prints the current item.
1252 * @private
1253 */
1254 SlideMode.prototype.print_ = function() {
1255 cr.dispatchSimpleEvent(this, 'useraction');
1256 window.print();
1257 };
1258
1259 /**
1260 * Display the error banner.
1261 * @param {string} message Message.
1262 * @private
1263 */
1264 SlideMode.prototype.showErrorBanner_ = function(message) {
1265 if (message) {
1266 this.errorBanner_.textContent = this.displayStringFunction_(message);
1267 }
1268 ImageUtil.setAttribute(this.container_, 'error', !!message);
1269 };
1270
1271 /**
1272 * Show/hide the busy spinner.
1273 *
1274 * @param {boolean} on True if show, false if hide.
1275 * @private
1276 */
1277 SlideMode.prototype.showSpinner_ = function(on) {
1278 if (this.spinnerTimer_) {
1279 clearTimeout(this.spinnerTimer_);
1280 this.spinnerTimer_ = null;
1281 }
1282
1283 if (on) {
1284 this.spinnerTimer_ = setTimeout(function() {
1285 this.spinnerTimer_ = null;
1286 ImageUtil.setAttribute(this.container_, 'spinner', true);
1287 }.bind(this), 1000);
1288 } else {
1289 ImageUtil.setAttribute(this.container_, 'spinner', false);
1290 }
1291 };
1292
1293 /**
1294 * @return {boolean} True if the current item is a video.
1295 * @private
1296 */
1297 SlideMode.prototype.isShowingVideo_ = function() {
1298 return !!this.imageView_.getVideo();
1299 };
1300
1301 /**
1302 * Overlay that handles swipe gestures. Changes to the next or previous file.
1303 * @param {function(number)} callback A callback accepting the swipe direction
1304 * (1 means left, -1 right).
1305 * @constructor
1306 * @implements {ImageBuffer.Overlay}
1307 */
1308 function SwipeOverlay(callback) {
1309 this.callback_ = callback;
1310 }
1311
1312 /**
1313 * Inherit ImageBuffer.Overlay.
1314 */
1315 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype;
1316
1317 /**
1318 * @param {number} x X pointer position.
1319 * @param {number} y Y pointer position.
1320 * @param {boolean} touch True if dragging caused by touch.
1321 * @return {function} The closure to call on drag.
1322 */
1323 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) {
1324 if (!touch)
1325 return null;
1326 var origin = x;
1327 var done = false;
1328 return function(x, y) {
1329 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) {
1330 this.callback_(1);
1331 done = true;
1332 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) {
1333 this.callback_(-1);
1334 done = true;
1335 }
1336 }.bind(this);
1337 };
1338
1339 /**
1340 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
1341 * horizontally it's considered as a swipe gesture (change the current image).
1342 */
1343 SwipeOverlay.SWIPE_THRESHOLD = 100;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698