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 | |
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; | |
OLD | NEW |