| 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 |