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