1// Copyright (c) 2013 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 * PreviewPanel UI class. 9 * @param {HTMLElement} element DOM Element of preview panel. 10 * @param {PreviewPanel.VisibilityType} visibilityType Initial value of the 11 * visibility type. 12 * @param {MetadataCache} metadataCache Metadata cache. 13 * @param {VolumeManagerWrapper} volumeManager Volume manager. 14 * @constructor 15 * @extends {cr.EventTarget} 16 */ 17var PreviewPanel = function(element, 18 visibilityType, 19 metadataCache, 20 volumeManager) { 21 /** 22 * The cached height of preview panel. 23 * @type {number} 24 * @private 25 */ 26 this.height_ = 0; 27 28 /** 29 * Visibility type of the preview panel. 30 * @type {PreviewPanel.VisiblityType} 31 * @private 32 */ 33 this.visibilityType_ = visibilityType; 34 35 /** 36 * Current entry to be displayed. 37 * @type {Entry} 38 * @private 39 */ 40 this.currentEntry_ = null; 41 42 /** 43 * Dom element of the preview panel. 44 * @type {HTMLElement} 45 * @private 46 */ 47 this.element_ = element; 48 49 /** 50 * @type {PreviewPanel.Thumbnails} 51 */ 52 this.thumbnails = new PreviewPanel.Thumbnails( 53 element.querySelector('.preview-thumbnails'), 54 metadataCache, 55 volumeManager); 56 57 /** 58 * @type {HTMLElement} 59 * @private 60 */ 61 this.summaryElement_ = element.querySelector('.preview-summary'); 62 63 /** 64 * @type {PreviewPanel.CalculatingSizeLabel} 65 * @private 66 */ 67 this.calculatingSizeLabel_ = new PreviewPanel.CalculatingSizeLabel( 68 this.summaryElement_.querySelector('.calculating-size')); 69 70 /** 71 * @type {HTMLElement} 72 * @private 73 */ 74 this.previewText_ = element.querySelector('.preview-text'); 75 76 /** 77 * FileSelection to be displayed. 78 * @type {FileSelection} 79 * @private 80 */ 81 this.selection_ = {entries: [], computeBytes: function() {}}; 82 83 /** 84 * Sequence value that is incremented by every selection update and is used to 85 * check if the callback is up to date or not. 86 * @type {number} 87 * @private 88 */ 89 this.sequence_ = 0; 90 91 /** 92 * @type {VolumeManagerWrapper} 93 * @private 94 */ 95 this.volumeManager_ = volumeManager; 96 97 cr.EventTarget.call(this); 98}; 99 100/** 101 * Name of PreviewPanels's event. 102 * @enum {string} 103 * @const 104 */ 105PreviewPanel.Event = Object.freeze({ 106 // Event to be triggered at the end of visibility change. 107 VISIBILITY_CHANGE: 'visibilityChange' 108}); 109 110/** 111 * Visibility type of the preview panel. 112 */ 113PreviewPanel.VisibilityType = Object.freeze({ 114 // Preview panel always shows. 115 ALWAYS_VISIBLE: 'alwaysVisible', 116 // Preview panel shows when the selection property are set. 117 AUTO: 'auto', 118 // Preview panel does not show. 119 ALWAYS_HIDDEN: 'alwaysHidden' 120}); 121 122/** 123 * @private 124 */ 125PreviewPanel.Visibility_ = Object.freeze({ 126 VISIBLE: 'visible', 127 HIDING: 'hiding', 128 HIDDEN: 'hidden' 129}); 130 131PreviewPanel.prototype = { 132 __proto__: cr.EventTarget.prototype, 133 134 /** 135 * Setter for the current entry. 136 * @param {Entry} entry New entry. 137 */ 138 set currentEntry(entry) { 139 if (util.isSameEntry(this.currentEntry_, entry)) 140 return; 141 this.currentEntry_ = entry; 142 this.updateVisibility_(); 143 this.updatePreviewArea_(); 144 }, 145 146 /** 147 * Setter for the visibility type. 148 * @param {PreviewPanel.VisibilityType} visibilityType New value of visibility 149 * type. 150 */ 151 set visibilityType(visibilityType) { 152 this.visibilityType_ = visibilityType; 153 this.updateVisibility_(); 154 // Also update the preview area contents, because the update is suppressed 155 // while the visibility is hiding or hidden. 156 this.updatePreviewArea_(); 157 }, 158 159 get visible() { 160 return this.element_.getAttribute('visibility') == 161 PreviewPanel.Visibility_.VISIBLE; 162 }, 163 164 /** 165 * Obtains the height of preview panel. 166 * @return {number} Height of preview panel. 167 */ 168 get height() { 169 this.height_ = this.height_ || this.element_.clientHeight; 170 return this.height_; 171 } 172}; 173 174/** 175 * Initializes the element. 176 */ 177PreviewPanel.prototype.initialize = function() { 178 this.element_.addEventListener('webkitTransitionEnd', 179 this.onTransitionEnd_.bind(this)); 180 this.updateVisibility_(); 181 // Also update the preview area contents, because the update is suppressed 182 // while the visibility is hiding or hidden. 183 this.updatePreviewArea_(); 184}; 185 186/** 187 * Apply the selection and update the view of the preview panel. 188 * @param {FileSelection} selection Selection to be applied. 189 */ 190PreviewPanel.prototype.setSelection = function(selection) { 191 this.sequence_++; 192 this.selection_ = selection; 193 this.updateVisibility_(); 194 this.updatePreviewArea_(); 195}; 196 197/** 198 * Update the visibility of the preview panel. 199 * @private 200 */ 201PreviewPanel.prototype.updateVisibility_ = function() { 202 // Get the new visibility value. 203 var visibility = this.element_.getAttribute('visibility'); 204 var newVisible = null; 205 switch (this.visibilityType_) { 206 case PreviewPanel.VisibilityType.ALWAYS_VISIBLE: 207 newVisible = true; 208 break; 209 case PreviewPanel.VisibilityType.AUTO: 210 newVisible = this.selection_.entries.length !== 0; 211 break; 212 case PreviewPanel.VisibilityType.ALWAYS_HIDDEN: 213 newVisible = false; 214 break; 215 default: 216 console.error('Invalid visibilityType.'); 217 return; 218 } 219 220 // If the visibility has been already the new value, just return. 221 if ((visibility == PreviewPanel.Visibility_.VISIBLE && newVisible) || 222 (visibility == PreviewPanel.Visibility_.HIDDEN && !newVisible)) 223 return; 224 225 // Set the new visibility value. 226 if (newVisible) { 227 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.VISIBLE); 228 cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); 229 } else { 230 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDING); 231 } 232}; 233 234/** 235 * Update the text in the preview panel. 236 * @private 237 */ 238PreviewPanel.prototype.updatePreviewArea_ = function() { 239 // If the preview panel is hiding, does not update the current view. 240 if (!this.visible) 241 return; 242 var selection = this.selection_; 243 244 // If no item is selected, no information is displayed on the footer. 245 if (selection.totalCount === 0) { 246 this.thumbnails.hidden = true; 247 this.calculatingSizeLabel_.hidden = true; 248 this.previewText_.textContent = ''; 249 return; 250 } 251 252 // If one item is selected, show thumbnail and entry name of the item. 253 if (selection.totalCount === 1) { 254 this.thumbnails.hidden = false; 255 this.thumbnails.selection = selection; 256 this.calculatingSizeLabel_.hidden = true; 257 this.previewText_.textContent = util.getEntryLabel( 258 this.volumeManager_, selection.entries[0]); 259 return; 260 } 261 262 // Update thumbnails. 263 this.thumbnails.hidden = false; 264 this.thumbnails.selection = selection; 265 266 // Obtains the preview text. 267 var text; 268 if (selection.directoryCount == 0) 269 text = strf('MANY_FILES_SELECTED', selection.fileCount); 270 else if (selection.fileCount == 0) 271 text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount); 272 else 273 text = strf('MANY_ENTRIES_SELECTED', selection.totalCount); 274 275 // Obtains the size of files. 276 this.calculatingSizeLabel_.hidden = selection.bytesKnown; 277 if (selection.bytesKnown && selection.showBytes) 278 text += ', ' + util.bytesToString(selection.bytes); 279 280 // Set the preview text to the element. 281 this.previewText_.textContent = text; 282 283 // Request the byte calculation if needed. 284 if (!selection.bytesKnown) { 285 this.selection_.computeBytes(function(sequence) { 286 // Selection has been already updated. 287 if (this.sequence_ != sequence) 288 return; 289 this.updatePreviewArea_(); 290 }.bind(this, this.sequence_)); 291 } 292}; 293 294/** 295 * Event handler to be called at the end of hiding transition. 296 * @param {Event} event The webkitTransitionEnd event. 297 * @private 298 */ 299PreviewPanel.prototype.onTransitionEnd_ = function(event) { 300 if (event.target != this.element_ || event.propertyName != 'opacity') 301 return; 302 var visibility = this.element_.getAttribute('visibility'); 303 if (visibility != PreviewPanel.Visibility_.HIDING) 304 return; 305 this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDDEN); 306 cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE); 307}; 308 309/** 310 * Animating label that is shown during the bytes of selection entries is being 311 * calculated. 312 * 313 * This label shows dots and varying the number of dots every 314 * CalculatingSizeLabel.PERIOD milliseconds. 315 * @param {HTMLElement} element DOM element of the label. 316 * @constructor 317 */ 318PreviewPanel.CalculatingSizeLabel = function(element) { 319 this.element_ = element; 320 this.count_ = 0; 321 this.intervalID_ = null; 322 Object.seal(this); 323}; 324 325/** 326 * Time period in milliseconds. 327 * @const {number} 328 */ 329PreviewPanel.CalculatingSizeLabel.PERIOD = 500; 330 331PreviewPanel.CalculatingSizeLabel.prototype = { 332 /** 333 * Set visibility of the label. 334 * When it is displayed, the text is animated. 335 * @param {boolean} hidden Whether to hide the label or not. 336 */ 337 set hidden(hidden) { 338 this.element_.hidden = hidden; 339 if (!hidden) { 340 if (this.intervalID_ != null) 341 return; 342 this.count_ = 2; 343 this.intervalID_ = 344 setInterval(this.onStep_.bind(this), 345 PreviewPanel.CalculatingSizeLabel.PERIOD); 346 this.onStep_(); 347 } else { 348 if (this.intervalID_ == null) 349 return; 350 clearInterval(this.intervalID_); 351 this.intervalID_ = null; 352 } 353 } 354}; 355 356/** 357 * Increments the counter and updates the number of dots. 358 * @private 359 */ 360PreviewPanel.CalculatingSizeLabel.prototype.onStep_ = function() { 361 var text = str('CALCULATING_SIZE'); 362 for (var i = 0; i < ~~(this.count_ / 2) % 4; i++) { 363 text += '.'; 364 } 365 this.element_.textContent = text; 366 this.count_++; 367}; 368 369/** 370 * Thumbnails on the preview panel. 371 * 372 * @param {HTMLElement} element DOM Element of thumbnail container. 373 * @param {MetadataCache} metadataCache MetadataCache. 374 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 375 * @constructor 376 */ 377PreviewPanel.Thumbnails = function(element, metadataCache, volumeManager) { 378 this.element_ = element; 379 this.metadataCache_ = metadataCache; 380 this.volumeManager_ = volumeManager; 381 this.sequence_ = 0; 382 Object.seal(this); 383}; 384 385/** 386 * Maximum number of thumbnails. 387 * @const {number} 388 */ 389PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT = 4; 390 391/** 392 * Edge length of the thumbnail square. 393 * @const {number} 394 */ 395PreviewPanel.Thumbnails.THUMBNAIL_SIZE = 35; 396 397/** 398 * Longer edge length of zoomed thumbnail rectangle. 399 * @const {number} 400 */ 401PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE = 200; 402 403PreviewPanel.Thumbnails.prototype = { 404 /** 405 * Sets entries to be displayed in the view. 406 * @param {Array.<Entry>} value Entries. 407 */ 408 set selection(value) { 409 this.sequence_++; 410 this.loadThumbnails_(value); 411 }, 412 413 /** 414 * Set visibility of the thumbnails. 415 * @param {boolean} value Whether to hide the thumbnails or not. 416 */ 417 set hidden(value) { 418 this.element_.hidden = value; 419 } 420}; 421 422/** 423 * Loads thumbnail images. 424 * @param {FileSelection} selection Selection containing entries that are 425 * sources of images. 426 * @private 427 */ 428PreviewPanel.Thumbnails.prototype.loadThumbnails_ = function(selection) { 429 var entries = selection.entries; 430 this.element_.classList.remove('has-zoom'); 431 this.element_.innerText = ''; 432 var clickHandler = selection.tasks && 433 selection.tasks.executeDefault.bind(selection.tasks); 434 var length = Math.min(entries.length, 435 PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT); 436 for (var i = 0; i < length; i++) { 437 // Create a box. 438 var box = this.element_.ownerDocument.createElement('div'); 439 box.style.zIndex = PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT + 1 - i; 440 441 // Load the image. 442 if (entries[i]) { 443 FileGrid.decorateThumbnailBox(box, 444 entries[i], 445 this.metadataCache_, 446 this.volumeManager_, 447 ThumbnailLoader.FillMode.FILL, 448 FileGrid.ThumbnailQuality.LOW, 449 i == 0 && length == 1 && 450 this.setZoomedImage_.bind(this)); 451 } 452 453 // Register the click handler. 454 if (clickHandler) 455 box.addEventListener('click', clickHandler); 456 457 // Append 458 this.element_.appendChild(box); 459 } 460}; 461 462/** 463 * Create the zoomed version of image and set it to the DOM element to show the 464 * zoomed image. 465 * 466 * @param {Image} image Image to be source of the zoomed image. 467 * @param {transform} transform Transformation to be applied to the image. 468 * @private 469 */ 470PreviewPanel.Thumbnails.prototype.setZoomedImage_ = function(image, transform) { 471 if (!image) 472 return; 473 var width = image.width || 0; 474 var height = image.height || 0; 475 if (width == 0 || 476 height == 0 || 477 (width < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2 && 478 height < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2)) 479 return; 480 481 var scale = Math.min(1, 482 PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE / 483 Math.max(width, height)); 484 var imageWidth = ~~(width * scale); 485 var imageHeight = ~~(height * scale); 486 var zoomedImage = this.element_.ownerDocument.createElement('img'); 487 488 if (scale < 0.3) { 489 // Scaling large images kills animation. Downscale it in advance. 490 // Canvas scales images with liner interpolation. Make a larger 491 // image (but small enough to not kill animation) and let IMAGE 492 // scale it smoothly. 493 var INTERMEDIATE_SCALE = 3; 494 var canvas = this.element_.ownerDocument.createElement('canvas'); 495 canvas.width = imageWidth * INTERMEDIATE_SCALE; 496 canvas.height = imageHeight * INTERMEDIATE_SCALE; 497 var ctx = canvas.getContext('2d'); 498 ctx.drawImage(image, 0, 0, canvas.width, canvas.height); 499 // Using bigger than default compression reduces image size by 500 // several times. Quality degradation compensated by greater resolution. 501 zoomedImage.src = canvas.toDataURL('image/jpeg', 0.6); 502 } else { 503 zoomedImage.src = image.src; 504 } 505 506 var boxWidth = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageWidth); 507 var boxHeight = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageHeight); 508 if (transform && transform.rotate90 % 2 == 1) { 509 var t = boxWidth; 510 boxWidth = boxHeight; 511 boxHeight = t; 512 } 513 514 util.applyTransform(zoomedImage, transform); 515 516 var zoomedBox = this.element_.ownerDocument.createElement('div'); 517 zoomedBox.className = 'popup'; 518 zoomedBox.style.width = boxWidth + 'px'; 519 zoomedBox.style.height = boxHeight + 'px'; 520 zoomedBox.appendChild(zoomedImage); 521 522 this.element_.appendChild(zoomedBox); 523 this.element_.classList.add('has-zoom'); 524 return; 525}; 526