OLD | NEW |
| (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 this.onToolsVisibilityChanged_.bind(this)); | |
236 | |
237 this.editor_.getBuffer().addOverlay( | |
238 new SwipeOverlay(this.advanceManually.bind(this))); | |
239 }; | |
240 | |
241 /** | |
242 * Load items, display the selected item. | |
243 * @param {Rect} zoomFromRect Rectangle for zoom effect. | |
244 * @param {function} displayCallback Called when the image is displayed. | |
245 * @param {function} loadCallback Called when the image is displayed. | |
246 */ | |
247 SlideMode.prototype.enter = function( | |
248 zoomFromRect, displayCallback, loadCallback) { | |
249 this.sequenceDirection_ = 0; | |
250 this.sequenceLength_ = 0; | |
251 | |
252 var loadDone = function(loadType, delay) { | |
253 this.active_ = true; | |
254 | |
255 this.selectionModel_.addEventListener('change', this.onSelectionBound_); | |
256 this.dataModel_.addEventListener('splice', this.onSpliceBound_); | |
257 this.dataModel_.addEventListener('content', this.onContentBound_); | |
258 | |
259 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); | |
260 this.ribbon_.enable(); | |
261 | |
262 // Wait 1000ms after the animation is done, then prefetch the next image. | |
263 this.requestPrefetch(1, delay + 1000); | |
264 | |
265 if (loadCallback) loadCallback(); | |
266 }.bind(this); | |
267 | |
268 // The latest |leave| call might have left the image animating. Remove it. | |
269 this.unloadImage_(); | |
270 | |
271 if (this.getItemCount_() === 0) { | |
272 this.displayedIndex_ = -1; | |
273 //TODO(kaznacheev) Show this message in the grid mode too. | |
274 this.showErrorBanner_('GALLERY_NO_IMAGES'); | |
275 loadDone(); | |
276 } else { | |
277 // Remember the selection if it is empty or multiple. It will be restored | |
278 // in |leave| if the user did not changing the selection manually. | |
279 var currentSelection = this.selectionModel_.selectedIndexes; | |
280 if (currentSelection.length === 1) | |
281 this.savedSelection_ = null; | |
282 else | |
283 this.savedSelection_ = currentSelection; | |
284 | |
285 // Ensure valid single selection. | |
286 // Note that the SlideMode object is not listening to selection change yet. | |
287 this.select(Math.max(0, this.getSelectedIndex())); | |
288 this.displayedIndex_ = this.getSelectedIndex(); | |
289 | |
290 var selectedItem = this.getSelectedItem(); | |
291 // Show the selected item ASAP, then complete the initialization | |
292 // (loading the ribbon thumbnails can take some time). | |
293 this.metadataCache_.get(selectedItem.getEntry(), Gallery.METADATA_TYPE, | |
294 function(metadata) { | |
295 this.loadItem_(selectedItem.getEntry(), 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 * Handles changes in tools visibility, and if the header is dimmed, then | |
426 * requests disabling the draggable app region. | |
427 * | |
428 * @private | |
429 */ | |
430 SlideMode.prototype.onToolsVisibilityChanged_ = function() { | |
431 var headerDimmed = | |
432 this.document_.querySelector('.header').hasAttribute('dimmed'); | |
433 this.context_.onAppRegionChanged(!headerDimmed); | |
434 }; | |
435 | |
436 /** | |
437 * Change the selection. | |
438 * | |
439 * @param {number} index New selected index. | |
440 * @param {number=} opt_slideHint Slide animation direction (-1|1). | |
441 */ | |
442 SlideMode.prototype.select = function(index, opt_slideHint) { | |
443 this.slideHint_ = opt_slideHint; | |
444 this.selectionModel_.selectedIndex = index; | |
445 this.selectionModel_.leadIndex = index; | |
446 }; | |
447 | |
448 /** | |
449 * Load the selected item. | |
450 * | |
451 * @private | |
452 */ | |
453 SlideMode.prototype.loadSelectedItem_ = function() { | |
454 var slideHint = this.slideHint_; | |
455 this.slideHint_ = undefined; | |
456 | |
457 var index = this.getSelectedIndex(); | |
458 if (index === this.displayedIndex_) | |
459 return; // Do not reselect. | |
460 | |
461 var step = slideHint || (index - this.displayedIndex_); | |
462 | |
463 if (Math.abs(step) != 1) { | |
464 // Long leap, the sequence is broken, we have no good prefetch candidate. | |
465 this.sequenceDirection_ = 0; | |
466 this.sequenceLength_ = 0; | |
467 } else if (this.sequenceDirection_ === step) { | |
468 // Keeping going in sequence. | |
469 this.sequenceLength_++; | |
470 } else { | |
471 // Reversed the direction. Reset the counter. | |
472 this.sequenceDirection_ = step; | |
473 this.sequenceLength_ = 1; | |
474 } | |
475 | |
476 if (this.sequenceLength_ <= 1) { | |
477 // We have just broke the sequence. Touch the current image so that it stays | |
478 // in the cache longer. | |
479 this.imageView_.prefetch(this.imageView_.contentEntry_); | |
480 } | |
481 | |
482 this.displayedIndex_ = index; | |
483 | |
484 function shouldPrefetch(loadType, step, sequenceLength) { | |
485 // Never prefetch when selecting out of sequence. | |
486 if (Math.abs(step) != 1) | |
487 return false; | |
488 | |
489 // Never prefetch after a video load (decoding the next image can freeze | |
490 // the UI for a second or two). | |
491 if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE) | |
492 return false; | |
493 | |
494 // Always prefetch if the previous load was from cache. | |
495 if (loadType === ImageView.LOAD_TYPE_CACHED_FULL) | |
496 return true; | |
497 | |
498 // Prefetch if we have been going in the same direction for long enough. | |
499 return sequenceLength >= 3; | |
500 } | |
501 | |
502 var selectedItem = this.getSelectedItem(); | |
503 this.currentUniqueKey_++; | |
504 var selectedUniqueKey = this.currentUniqueKey_; | |
505 var onMetadata = function(metadata) { | |
506 // Discard, since another load has been invoked after this one. | |
507 if (selectedUniqueKey != this.currentUniqueKey_) return; | |
508 this.loadItem_(selectedItem.getEntry(), metadata, | |
509 new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()), | |
510 function() {} /* no displayCallback */, | |
511 function(loadType, delay) { | |
512 // Discard, since another load has been invoked after this one. | |
513 if (selectedUniqueKey != this.currentUniqueKey_) return; | |
514 if (shouldPrefetch(loadType, step, this.sequenceLength_)) { | |
515 this.requestPrefetch(step, delay); | |
516 } | |
517 if (this.isSlideshowPlaying_()) | |
518 this.scheduleNextSlide_(); | |
519 }.bind(this)); | |
520 }.bind(this); | |
521 this.metadataCache_.get( | |
522 selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata); | |
523 }; | |
524 | |
525 /** | |
526 * Unload the current image. | |
527 * | |
528 * @param {Rect} zoomToRect Rectangle for zoom effect. | |
529 * @private | |
530 */ | |
531 SlideMode.prototype.unloadImage_ = function(zoomToRect) { | |
532 this.imageView_.unload(zoomToRect); | |
533 this.container_.removeAttribute('video'); | |
534 }; | |
535 | |
536 /** | |
537 * Data model 'splice' event handler. | |
538 * @param {Event} event Event. | |
539 * @private | |
540 */ | |
541 SlideMode.prototype.onSplice_ = function(event) { | |
542 ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1); | |
543 | |
544 // Splice invalidates saved indices, drop the saved selection. | |
545 this.savedSelection_ = null; | |
546 | |
547 if (event.removed.length != 1) | |
548 return; | |
549 | |
550 // Delay the selection to let the ribbon splice handler work first. | |
551 setTimeout(function() { | |
552 if (event.index < this.dataModel_.length) { | |
553 // There is the next item, select it. | |
554 // The next item is now at the same index as the removed one, so we need | |
555 // to correct displayIndex_ so that loadSelectedItem_ does not think | |
556 // we are re-selecting the same item (and does right-to-left slide-in | |
557 // animation). | |
558 this.displayedIndex_ = event.index - 1; | |
559 this.select(event.index); | |
560 } else if (this.dataModel_.length) { | |
561 // Removed item is the rightmost, but there are more items. | |
562 this.select(event.index - 1); // Select the new last index. | |
563 } else { | |
564 // No items left. Unload the image and show the banner. | |
565 this.commitItem_(function() { | |
566 this.unloadImage_(); | |
567 this.showErrorBanner_('GALLERY_NO_IMAGES'); | |
568 }.bind(this)); | |
569 } | |
570 }.bind(this), 0); | |
571 }; | |
572 | |
573 /** | |
574 * @param {number} direction -1 for left, 1 for right. | |
575 * @return {number} Next index in the given direction, with wrapping. | |
576 * @private | |
577 */ | |
578 SlideMode.prototype.getNextSelectedIndex_ = function(direction) { | |
579 function advance(index, limit) { | |
580 index += (direction > 0 ? 1 : -1); | |
581 if (index < 0) | |
582 return limit - 1; | |
583 if (index === limit) | |
584 return 0; | |
585 return index; | |
586 } | |
587 | |
588 // If the saved selection is multiple the Slideshow should cycle through | |
589 // the saved selection. | |
590 if (this.isSlideshowOn_() && | |
591 this.savedSelection_ && this.savedSelection_.length > 1) { | |
592 var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()), | |
593 this.savedSelection_.length); | |
594 return this.savedSelection_[pos]; | |
595 } else { | |
596 return advance(this.getSelectedIndex(), this.getItemCount_()); | |
597 } | |
598 }; | |
599 | |
600 /** | |
601 * Advance the selection based on the pressed key ID. | |
602 * @param {string} keyID Key identifier. | |
603 */ | |
604 SlideMode.prototype.advanceWithKeyboard = function(keyID) { | |
605 this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1); | |
606 }; | |
607 | |
608 /** | |
609 * Advance the selection as a result of a user action (as opposed to an | |
610 * automatic change in the slideshow mode). | |
611 * @param {number} direction -1 for left, 1 for right. | |
612 */ | |
613 SlideMode.prototype.advanceManually = function(direction) { | |
614 if (this.isSlideshowPlaying_()) { | |
615 this.pauseSlideshow_(); | |
616 cr.dispatchSimpleEvent(this, 'useraction'); | |
617 } | |
618 this.selectNext(direction); | |
619 }; | |
620 | |
621 /** | |
622 * Select the next item. | |
623 * @param {number} direction -1 for left, 1 for right. | |
624 */ | |
625 SlideMode.prototype.selectNext = function(direction) { | |
626 this.select(this.getNextSelectedIndex_(direction), direction); | |
627 }; | |
628 | |
629 /** | |
630 * Select the first item. | |
631 */ | |
632 SlideMode.prototype.selectFirst = function() { | |
633 this.select(0); | |
634 }; | |
635 | |
636 /** | |
637 * Select the last item. | |
638 */ | |
639 SlideMode.prototype.selectLast = function() { | |
640 this.select(this.getItemCount_() - 1); | |
641 }; | |
642 | |
643 // Loading/unloading | |
644 | |
645 /** | |
646 * Load and display an item. | |
647 * | |
648 * @param {FileEntry} entry Item entry to be loaded. | |
649 * @param {Object} metadata Item metadata. | |
650 * @param {Object} effect Transition effect object. | |
651 * @param {function} displayCallback Called when the image is displayed | |
652 * (which can happen before the image load due to caching). | |
653 * @param {function} loadCallback Called when the image is fully loaded. | |
654 * @private | |
655 */ | |
656 SlideMode.prototype.loadItem_ = function( | |
657 entry, metadata, effect, displayCallback, loadCallback) { | |
658 this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata); | |
659 | |
660 this.showSpinner_(true); | |
661 | |
662 var loadDone = function(loadType, delay, error) { | |
663 var video = this.isShowingVideo_(); | |
664 ImageUtil.setAttribute(this.container_, 'video', video); | |
665 | |
666 this.showSpinner_(false); | |
667 if (loadType === ImageView.LOAD_TYPE_ERROR) { | |
668 // if we have a specific error, then display it | |
669 if (error) { | |
670 this.showErrorBanner_(error); | |
671 } else { | |
672 // otherwise try to infer general error | |
673 this.showErrorBanner_( | |
674 video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR'); | |
675 } | |
676 } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) { | |
677 this.showErrorBanner_( | |
678 video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE'); | |
679 } | |
680 | |
681 if (video) { | |
682 // The editor toolbar does not make sense for video, hide it. | |
683 this.stopEditing_(); | |
684 this.mediaControls_.attachMedia(this.imageView_.getVideo()); | |
685 | |
686 // TODO(kaznacheev): Add metrics for video playback. | |
687 } else { | |
688 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); | |
689 | |
690 var toMillions = function(number) { | |
691 return Math.round(number / (1000 * 1000)); | |
692 }; | |
693 | |
694 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), | |
695 toMillions(metadata.filesystem.size)); | |
696 | |
697 var canvas = this.imageView_.getCanvas(); | |
698 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), | |
699 toMillions(canvas.width * canvas.height)); | |
700 | |
701 var extIndex = entry.name.lastIndexOf('.'); | |
702 var ext = extIndex < 0 ? '' : | |
703 entry.name.substr(extIndex + 1).toLowerCase(); | |
704 if (ext === 'jpeg') ext = 'jpg'; | |
705 ImageUtil.metrics.recordEnum( | |
706 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); | |
707 } | |
708 | |
709 // Enable or disable buttons for editing and printing. | |
710 if (video || error) { | |
711 this.editButton_.setAttribute('disabled', ''); | |
712 this.printButton_.setAttribute('disabled', ''); | |
713 } else { | |
714 this.editButton_.removeAttribute('disabled'); | |
715 this.printButton_.removeAttribute('disabled'); | |
716 } | |
717 | |
718 // For once edited image, disallow the 'overwrite' setting change. | |
719 ImageUtil.setAttribute(this.options_, 'saved', | |
720 !this.getSelectedItem().isOriginal()); | |
721 | |
722 util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY, | |
723 function(value) { | |
724 var times = typeof value === 'string' ? parseInt(value, 10) : 0; | |
725 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) { | |
726 this.bubble_.hidden = false; | |
727 if (this.isEditing()) { | |
728 util.platform.setPreference( | |
729 SlideMode.OVERWRITE_BUBBLE_KEY, times + 1); | |
730 } | |
731 } | |
732 }.bind(this)); | |
733 | |
734 loadCallback(loadType, delay); | |
735 }.bind(this); | |
736 | |
737 var displayDone = function() { | |
738 cr.dispatchSimpleEvent(this, 'image-displayed'); | |
739 displayCallback(); | |
740 }.bind(this); | |
741 | |
742 this.editor_.openSession(entry, metadata, effect, | |
743 this.saveCurrentImage_.bind(this), displayDone, loadDone); | |
744 }; | |
745 | |
746 /** | |
747 * Commit changes to the current item and reset all messages/indicators. | |
748 * | |
749 * @param {function} callback Callback. | |
750 * @private | |
751 */ | |
752 SlideMode.prototype.commitItem_ = function(callback) { | |
753 this.showSpinner_(false); | |
754 this.showErrorBanner_(false); | |
755 this.editor_.getPrompt().hide(); | |
756 | |
757 // Detach any media attached to the controls. | |
758 if (this.mediaControls_.getMedia()) | |
759 this.mediaControls_.detachMedia(); | |
760 | |
761 // If showing the video, then pause it. Note, that it may not be attached | |
762 // to the media controls yet. | |
763 if (this.isShowingVideo_()) { | |
764 this.imageView_.getVideo().pause(); | |
765 // Force stop downloading, if uncached on Drive. | |
766 this.imageView_.getVideo().src = ''; | |
767 this.imageView_.getVideo().load(); | |
768 } | |
769 | |
770 this.editor_.closeSession(callback); | |
771 }; | |
772 | |
773 /** | |
774 * Request a prefetch for the next image. | |
775 * | |
776 * @param {number} direction -1 or 1. | |
777 * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image | |
778 * loading from disrupting the animation that might be still in progress. | |
779 */ | |
780 SlideMode.prototype.requestPrefetch = function(direction, delay) { | |
781 if (this.getItemCount_() <= 1) return; | |
782 | |
783 var index = this.getNextSelectedIndex_(direction); | |
784 var nextItemEntry = this.getItem(index).getEntry(); | |
785 this.imageView_.prefetch(nextItemEntry, delay); | |
786 }; | |
787 | |
788 // Event handlers. | |
789 | |
790 /** | |
791 * Unload handler, to be called from the top frame. | |
792 * @param {boolean} exiting True if the app is exiting. | |
793 */ | |
794 SlideMode.prototype.onUnload = function(exiting) { | |
795 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { | |
796 this.mediaControls_.savePosition(exiting); | |
797 } | |
798 }; | |
799 | |
800 /** | |
801 * Click handler for the image container. | |
802 * | |
803 * @param {Event} event Mouse click event. | |
804 * @private | |
805 */ | |
806 SlideMode.prototype.onClick_ = function(event) { | |
807 if (!this.isShowingVideo_() || !this.mediaControls_.getMedia()) | |
808 return; | |
809 if (event.ctrlKey) { | |
810 this.mediaControls_.toggleLoopedModeWithFeedback(true); | |
811 if (!this.mediaControls_.isPlaying()) | |
812 this.mediaControls_.togglePlayStateWithFeedback(); | |
813 } else { | |
814 this.mediaControls_.togglePlayStateWithFeedback(); | |
815 } | |
816 }; | |
817 | |
818 /** | |
819 * Click handler for the entire document. | |
820 * @param {Event} e Mouse click event. | |
821 * @private | |
822 */ | |
823 SlideMode.prototype.onDocumentClick_ = function(e) { | |
824 // Close the bubble if clicked outside of it and if it is visible. | |
825 if (!this.bubble_.contains(e.target) && | |
826 !this.editButton_.contains(e.target) && | |
827 !this.arrowLeft_.contains(e.target) && | |
828 !this.arrowRight_.contains(e.target) && | |
829 !this.bubble_.hidden) { | |
830 this.bubble_.hidden = true; | |
831 } | |
832 }; | |
833 | |
834 /** | |
835 * Keydown handler. | |
836 * | |
837 * @param {Event} event Event. | |
838 * @return {boolean} True if handled. | |
839 */ | |
840 SlideMode.prototype.onKeyDown = function(event) { | |
841 var keyID = util.getKeyModifiers(event) + event.keyIdentifier; | |
842 | |
843 if (this.isSlideshowOn_()) { | |
844 switch (keyID) { | |
845 case 'U+001B': // Escape exits the slideshow. | |
846 this.stopSlideshow_(event); | |
847 break; | |
848 | |
849 case 'U+0020': // Space pauses/resumes the slideshow. | |
850 this.toggleSlideshowPause_(); | |
851 break; | |
852 | |
853 case 'Up': | |
854 case 'Down': | |
855 case 'Left': | |
856 case 'Right': | |
857 this.advanceWithKeyboard(keyID); | |
858 break; | |
859 } | |
860 return true; // Consume all keystrokes in the slideshow mode. | |
861 } | |
862 | |
863 if (this.isEditing() && this.editor_.onKeyDown(event)) | |
864 return true; | |
865 | |
866 switch (keyID) { | |
867 case 'U+0020': // Space toggles the video playback. | |
868 if (this.isShowingVideo_() && this.mediaControls_.getMedia()) | |
869 this.mediaControls_.togglePlayStateWithFeedback(); | |
870 break; | |
871 | |
872 case 'Ctrl-U+0050': // Ctrl+'p' prints the current image. | |
873 if (!this.printButton_.hasAttribute('disabled')) | |
874 this.print_(); | |
875 break; | |
876 | |
877 case 'U+0045': // 'e' toggles the editor. | |
878 if (!this.editButton_.hasAttribute('disabled')) | |
879 this.toggleEditor(event); | |
880 break; | |
881 | |
882 case 'U+001B': // Escape | |
883 if (!this.isEditing()) | |
884 return false; // Not handled. | |
885 this.toggleEditor(event); | |
886 break; | |
887 | |
888 case 'Home': | |
889 this.selectFirst(); | |
890 break; | |
891 case 'End': | |
892 this.selectLast(); | |
893 break; | |
894 case 'Up': | |
895 case 'Down': | |
896 case 'Left': | |
897 case 'Right': | |
898 this.advanceWithKeyboard(keyID); | |
899 break; | |
900 | |
901 default: return false; | |
902 } | |
903 | |
904 return true; | |
905 }; | |
906 | |
907 /** | |
908 * Resize handler. | |
909 * @private | |
910 */ | |
911 SlideMode.prototype.onResize_ = function() { | |
912 this.viewport_.sizeByFrameAndFit(this.container_); | |
913 this.viewport_.repaint(); | |
914 }; | |
915 | |
916 /** | |
917 * Update thumbnails. | |
918 */ | |
919 SlideMode.prototype.updateThumbnails = function() { | |
920 this.ribbon_.reset(); | |
921 if (this.active_) | |
922 this.ribbon_.redraw(); | |
923 }; | |
924 | |
925 // Saving | |
926 | |
927 /** | |
928 * Save the current image to a file. | |
929 * | |
930 * @param {function} callback Callback. | |
931 * @private | |
932 */ | |
933 SlideMode.prototype.saveCurrentImage_ = function(callback) { | |
934 var item = this.getSelectedItem(); | |
935 var oldEntry = item.getEntry(); | |
936 var canvas = this.imageView_.getCanvas(); | |
937 | |
938 this.showSpinner_(true); | |
939 var metadataEncoder = ImageEncoder.encodeMetadata( | |
940 this.selectedImageMetadata_.media, canvas, 1 /* quality */); | |
941 | |
942 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( | |
943 metadataEncoder.getMetadata(), this.selectedImageMetadata_); | |
944 | |
945 item.saveToFile( | |
946 this.context_.saveDirEntry, | |
947 this.shouldOverwriteOriginal_(), | |
948 canvas, | |
949 metadataEncoder, | |
950 function(success) { | |
951 // TODO(kaznacheev): Implement write error handling. | |
952 // Until then pretend that the save succeeded. | |
953 this.showSpinner_(false); | |
954 this.flashSavedLabel_(); | |
955 | |
956 var event = new Event('content'); | |
957 event.item = item; | |
958 event.oldEntry = oldEntry; | |
959 event.metadata = this.selectedImageMetadata_; | |
960 this.dataModel_.dispatchEvent(event); | |
961 | |
962 // Allow changing the 'Overwrite original' setting only if the user | |
963 // used Undo to restore the original image AND it is not a copy. | |
964 // Otherwise lock the setting in its current state. | |
965 var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal(); | |
966 ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite); | |
967 | |
968 if (this.imageView_.getContentRevision() === 1) { // First edit. | |
969 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); | |
970 } | |
971 | |
972 if (!util.isSameEntry(oldEntry, item.getEntry())) { | |
973 this.dataModel_.splice( | |
974 this.getSelectedIndex(), 0, new Gallery.Item(oldEntry)); | |
975 // The ribbon will ignore the splice above and redraw after the | |
976 // select call below (while being obscured by the Editor toolbar, | |
977 // so there is no need for nice animation here). | |
978 // SlideMode will ignore the selection change as the displayed item | |
979 // index has not changed. | |
980 this.select(++this.displayedIndex_); | |
981 } | |
982 callback(); | |
983 cr.dispatchSimpleEvent(this, 'image-saved'); | |
984 }.bind(this)); | |
985 }; | |
986 | |
987 /** | |
988 * Update caches when the selected item has been renamed. | |
989 * @param {Event} event Event. | |
990 * @private | |
991 */ | |
992 SlideMode.prototype.onContentChange_ = function(event) { | |
993 var newEntry = event.item.getEntry(); | |
994 if (util.isSameEntry(newEntry, event.oldEntry)) | |
995 this.imageView_.changeEntry(newEntry); | |
996 this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE); | |
997 }; | |
998 | |
999 /** | |
1000 * Flash 'Saved' label briefly to indicate that the image has been saved. | |
1001 * @private | |
1002 */ | |
1003 SlideMode.prototype.flashSavedLabel_ = function() { | |
1004 var setLabelHighlighted = | |
1005 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); | |
1006 setTimeout(setLabelHighlighted.bind(null, true), 0); | |
1007 setTimeout(setLabelHighlighted.bind(null, false), 300); | |
1008 }; | |
1009 | |
1010 /** | |
1011 * Local storage key for the 'Overwrite original' setting. | |
1012 * @type {string} | |
1013 */ | |
1014 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original'; | |
1015 | |
1016 /** | |
1017 * Local storage key for the number of times that | |
1018 * the overwrite info bubble has been displayed. | |
1019 * @type {string} | |
1020 */ | |
1021 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble'; | |
1022 | |
1023 /** | |
1024 * Max number that the overwrite info bubble is shown. | |
1025 * @type {number} | |
1026 */ | |
1027 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5; | |
1028 | |
1029 /** | |
1030 * @return {boolean} True if 'Overwrite original' is set. | |
1031 * @private | |
1032 */ | |
1033 SlideMode.prototype.shouldOverwriteOriginal_ = function() { | |
1034 return this.overwriteOriginal_.checked; | |
1035 }; | |
1036 | |
1037 /** | |
1038 * 'Overwrite original' checkbox handler. | |
1039 * @param {Event} event Event. | |
1040 * @private | |
1041 */ | |
1042 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) { | |
1043 util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked); | |
1044 }; | |
1045 | |
1046 /** | |
1047 * Overwrite info bubble close handler. | |
1048 * @private | |
1049 */ | |
1050 SlideMode.prototype.onCloseBubble_ = function() { | |
1051 this.bubble_.hidden = true; | |
1052 util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY, | |
1053 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES); | |
1054 }; | |
1055 | |
1056 // Slideshow | |
1057 | |
1058 /** | |
1059 * Slideshow interval in ms. | |
1060 */ | |
1061 SlideMode.SLIDESHOW_INTERVAL = 5000; | |
1062 | |
1063 /** | |
1064 * First slideshow interval in ms. It should be shorter so that the user | |
1065 * is not guessing whether the button worked. | |
1066 */ | |
1067 SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000; | |
1068 | |
1069 /** | |
1070 * Empirically determined duration of the fullscreen toggle animation. | |
1071 */ | |
1072 SlideMode.FULLSCREEN_TOGGLE_DELAY = 500; | |
1073 | |
1074 /** | |
1075 * @return {boolean} True if the slideshow is on. | |
1076 * @private | |
1077 */ | |
1078 SlideMode.prototype.isSlideshowOn_ = function() { | |
1079 return this.container_.hasAttribute('slideshow'); | |
1080 }; | |
1081 | |
1082 /** | |
1083 * Start the slideshow. | |
1084 * @param {number=} opt_interval First interval in ms. | |
1085 * @param {Event=} opt_event Event. | |
1086 */ | |
1087 SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) { | |
1088 // Set the attribute early to prevent the toolbar from flashing when | |
1089 // the slideshow is being started from the mosaic view. | |
1090 this.container_.setAttribute('slideshow', 'playing'); | |
1091 | |
1092 if (this.active_) { | |
1093 this.stopEditing_(); | |
1094 } else { | |
1095 // We are in the Mosaic mode. Toggle the mode but remember to return. | |
1096 this.leaveAfterSlideshow_ = true; | |
1097 this.toggleMode_(this.startSlideshow.bind( | |
1098 this, SlideMode.SLIDESHOW_INTERVAL, opt_event)); | |
1099 return; | |
1100 } | |
1101 | |
1102 if (opt_event) // Caused by user action, notify the Gallery. | |
1103 cr.dispatchSimpleEvent(this, 'useraction'); | |
1104 | |
1105 this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow); | |
1106 if (!this.fullscreenBeforeSlideshow_) { | |
1107 // Wait until the zoom animation from the mosaic mode is done. | |
1108 setTimeout(this.toggleFullScreen_.bind(this), | |
1109 ImageView.ZOOM_ANIMATION_DURATION); | |
1110 opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) + | |
1111 SlideMode.FULLSCREEN_TOGGLE_DELAY; | |
1112 } | |
1113 | |
1114 this.resumeSlideshow_(opt_interval); | |
1115 }; | |
1116 | |
1117 /** | |
1118 * Stop the slideshow. | |
1119 * @param {Event=} opt_event Event. | |
1120 * @private | |
1121 */ | |
1122 SlideMode.prototype.stopSlideshow_ = function(opt_event) { | |
1123 if (!this.isSlideshowOn_()) | |
1124 return; | |
1125 | |
1126 if (opt_event) // Caused by user action, notify the Gallery. | |
1127 cr.dispatchSimpleEvent(this, 'useraction'); | |
1128 | |
1129 this.pauseSlideshow_(); | |
1130 this.container_.removeAttribute('slideshow'); | |
1131 | |
1132 // Do not restore fullscreen if we exited fullscreen while in slideshow. | |
1133 var fullscreen = util.isFullScreen(this.context_.appWindow); | |
1134 var toggleModeDelay = 0; | |
1135 if (!this.fullscreenBeforeSlideshow_ && fullscreen) { | |
1136 this.toggleFullScreen_(); | |
1137 toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY; | |
1138 } | |
1139 if (this.leaveAfterSlideshow_) { | |
1140 this.leaveAfterSlideshow_ = false; | |
1141 setTimeout(this.toggleMode_.bind(this), toggleModeDelay); | |
1142 } | |
1143 }; | |
1144 | |
1145 /** | |
1146 * @return {boolean} True if the slideshow is playing (not paused). | |
1147 * @private | |
1148 */ | |
1149 SlideMode.prototype.isSlideshowPlaying_ = function() { | |
1150 return this.container_.getAttribute('slideshow') === 'playing'; | |
1151 }; | |
1152 | |
1153 /** | |
1154 * Pause/resume the slideshow. | |
1155 * @private | |
1156 */ | |
1157 SlideMode.prototype.toggleSlideshowPause_ = function() { | |
1158 cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools. | |
1159 if (this.isSlideshowPlaying_()) { | |
1160 this.pauseSlideshow_(); | |
1161 } else { | |
1162 this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST); | |
1163 } | |
1164 }; | |
1165 | |
1166 /** | |
1167 * @param {number=} opt_interval Slideshow interval in ms. | |
1168 * @private | |
1169 */ | |
1170 SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) { | |
1171 console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state'); | |
1172 | |
1173 if (this.slideShowTimeout_) | |
1174 clearTimeout(this.slideShowTimeout_); | |
1175 | |
1176 this.slideShowTimeout_ = setTimeout(function() { | |
1177 this.slideShowTimeout_ = null; | |
1178 this.selectNext(1); | |
1179 }.bind(this), | |
1180 opt_interval || SlideMode.SLIDESHOW_INTERVAL); | |
1181 }; | |
1182 | |
1183 /** | |
1184 * Resume the slideshow. | |
1185 * @param {number=} opt_interval Slideshow interval in ms. | |
1186 * @private | |
1187 */ | |
1188 SlideMode.prototype.resumeSlideshow_ = function(opt_interval) { | |
1189 this.container_.setAttribute('slideshow', 'playing'); | |
1190 this.scheduleNextSlide_(opt_interval); | |
1191 }; | |
1192 | |
1193 /** | |
1194 * Pause the slideshow. | |
1195 * @private | |
1196 */ | |
1197 SlideMode.prototype.pauseSlideshow_ = function() { | |
1198 this.container_.setAttribute('slideshow', 'paused'); | |
1199 if (this.slideShowTimeout_) { | |
1200 clearTimeout(this.slideShowTimeout_); | |
1201 this.slideShowTimeout_ = null; | |
1202 } | |
1203 }; | |
1204 | |
1205 /** | |
1206 * @return {boolean} True if the editor is active. | |
1207 */ | |
1208 SlideMode.prototype.isEditing = function() { | |
1209 return this.container_.hasAttribute('editing'); | |
1210 }; | |
1211 | |
1212 /** | |
1213 * Stop editing. | |
1214 * @private | |
1215 */ | |
1216 SlideMode.prototype.stopEditing_ = function() { | |
1217 if (this.isEditing()) | |
1218 this.toggleEditor(); | |
1219 }; | |
1220 | |
1221 /** | |
1222 * Activate/deactivate editor. | |
1223 * @param {Event=} opt_event Event. | |
1224 */ | |
1225 SlideMode.prototype.toggleEditor = function(opt_event) { | |
1226 if (opt_event) // Caused by user action, notify the Gallery. | |
1227 cr.dispatchSimpleEvent(this, 'useraction'); | |
1228 | |
1229 if (!this.active_) { | |
1230 this.toggleMode_(this.toggleEditor.bind(this)); | |
1231 return; | |
1232 } | |
1233 | |
1234 this.stopSlideshow_(); | |
1235 if (!this.isEditing() && this.isShowingVideo_()) | |
1236 return; // No editing for videos. | |
1237 | |
1238 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing()); | |
1239 | |
1240 if (this.isEditing()) { // isEditing has just been flipped to a new value. | |
1241 if (this.context_.readonlyDirName) { | |
1242 this.editor_.getPrompt().showAt( | |
1243 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName); | |
1244 } | |
1245 } else { | |
1246 this.editor_.getPrompt().hide(); | |
1247 this.editor_.leaveModeGently(); | |
1248 } | |
1249 }; | |
1250 | |
1251 /** | |
1252 * Prints the current item. | |
1253 * @private | |
1254 */ | |
1255 SlideMode.prototype.print_ = function() { | |
1256 cr.dispatchSimpleEvent(this, 'useraction'); | |
1257 window.print(); | |
1258 }; | |
1259 | |
1260 /** | |
1261 * Display the error banner. | |
1262 * @param {string} message Message. | |
1263 * @private | |
1264 */ | |
1265 SlideMode.prototype.showErrorBanner_ = function(message) { | |
1266 if (message) { | |
1267 this.errorBanner_.textContent = this.displayStringFunction_(message); | |
1268 } | |
1269 ImageUtil.setAttribute(this.container_, 'error', !!message); | |
1270 }; | |
1271 | |
1272 /** | |
1273 * Show/hide the busy spinner. | |
1274 * | |
1275 * @param {boolean} on True if show, false if hide. | |
1276 * @private | |
1277 */ | |
1278 SlideMode.prototype.showSpinner_ = function(on) { | |
1279 if (this.spinnerTimer_) { | |
1280 clearTimeout(this.spinnerTimer_); | |
1281 this.spinnerTimer_ = null; | |
1282 } | |
1283 | |
1284 if (on) { | |
1285 this.spinnerTimer_ = setTimeout(function() { | |
1286 this.spinnerTimer_ = null; | |
1287 ImageUtil.setAttribute(this.container_, 'spinner', true); | |
1288 }.bind(this), 1000); | |
1289 } else { | |
1290 ImageUtil.setAttribute(this.container_, 'spinner', false); | |
1291 } | |
1292 }; | |
1293 | |
1294 /** | |
1295 * @return {boolean} True if the current item is a video. | |
1296 * @private | |
1297 */ | |
1298 SlideMode.prototype.isShowingVideo_ = function() { | |
1299 return !!this.imageView_.getVideo(); | |
1300 }; | |
1301 | |
1302 /** | |
1303 * Overlay that handles swipe gestures. Changes to the next or previous file. | |
1304 * @param {function(number)} callback A callback accepting the swipe direction | |
1305 * (1 means left, -1 right). | |
1306 * @constructor | |
1307 * @implements {ImageBuffer.Overlay} | |
1308 */ | |
1309 function SwipeOverlay(callback) { | |
1310 this.callback_ = callback; | |
1311 } | |
1312 | |
1313 /** | |
1314 * Inherit ImageBuffer.Overlay. | |
1315 */ | |
1316 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; | |
1317 | |
1318 /** | |
1319 * @param {number} x X pointer position. | |
1320 * @param {number} y Y pointer position. | |
1321 * @param {boolean} touch True if dragging caused by touch. | |
1322 * @return {function} The closure to call on drag. | |
1323 */ | |
1324 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { | |
1325 if (!touch) | |
1326 return null; | |
1327 var origin = x; | |
1328 var done = false; | |
1329 return function(x, y) { | |
1330 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { | |
1331 this.callback_(1); | |
1332 done = true; | |
1333 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { | |
1334 this.callback_(-1); | |
1335 done = true; | |
1336 } | |
1337 }.bind(this); | |
1338 }; | |
1339 | |
1340 /** | |
1341 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD | |
1342 * horizontally it's considered as a swipe gesture (change the current image). | |
1343 */ | |
1344 SwipeOverlay.SWIPE_THRESHOLD = 100; | |
OLD | NEW |