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