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 /** | |
6 * Slide mode displays a single image and has a set of controls to navigate | |
7 * between the images and to edit an image. | |
8 * | |
9 * @param {Element} container Container element. | |
10 * @param {Element} toolbar Toolbar element. | |
11 * @param {ImageEditor.Prompt} prompt Prompt. | |
12 * @param {Object} context Context. | |
13 * @param {function(string):string} displayStringFunction String formatting | |
14 * function. | |
15 * @constructor | |
16 */ | |
17 function SlideMode(container, toolbar, prompt, context, displayStringFunction) { | |
18 this.container_ = container; | |
19 this.toolbar_ = toolbar; | |
20 this.document_ = container.ownerDocument; | |
21 this.prompt_ = prompt; | |
22 this.context_ = context; | |
23 this.metadataCache_ = context.metadataCache; | |
24 this.displayStringFunction_ = displayStringFunction; | |
25 | |
26 this.initListeners_(); | |
27 this.initDom_(); | |
28 } | |
29 | |
30 /** | |
31 * SlideMode extends cr.EventTarget. | |
32 */ | |
33 SlideMode.prototype.__proto__ = cr.EventTarget.prototype; | |
34 | |
35 /** | |
36 * List of available editor modes. | |
37 * @type {Array.<ImageEditor.Mode>} | |
38 */ | |
39 SlideMode.editorModes = [ | |
40 new ImageEditor.Mode.InstantAutofix(), | |
41 new ImageEditor.Mode.Crop(), | |
42 new ImageEditor.Mode.Exposure(), | |
43 new ImageEditor.Mode.OneClick('rotate_left', new Command.Rotate(-1)), | |
44 new ImageEditor.Mode.OneClick('rotate_right', new Command.Rotate(1)) | |
45 ]; | |
46 | |
47 /** | |
48 * Initialize the listeners. | |
49 * @private | |
50 */ | |
51 SlideMode.prototype.initListeners_ = function() { | |
52 var win = this.document_.defaultView; | |
53 | |
54 this.onBeforeUnloadBound_ = this.onBeforeUnload_.bind(this); | |
55 win.top.addEventListener('beforeunload', this.onBeforeUnloadBound_); | |
56 | |
57 win.addEventListener('unload', this.onUnload_.bind(this)); | |
58 | |
59 // We need to listen to the top window 'unload' and 'beforeunload' because | |
60 // the Gallery iframe does not get notified if the tab is closed. | |
61 this.onTopUnloadBound_ = this.onTopUnload_.bind(this); | |
62 win.top.addEventListener('unload', this.onTopUnloadBound_); | |
63 | |
64 win.addEventListener('resize', this.onResize_.bind(this), false); | |
65 }; | |
66 | |
67 /** | |
68 * Initialize the UI. | |
69 * @private | |
70 */ | |
71 SlideMode.prototype.initDom_ = function() { | |
72 // Container for displayed image or video. | |
73 this.imageContainer_ = createChild( | |
74 this.document_.querySelector('.content'), 'image-container'); | |
75 this.imageContainer_.addEventListener('click', this.onClick_.bind(this)); | |
76 | |
77 // Overwrite options and info bubble. | |
78 this.options_ = createChild( | |
79 this.toolbar_.querySelector('.filename-spacer'), 'options'); | |
80 | |
81 this.savedLabel_ = createChild(this.options_, 'saved'); | |
82 this.savedLabel_.textContent = this.displayStringFunction_('saved'); | |
83 | |
84 var overwriteOriginalBox = createChild(this.options_, 'overwrite-original'); | |
85 | |
86 this.overwriteOriginal_ = createChild( | |
87 overwriteOriginalBox, 'common white', 'input'); | |
88 this.overwriteOriginal_.type = 'checkbox'; | |
89 this.overwriteOriginal_.id = 'overwrite-checkbox'; | |
90 this.overwriteOriginal_.checked = this.shouldOverwriteOriginal_(); | |
91 this.overwriteOriginal_.addEventListener('click', | |
92 this.onOverwriteOriginalClick_.bind(this)); | |
93 | |
94 var overwriteLabel = createChild( | |
95 overwriteOriginalBox, undefined, 'label'); | |
dgozman
2012/08/17 09:48:37
better pass '' instead of undefined
Vladislav Kaznacheev
2012/08/20 07:25:34
Done.
| |
96 overwriteLabel.textContent = | |
97 this.displayStringFunction_('overwrite_original'); | |
98 overwriteLabel.setAttribute('for', 'overwrite-checkbox'); | |
99 | |
100 this.bubble_ = createChild(this.toolbar_, 'bubble'); | |
101 this.bubble_.hidden = true; | |
102 | |
103 var bubbleContent = createChild(this.bubble_); | |
104 bubbleContent.innerHTML = this.displayStringFunction_('overwrite_bubble'); | |
105 | |
106 createChild(this.bubble_, 'pointer bottom', 'span'); | |
107 | |
108 var bubbleClose = createChild(this.bubble_, 'close-x'); | |
109 bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this)); | |
110 | |
111 // Video player controls. | |
112 this.mediaSpacer_ = createChild(this.container_, 'video-controls-spacer'); | |
113 this.mediaToolbar_ = createChild(this.mediaSpacer_, 'tool'); | |
114 this.mediaControls_ = new VideoControls( | |
115 this.mediaToolbar_, | |
116 this.showErrorBanner_.bind(this, 'VIDEO_ERROR'), | |
117 Gallery.toggleFullscreen, | |
118 this.container_); | |
119 | |
120 // Ribbon and related controls. | |
121 this.arrowBox_ = createChild(this.container_, 'arrow-box'); | |
122 | |
123 this.arrowLeft_ = createChild(this.arrowBox_, 'arrow left tool dimmable'); | |
124 this.arrowLeft_.addEventListener('click', | |
125 this.selectNext.bind(this, -1, null)); | |
126 createChild(this.arrowLeft_); | |
127 | |
128 createChild(this.arrowBox_, 'arrow-spacer'); | |
129 | |
130 this.arrowRight_ = createChild(this.arrowBox_, 'arrow right tool dimmable'); | |
131 this.arrowRight_.addEventListener('click', | |
132 this.selectNext.bind(this, 1, null)); | |
133 createChild(this.arrowRight_); | |
134 | |
135 this.ribbonSpacer_ = createChild(this.toolbar_, 'ribbon-spacer'); | |
136 | |
137 // Error indicator. | |
138 var errorWrapper = createChild(this.container_, 'prompt-wrapper'); | |
139 errorWrapper.setAttribute('pos', 'center'); | |
140 | |
141 this.errorBanner_ = createChild(errorWrapper, 'error-banner'); | |
142 | |
143 createChild(this.container_, 'spinner'); | |
144 | |
145 | |
146 this.editButton_ = createChild(this.toolbar_, 'button edit'); | |
147 this.editButton_.textContent = this.displayStringFunction_('edit'); | |
148 this.editButton_.addEventListener('click', this.onEdit_.bind(this)); | |
149 | |
150 // Editor toolbar. | |
151 | |
152 this.editBar_ = createChild(this.toolbar_, 'edit-bar'); | |
153 this.editBarMain_ = createChild(this.editBar_, 'edit-main'); | |
154 | |
155 this.editBarMode_ = createChild(this.container_, 'edit-modal'); | |
156 this.editBarModeWrapper_ = createChild( | |
157 this.editBarMode_, 'edit-modal-wrapper'); | |
158 this.editBarModeWrapper_.hidden = true; | |
159 | |
160 // Objects supporting image display and editing. | |
161 this.viewport_ = new Viewport(); | |
162 | |
163 this.imageView_ = new ImageView( | |
164 this.imageContainer_, | |
165 this.viewport_, | |
166 this.metadataCache_); | |
167 | |
168 this.imageView_.addContentCallback(this.onImageContentChanged_.bind(this)); | |
169 | |
170 this.editor_ = new ImageEditor( | |
171 this.viewport_, | |
172 this.imageView_, | |
173 this.prompt_, | |
174 { | |
175 root: this.container_, | |
176 image: this.imageContainer_, | |
177 toolbar: this.editBarMain_, | |
178 mode: this.editBarModeWrapper_ | |
179 }, | |
180 SlideMode.editorModes, | |
181 this.displayStringFunction_); | |
182 | |
183 this.editor_.getBuffer().addOverlay( | |
184 new SwipeOverlay(this.selectNext.bind(this))); | |
185 }; | |
186 | |
187 /** | |
188 * Load items, display the selected item. | |
189 * | |
190 * @param {Array.<Gallery.Item>} items Array of items. | |
191 * @param {number} selectedIndex Selected index. | |
192 * @param {function} callback Callback. | |
193 */ | |
194 SlideMode.prototype.load = function(items, selectedIndex, callback) { | |
195 var selectedItem = items[selectedIndex]; | |
196 if (!selectedItem) { | |
197 this.showErrorBanner_('IMAGE_ERROR'); | |
198 return; | |
199 } | |
200 | |
201 var loadDone = function() { | |
202 this.items_ = items; | |
203 this.setSelectedIndex_(selectedIndex); | |
204 this.setupNavigation_(); | |
205 setTimeout(this.requestPrefetch.bind(this, 1), 1000); | |
dgozman
2012/08/17 09:48:37
Need a comment what '1' means.
Vladislav Kaznacheev
2012/08/20 07:25:34
Done.
| |
206 callback(); | |
207 }.bind(this); | |
208 | |
209 var selectedUrl = selectedItem.getUrl(); | |
210 // Show the selected item ASAP, then complete the initialization | |
211 // (loading the ribbon thumbnails can take some time). | |
212 this.metadataCache_.get(selectedUrl, Gallery.METADATA_TYPE, | |
213 function(metadata) { | |
214 this.loadItem_(selectedUrl, metadata, 0, loadDone); | |
215 }.bind(this)); | |
216 }; | |
217 | |
218 /** | |
219 * Setup navigation controls. | |
220 * @private | |
221 */ | |
222 SlideMode.prototype.setupNavigation_ = function() { | |
223 this.sequenceDirection_ = 0; | |
224 this.sequenceLength_ = 0; | |
225 | |
226 ImageUtil.setAttribute(this.arrowLeft_, 'active', this.items_.length > 1); | |
227 ImageUtil.setAttribute(this.arrowRight_, 'active', this.items_.length > 1); | |
228 | |
229 this.ribbon_ = new Ribbon( | |
230 this.document_, this.metadataCache_, this.select.bind(this)); | |
231 this.ribbonSpacer_.appendChild(this.ribbon_); | |
232 this.ribbon_.update(this.items_, this.selectedIndex_); | |
233 }; | |
234 | |
235 /** | |
236 * @return {Gallery.Item} Selected item | |
237 */ | |
238 SlideMode.prototype.getSelectedItem = function() { | |
239 return this.items_ && this.items_[this.selectedIndex_]; | |
240 }; | |
241 | |
242 /** | |
243 * Change the selection. | |
244 * | |
245 * Commits the current image and changes the selection. | |
246 * | |
247 * @param {number} index New selected index. | |
248 * @param {number} opt_slideDir Slide animation direction (-1|0|1). | |
249 * Derived from the new and the old selected indices if omitted. | |
250 * @param {function} opt_callback Callback. | |
251 */ | |
252 SlideMode.prototype.select = function(index, opt_slideDir, opt_callback) { | |
253 if (!this.items_) | |
254 return; // Not fully initialized, still loading the first image. | |
255 | |
256 if (index == this.selectedIndex_) | |
257 return; // Do not reselect. | |
258 | |
259 this.commitItem_( | |
260 this.doSelect_.bind(this, index, opt_slideDir, opt_callback)); | |
261 }; | |
262 | |
263 /** | |
264 * Set the new selected index value. | |
265 * @param {number} index New selected index. | |
266 * @private | |
267 */ | |
268 SlideMode.prototype.setSelectedIndex_ = function(index) { | |
269 this.selectedIndex_ = index; | |
270 cr.dispatchSimpleEvent(this, 'selection'); | |
271 | |
272 // For once edited image, disallow the 'overwrite' setting change. | |
273 ImageUtil.setAttribute(this.options_, 'saved', | |
274 !this.getSelectedItem().isOriginal()); | |
275 }; | |
276 | |
277 /** | |
278 * Perform the actual selection change. | |
279 * | |
280 * @param {number} index New selected index. | |
281 * @param {number} opt_slideDir Slide animation direction (-1|0|1). | |
282 * Derived from the new and the old selected indices if omitted. | |
dgozman
2012/08/17 09:48:37
Can we always derive direction and pass it around?
Vladislav Kaznacheev
2012/08/20 07:25:34
There is one case when deriving the direction is n
| |
283 * @param {function} opt_callback Callback. | |
284 * @private | |
285 */ | |
286 SlideMode.prototype.doSelect_ = function(index, opt_slideDir, opt_callback) { | |
287 if (index == this.selectedIndex_) | |
288 return; // Do not reselect | |
289 | |
290 var step = opt_slideDir != undefined ? | |
291 opt_slideDir : | |
292 (index - this.selectedIndex_); | |
293 | |
294 if (Math.abs(step) != 1) { | |
295 // Long leap, the sequence is broken, we have no good prefetch candidate. | |
296 this.sequenceDirection_ = 0; | |
297 this.sequenceLength_ = 0; | |
298 } else if (this.sequenceDirection_ == step) { | |
299 // Keeping going in sequence. | |
300 this.sequenceLength_++; | |
301 } else { | |
302 // Reversed the direction. Reset the counter. | |
303 this.sequenceDirection_ = step; | |
304 this.sequenceLength_ = 1; | |
305 } | |
306 | |
307 if (this.sequenceLength_ <= 1) { | |
308 // We have just broke the sequence. Touch the current image so that it stays | |
309 // in the cache longer. | |
310 this.editor_.prefetchImage(this.getSelectedItem().getUrl()); | |
311 } | |
312 | |
313 this.setSelectedIndex_(index); | |
314 | |
315 if (this.ribbon_) | |
316 this.ribbon_.update(this.items_, this.selectedIndex_); | |
dgozman
2012/08/17 09:48:37
Why passing items here? Could they change?
Vladislav Kaznacheev
2012/08/20 07:25:34
Not really, but they will be able to in the next p
| |
317 | |
318 function shouldPrefetch(loadType, step, sequenceLength) { | |
319 // Never prefetch when selecting out of sequence. | |
320 if (Math.abs(step) != 1) | |
321 return false; | |
322 | |
323 // Never prefetch after a video load (decoding the next image can freeze | |
324 // the UI for a second or two). | |
325 if (loadType == ImageView.LOAD_TYPE_VIDEO_FILE) | |
326 return false; | |
327 | |
328 // Always prefetch if the previous load was from cache. | |
329 if (loadType == ImageView.LOAD_TYPE_CACHED_FULL) | |
330 return true; | |
331 | |
332 // Prefetch if we have been going in the same direction for long enough. | |
333 return sequenceLength >= 3; | |
334 } | |
335 | |
336 var selectedItem = this.getSelectedItem(); | |
337 var onMetadata = function(metadata) { | |
338 if (selectedItem != this.getSelectedItem()) return; | |
339 this.loadItem_(selectedItem.getUrl(), metadata, step, | |
340 function(loadType) { | |
341 if (selectedItem != this.getSelectedItem()) return; | |
342 if (shouldPrefetch(loadType, step, this.sequenceLength_)) { | |
343 this.requestPrefetch(step); | |
344 } | |
345 if (opt_callback) opt_callback(); | |
346 }.bind(this)); | |
347 }.bind(this); | |
348 this.metadataCache_.get( | |
349 selectedItem.getUrl(), Gallery.METADATA_TYPE, onMetadata); | |
350 }; | |
351 | |
352 /** | |
353 * @param {number} direction -1 for left, 1 for right. | |
354 * @return {number} Next index in the gived direction, with wrapping. | |
355 * @private | |
356 */ | |
357 SlideMode.prototype.getNextSelectedIndex_ = function(direction) { | |
358 var index = this.selectedIndex_ + (direction > 0 ? 1 : -1); | |
359 if (index == -1) return this.items_.length - 1; | |
360 if (index == this.items_.length) return 0; | |
361 return index; | |
362 }; | |
363 | |
364 /** | |
365 * Select the next item. | |
366 * @param {number} direction -1 for left, 1 for right. | |
367 * @param {function} opt_callback Callback. | |
368 */ | |
369 SlideMode.prototype.selectNext = function(direction, opt_callback) { | |
370 this.select(this.getNextSelectedIndex_(direction), direction, opt_callback); | |
371 }; | |
372 | |
373 /** | |
374 * Select the first item. | |
375 */ | |
376 SlideMode.prototype.selectFirst = function() { | |
377 this.select(0); | |
378 }; | |
379 | |
380 /** | |
381 * Select the last item. | |
382 */ | |
383 SlideMode.prototype.selectLast = function() { | |
384 this.select(this.items_.length - 1); | |
385 }; | |
386 | |
387 // Loading/unloading | |
388 | |
389 /** | |
390 * Load and display an item. | |
391 * | |
392 * @param {string} url Item url. | |
393 * @param {Object} metadata Item metadata. | |
394 * @param {number} slide Slide animation direction (-1|0|1). | |
395 * @param {function} callback Callback. | |
396 * @private | |
397 */ | |
398 SlideMode.prototype.loadItem_ = function(url, metadata, slide, callback) { | |
399 this.selectedImageMetadata_ = ImageUtil.deepCopy(metadata); | |
400 | |
401 this.showSpinner_(true); | |
402 | |
403 var self = this; | |
dgozman
2012/08/17 09:48:37
While we are here, change |self| to |this|?
Vladislav Kaznacheev
2012/08/20 07:25:34
Done.
| |
404 function loadDone(loadType) { | |
405 var video = self.isShowingVideo_(); | |
406 ImageUtil.setAttribute(self.container_, 'video', video); | |
407 | |
408 self.showSpinner_(false); | |
409 if (loadType == ImageView.LOAD_TYPE_ERROR) { | |
410 self.showErrorBanner_(video ? 'VIDEO_ERROR' : 'IMAGE_ERROR'); | |
411 } else if (loadType == ImageView.LOAD_TYPE_OFFLINE) { | |
412 self.showErrorBanner_(video ? 'VIDEO_OFFLINE' : 'IMAGE_OFFLINE'); | |
413 } | |
414 | |
415 if (video) { | |
416 if (self.isEditing()) { | |
417 // The editor toolbar does not make sense for video, hide it. | |
418 self.onEdit_(); | |
419 } | |
420 self.mediaControls_.attachMedia(self.imageView_.getVideo()); | |
421 //TODO(kaznacheev): Add metrics for video playback. | |
422 } else { | |
423 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View')); | |
424 | |
425 function toMillions(number) { return Math.round(number / (1000 * 1000)) } | |
426 | |
427 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'), | |
428 toMillions(metadata.filesystem.size)); | |
429 | |
430 var canvas = self.imageView_.getCanvas(); | |
431 ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'), | |
432 toMillions(canvas.width * canvas.height)); | |
433 | |
434 var extIndex = url.lastIndexOf('.'); | |
435 var ext = extIndex < 0 ? '' : url.substr(extIndex + 1).toLowerCase(); | |
436 if (ext == 'jpeg') ext = 'jpg'; | |
437 ImageUtil.metrics.recordEnum( | |
438 ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES); | |
439 } | |
440 | |
441 callback(loadType); | |
442 } | |
443 | |
444 this.editor_.openSession( | |
445 url, metadata, slide, this.saveCurrentImage_.bind(this), loadDone); | |
446 }; | |
447 | |
448 /** | |
449 * Commit changes to the current item and reset all messages/indicators. | |
450 * | |
451 * @param {function} callback Callback. | |
452 * @private | |
453 */ | |
454 SlideMode.prototype.commitItem_ = function(callback) { | |
455 this.showSpinner_(false); | |
456 this.showErrorBanner_(false); | |
457 this.editor_.getPrompt().hide(); | |
458 if (this.isShowingVideo_()) { | |
459 this.mediaControls_.pause(); | |
460 this.mediaControls_.detachMedia(); | |
461 } | |
462 this.editor_.closeSession(callback); | |
463 }; | |
464 | |
465 /** | |
466 * Request a prefetch for the next image. | |
467 * | |
468 * @param {number} direction -1 or 1. | |
469 */ | |
470 SlideMode.prototype.requestPrefetch = function(direction) { | |
471 if (this.items_.length < 2) return; | |
dgozman
2012/08/17 09:48:37
<= 1 is cleaner
Vladislav Kaznacheev
2012/08/20 07:25:34
Done.
| |
472 | |
473 var index = this.getNextSelectedIndex_(direction); | |
474 var nextItemUrl = this.items_[index].getUrl(); | |
475 | |
476 var selectedItem = this.getSelectedItem(); | |
477 this.metadataCache_.get(nextItemUrl, Gallery.METADATA_TYPE, | |
478 function(metadata) { | |
479 if (selectedItem != this.getSelectedItem()) return; | |
480 this.editor_.prefetchImage(nextItemUrl); | |
481 }.bind(this)); | |
482 }; | |
483 | |
484 // Event handlers. | |
485 | |
486 /** | |
487 * Unload handler. | |
488 * @private | |
489 */ | |
490 SlideMode.prototype.onUnload_ = function() { | |
491 this.saveVideoPosition_(); | |
492 window.top.removeEventListener('beforeunload', this.onBeforeUnloadBound_); | |
493 window.top.removeEventListener('unload', this.onTopUnloadBound_); | |
494 }; | |
495 | |
496 /** | |
497 * Top window unload handler. | |
498 * @private | |
499 */ | |
500 SlideMode.prototype.onTopUnload_ = function() { | |
501 this.saveVideoPosition_(); | |
502 }; | |
503 | |
504 /** | |
505 * Top window beforeunload handler. | |
506 * @return {string} Message to show if there are unsaved changes. | |
507 * @private | |
508 */ | |
509 SlideMode.prototype.onBeforeUnload_ = function() { | |
510 if (this.editor_.isBusy()) | |
511 return this.displayStringFunction_('unsaved_changes'); | |
512 return null; | |
513 }; | |
514 | |
515 /** | |
516 * Click handler. | |
517 * @private | |
518 */ | |
519 SlideMode.prototype.onClick_ = function() { | |
520 if (this.isShowingVideo_()) | |
521 this.mediaControls_.togglePlayStateWithFeedback(); | |
522 }; | |
523 | |
524 /** | |
525 * Keydown handler. | |
526 * | |
527 * @param {Event} event Event. | |
528 * @return {boolean} True if handled. | |
529 */ | |
530 SlideMode.prototype.onKeyDown = function(event) { | |
531 if (this.isEditing() && this.editor_.onKeyDown(event)) | |
532 return true; | |
533 | |
534 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
535 case 'U+0020': // Space toggles the video playback. | |
536 if (this.isShowingVideo_()) { | |
537 this.mediaControls_.togglePlayStateWithFeedback(); | |
538 } | |
539 break; | |
540 | |
541 case 'U+0045': // 'e' toggles the editor | |
542 this.onEdit_(); | |
543 break; | |
544 | |
545 case 'U+001B': // Escape | |
546 if (!this.isEditing()) | |
547 return false; // Not handled. | |
548 this.onEdit_(); | |
549 break; | |
550 | |
551 case 'Ctrl-U+00DD': // Ctrl+] (cryptic on purpose). | |
552 this.toggleSlideshow_(); | |
553 break; | |
554 | |
555 case 'Home': | |
556 this.selectFirst(); | |
557 break; | |
558 case 'End': | |
559 this.selectLast(); | |
560 break; | |
561 case 'Left': | |
562 this.selectNext(-1); | |
563 break; | |
564 case 'Right': | |
565 this.selectNext(1); | |
566 break; | |
567 | |
568 default: return false; | |
569 } | |
570 | |
571 return true; | |
572 }; | |
573 | |
574 /** | |
575 * Resize handler. | |
576 * @private | |
577 */ | |
578 SlideMode.prototype.onResize_ = function() { | |
579 this.viewport_.sizeByFrameAndFit(this.container_); | |
580 this.viewport_.repaint(); | |
581 }; | |
582 | |
583 // Saving | |
584 | |
585 /** | |
586 * Save the current image to a file. | |
587 * | |
588 * @param {function} callback Callback. | |
589 * @private | |
590 */ | |
591 SlideMode.prototype.saveCurrentImage_ = function(callback) { | |
592 var item = this.getSelectedItem(); | |
593 var oldUrl = item.getUrl(); | |
594 var canvas = this.imageView_.getCanvas(); | |
595 | |
596 this.showSpinner_(true); | |
597 var metadataEncoder = ImageEncoder.encodeMetadata( | |
598 this.selectedImageMetadata_.media, canvas, 1 /* quality */); | |
599 | |
600 this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata( | |
601 metadataEncoder.getMetadata(), this.selectedImageMetadata_); | |
602 | |
603 item.saveToFile( | |
604 this.context_.saveDirEntry, | |
605 this.shouldOverwriteOriginal_(), | |
606 canvas, | |
607 metadataEncoder, | |
608 function(success) { | |
609 // TODO(kaznacheev): Implement write error handling. | |
610 // Until then pretend that the save succeeded. | |
611 this.showSpinner_(false); | |
612 this.flashSavedLabel_(); | |
613 var newUrl = item.getUrl(); | |
614 this.updateSelectedUrl_(oldUrl, newUrl); | |
615 this.ribbon_.updateThumbnail( | |
616 this.selectedIndex_, newUrl, this.selectedImageMetadata_); | |
617 callback(); | |
618 }.bind(this)); | |
619 }; | |
620 | |
621 /** | |
622 * Update caches when the selected item url has changed. | |
623 * | |
624 * @param {string} oldUrl Old url. | |
625 * @param {string} newUrl New url. | |
626 * @private | |
627 */ | |
628 SlideMode.prototype.updateSelectedUrl_ = function(oldUrl, newUrl) { | |
629 this.metadataCache_.clear(oldUrl, Gallery.METADATA_TYPE); | |
630 | |
631 if (oldUrl == newUrl) | |
632 return; | |
633 | |
634 this.imageView_.changeUrl(newUrl); | |
635 this.ribbon_.remapCache(oldUrl, newUrl); | |
636 | |
637 // Let the gallery know that the selected item url has changed. | |
638 cr.dispatchSimpleEvent(this, 'selection'); | |
639 }; | |
640 | |
641 /** | |
642 * Flash 'Saved' label briefly to indicate that the image has been saved. | |
643 * @private | |
644 */ | |
645 SlideMode.prototype.flashSavedLabel_ = function() { | |
646 var selLabelHighlighted = | |
dgozman
2012/08/17 09:48:37
typo: sel -> set
Vladislav Kaznacheev
2012/08/20 07:25:34
Done.
| |
647 ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted'); | |
648 setTimeout(selLabelHighlighted.bind(null, true), 0); | |
649 setTimeout(selLabelHighlighted.bind(null, false), 300); | |
650 }; | |
651 | |
652 /** | |
653 * Local storage key for the 'Overwrite original' setting. | |
654 * @type {string} | |
655 */ | |
656 SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original'; | |
657 | |
658 /** | |
659 * Local storage key for the number of times that | |
660 * the overwrite info bubble has been displayed. | |
661 * @type {string} | |
662 */ | |
663 SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble'; | |
664 | |
665 /** | |
666 * Max number that the overwrite info bubble is shown. | |
667 * @type {number} | |
668 */ | |
669 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5; | |
670 | |
671 /** | |
672 * @return {boolean} True if 'Overwrite original' is set. | |
673 * @private | |
674 */ | |
675 SlideMode.prototype.shouldOverwriteOriginal_ = function() { | |
676 return SlideMode.OVERWRITE_KEY in localStorage && | |
677 (localStorage[SlideMode.OVERWRITE_KEY] == 'true'); | |
678 }; | |
679 | |
680 /** | |
681 * 'Overwrite original' checkbox handler. | |
682 * @param {Event} event Event. | |
683 * @private | |
684 */ | |
685 SlideMode.prototype.onOverwriteOriginalClick_ = function(event) { | |
686 localStorage['gallery-overwrite-original'] = event.target.checked; | |
687 }; | |
688 | |
689 /** | |
690 * Overwrite info bubble close handler. | |
691 * @private | |
692 */ | |
693 SlideMode.prototype.onCloseBubble_ = function() { | |
694 this.bubble_.hidden = true; | |
695 localStorage[SlideMode.OVERWRITE_BUBBLE_KEY] = | |
696 SlideMode.OVERWRITE_BUBBLE_MAX_TIMES; | |
697 }; | |
698 | |
699 /** | |
700 * Callback called when the image is edited. | |
701 * @private | |
702 */ | |
703 SlideMode.prototype.onImageContentChanged_ = function() { | |
704 var revision = this.imageView_.getContentRevision(); | |
705 if (revision == 0) { | |
706 // Is this required at all? | |
dgozman
2012/08/17 09:48:37
Debug?
Vladislav Kaznacheev
2012/08/20 07:25:34
Removed the comment. I still don't understand how
| |
707 // Just loaded. | |
708 var times = SlideMode.OVERWRITE_BUBBLE_KEY in localStorage ? | |
709 parseInt(localStorage[SlideMode.OVERWRITE_BUBBLE_KEY], 10) : 0; | |
710 if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) { | |
711 this.bubble_.hidden = false; | |
712 if (this.isEditing()) { | |
713 localStorage[SlideMode.OVERWRITE_BUBBLE_KEY] = times + 1; | |
714 } | |
715 } | |
716 } | |
717 | |
718 if (revision == 1) { | |
719 // First edit. | |
720 ImageUtil.setAttribute(this.options_, 'saved', true); | |
721 ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit')); | |
722 } | |
723 }; | |
724 | |
725 // Misc | |
726 | |
727 /** | |
728 * Start/stop the slide show. | |
729 * @private | |
730 */ | |
731 SlideMode.prototype.toggleSlideshow_ = function() { | |
732 if (this.slideShowTimeout_) { | |
733 clearInterval(this.slideShowTimeout_); | |
734 this.slideShowTimeout_ = null; | |
735 } else { | |
736 var self = this; | |
737 function nextSlide() { | |
738 self.selectNext(1, | |
739 function() { self.slideShowTimeout_ = setTimeout(nextSlide, 5000) }); | |
740 } | |
741 nextSlide(); | |
742 } | |
743 }; | |
744 | |
745 /** | |
746 * @return {boolean} True if the editor is active. | |
747 */ | |
748 SlideMode.prototype.isEditing = function() { | |
749 return this.container_.hasAttribute('editing'); | |
750 }; | |
751 | |
752 /** | |
753 * Activate/deactivate editor. | |
754 * @private | |
755 */ | |
756 SlideMode.prototype.onEdit_ = function() { | |
757 if (!this.isEditing() && this.isShowingVideo_()) | |
758 return; // No editing for videos. | |
759 | |
760 ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing()); | |
761 | |
762 if (this.isEditing()) { // isEditing_ has just been flipped to a new value. | |
763 if (this.context_.readonlyDirName) { | |
764 this.editor_.getPrompt().showAt( | |
765 'top', 'readonly_warning', 0, this.context_.readonlyDirName); | |
766 } | |
767 } else { | |
768 this.editor_.getPrompt().hide(); | |
769 } | |
770 | |
771 ImageUtil.setAttribute(this.editButton_, 'pressed', this.isEditing()); | |
772 | |
773 cr.dispatchSimpleEvent(this, 'edit'); | |
774 }; | |
775 | |
776 /** | |
777 * Display the error banner. | |
778 * @param {string} message Message. | |
779 * @private | |
780 */ | |
781 SlideMode.prototype.showErrorBanner_ = function(message) { | |
782 if (message) { | |
783 this.errorBanner_.textContent = this.displayStringFunction_(message); | |
784 } | |
785 ImageUtil.setAttribute(this.container_, 'error', !!message); | |
786 }; | |
787 | |
788 /** | |
789 * Show/hide the busy spinner. | |
790 * | |
791 * @param {boolean} on True if show, false if hide. | |
792 * @private | |
793 */ | |
794 SlideMode.prototype.showSpinner_ = function(on) { | |
795 if (this.spinnerTimer_) { | |
796 clearTimeout(this.spinnerTimer_); | |
797 this.spinnerTimer_ = null; | |
798 } | |
799 | |
800 if (on) { | |
801 this.spinnerTimer_ = setTimeout(function() { | |
802 this.spinnerTimer_ = null; | |
803 ImageUtil.setAttribute(this.container_, 'spinner', true); | |
804 }.bind(this), 1000); | |
805 } else { | |
806 ImageUtil.setAttribute(this.container_, 'spinner', false); | |
807 } | |
808 }; | |
809 | |
810 /** | |
811 * @return {boolean} True if the current item is a video. | |
812 * @private | |
813 */ | |
814 SlideMode.prototype.isShowingVideo_ = function() { | |
815 return !!this.imageView_.getVideo(); | |
816 }; | |
817 | |
818 /** | |
819 * Save the current video position. | |
820 * @private | |
821 */ | |
822 SlideMode.prototype.saveVideoPosition_ = function() { | |
823 if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) { | |
824 this.mediaControls_.savePosition(); | |
825 } | |
826 }; | |
827 | |
828 /** | |
829 * Overlay that handles swipe gestures. Changes to the next or previous file. | |
830 * @param {function(number)} callback A callback accepting the swipe direction | |
831 * (1 means left, -1 right). | |
832 * @constructor | |
833 * @implements {ImageBuffer.Overlay} | |
834 */ | |
835 function SwipeOverlay(callback) { | |
836 this.callback_ = callback; | |
837 } | |
838 | |
839 /** | |
840 * Inherit ImageBuffer.Overlay. | |
841 */ | |
842 SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype; | |
843 | |
844 /** | |
845 * @param {number} x X pointer position. | |
846 * @param {number} y Y pointer position. | |
847 * @param {boolean} touch True if dragging caused by touch. | |
848 * @return {function} The closure to call on drag. | |
849 */ | |
850 SwipeOverlay.prototype.getDragHandler = function(x, y, touch) { | |
851 if (!touch) | |
852 return null; | |
853 var origin = x; | |
854 var done = false; | |
855 return function(x, y) { | |
856 if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) { | |
857 this.callback_(1); | |
858 done = true; | |
859 } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) { | |
860 this.callback_(-1); | |
861 done = true; | |
862 } | |
863 }.bind(this); | |
864 }; | |
865 | |
866 /** | |
867 * If the user touched the image and moved the finger more than SWIPE_THRESHOLD | |
868 * horizontally it's considered as a swipe gesture (change the current image). | |
869 */ | |
870 SwipeOverlay.SWIPE_THRESHOLD = 100; | |
OLD | NEW |