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