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