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