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