1// Copyright (c) 2012 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 * The current selection object.
9 *
10 * @param {FileManager} fileManager FileManager instance.
11 * @param {Array.<number>} indexes Selected indexes.
12 * @constructor
13 */
14function FileSelection(fileManager, indexes) {
15  this.fileManager_ = fileManager;
16  this.computeBytesSequence_ = 0;
17  this.indexes = indexes;
18  this.entries = [];
19  this.totalCount = 0;
20  this.fileCount = 0;
21  this.directoryCount = 0;
22  this.bytes = 0;
23  this.showBytes = false;
24  this.allDriveFilesPresent = false,
25  this.iconType = null;
26  this.bytesKnown = false;
27  this.mustBeHidden_ = false;
28  this.mimeTypes = null;
29
30  // Synchronously compute what we can.
31  for (var i = 0; i < this.indexes.length; i++) {
32    var entry = fileManager.getFileList().item(this.indexes[i]);
33    if (!entry)
34      continue;
35
36    this.entries.push(entry);
37
38    if (this.iconType == null) {
39      this.iconType = FileType.getIcon(entry);
40    } else if (this.iconType != 'unknown') {
41      var iconType = FileType.getIcon(entry);
42      if (this.iconType != iconType)
43        this.iconType = 'unknown';
44    }
45
46    if (entry.isFile) {
47      this.fileCount += 1;
48    } else {
49      this.directoryCount += 1;
50    }
51    this.totalCount++;
52  }
53
54  this.tasks = new FileTasks(this.fileManager_);
55
56  Object.seal(this);
57}
58
59/**
60 * Computes data required to get file tasks and requests the tasks.
61 *
62 * @param {function} callback The callback.
63 */
64FileSelection.prototype.createTasks = function(callback) {
65  if (!this.fileManager_.isOnDrive()) {
66    this.tasks.init(this.entries);
67    callback();
68    return;
69  }
70
71  this.fileManager_.metadataCache_.get(
72      this.entries, 'external', function(props) {
73        var present = props.filter(function(p) {
74          return p && p.availableOffline;
75        });
76        this.allDriveFilesPresent = present.length == props.length;
77
78        // Collect all of the mime types and push that info into the selection.
79        this.mimeTypes = props.map(function(value) {
80          return (value && value.contentMimeType) || '';
81        });
82
83        this.tasks.init(this.entries, this.mimeTypes);
84        callback();
85      }.bind(this));
86};
87
88/**
89 * Computes the total size of selected files.
90 *
91 * @param {function} callback Completion callback. Not called when cancelled,
92 *     or a new call has been invoked in the meantime.
93 */
94FileSelection.prototype.computeBytes = function(callback) {
95  if (this.entries.length == 0) {
96    this.bytesKnown = true;
97    this.showBytes = false;
98    this.bytes = 0;
99    return;
100  }
101
102  var computeBytesSequence = ++this.computeBytesSequence_;
103  var pendingMetadataCount = 0;
104
105  var maybeDone = function() {
106    if (pendingMetadataCount == 0) {
107      this.bytesKnown = true;
108      callback();
109    }
110  }.bind(this);
111
112  var onProps = function(properties) {
113    // Ignore if the call got cancelled, or there is another new one fired.
114    if (computeBytesSequence != this.computeBytesSequence_)
115      return;
116
117    // It may happen that the metadata is not available because a file has been
118    // deleted in the meantime.
119    if (properties)
120      this.bytes += properties.size;
121    pendingMetadataCount--;
122    maybeDone();
123  }.bind(this);
124
125  for (var index = 0; index < this.entries.length; index++) {
126    var entry = this.entries[index];
127    if (entry.isFile) {
128      this.showBytes |= !FileType.isHosted(entry);
129      pendingMetadataCount++;
130      this.fileManager_.metadataCache_.getOne(entry, 'filesystem', onProps);
131    } else if (entry.isDirectory) {
132      // Don't compute the directory size as it's expensive.
133      // crbug.com/179073.
134      this.showBytes = false;
135      break;
136    }
137  }
138  maybeDone();
139};
140
141/**
142 * Cancels any async computation by increasing the sequence number. Results
143 * of any previous call to computeBytes() will be discarded.
144 *
145 * @private
146 */
147FileSelection.prototype.cancelComputing_ = function() {
148  this.computeBytesSequence_++;
149};
150
151/**
152 * This object encapsulates everything related to current selection.
153 *
154 * @param {FileManager} fileManager File manager instance.
155 * @extends {cr.EventTarget}
156 * @constructor
157 */
158function FileSelectionHandler(fileManager) {
159  this.fileManager_ = fileManager;
160  // TODO(dgozman): create a shared object with most of UI elements.
161  this.okButton_ = fileManager.okButton_;
162  this.filenameInput_ = fileManager.filenameInput_;
163  this.previewPanel_ = fileManager.previewPanel_;
164  this.taskItems_ = fileManager.taskItems_;
165}
166
167/**
168 * Create the temporary disabled action menu item.
169 * @return {Object} Created disabled item.
170 * @private
171 */
172FileSelectionHandler.createTemporaryDisabledActionMenuItem_ = function() {
173  if (!FileSelectionHandler.cachedDisabledActionMenuItem_) {
174    FileSelectionHandler.cachedDisabledActionMenuItem_ = {
175      label: str('ACTION_OPEN'),
176      disabled: true
177    };
178  }
179
180  return FileSelectionHandler.cachedDisabledActionMenuItem_;
181};
182
183/**
184 * Cached the temporary disabled action menu item. Used inside
185 * FileSelectionHandler.createTemporaryDisabledActionMenuItem_().
186 * @private
187 */
188FileSelectionHandler.cachedDisabledActionMenuItem_ = null;
189
190/**
191 * FileSelectionHandler extends cr.EventTarget.
192 */
193FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype;
194
195/**
196 * Maximum amount of thumbnails in the preview pane.
197 *
198 * @const
199 * @type {number}
200 */
201FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT = 4;
202
203/**
204 * Maximum width or height of an image what pops up when the mouse hovers
205 * thumbnail in the bottom panel (in pixels).
206 *
207 * @const
208 * @type {number}
209 */
210FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE = 200;
211
212/**
213 * Update the UI when the selection model changes.
214 *
215 * @param {Event} event The change event.
216 */
217FileSelectionHandler.prototype.onFileSelectionChanged = function(event) {
218  var indexes =
219      this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
220  if (this.selection) this.selection.cancelComputing_();
221  var selection = new FileSelection(this.fileManager_, indexes);
222  this.selection = selection;
223
224  if (this.fileManager_.dialogType == DialogType.SELECT_SAVEAS_FILE) {
225    // If this is a save-as dialog, copy the selected file into the filename
226    // input text box.
227    if (this.selection.totalCount == 1 &&
228        this.selection.entries[0].isFile &&
229        this.filenameInput_.value != this.selection.entries[0].name) {
230      this.filenameInput_.value = this.selection.entries[0].name;
231    }
232  }
233
234  this.updateOkButton();
235
236  if (this.selectionUpdateTimer_) {
237    clearTimeout(this.selectionUpdateTimer_);
238    this.selectionUpdateTimer_ = null;
239  }
240
241  // The rest of the selection properties are computed via (sometimes lengthy)
242  // asynchronous calls. We initiate these calls after a timeout. If the
243  // selection is changing quickly we only do this once when it slows down.
244
245  var updateDelay = 200;
246  var now = Date.now();
247  if (now > (this.lastFileSelectionTime_ || 0) + updateDelay) {
248    // The previous selection change happened a while ago. Update the UI soon.
249    updateDelay = 0;
250  }
251  this.lastFileSelectionTime_ = now;
252
253  if (this.fileManager_.dialogType === DialogType.FULL_PAGE &&
254      selection.directoryCount === 0 && selection.fileCount > 0) {
255    // Show disabled items for position calculation of the menu. They will be
256    // overridden in this.updateFileSelectionAsync().
257    this.fileManager_.updateContextMenuActionItems(
258        FileSelectionHandler.createTemporaryDisabledActionMenuItem_(), true);
259  } else {
260    // Update context menu.
261    this.fileManager_.updateContextMenuActionItems(null, false);
262  }
263
264  this.selectionUpdateTimer_ = setTimeout(function() {
265    this.selectionUpdateTimer_ = null;
266    if (this.selection == selection)
267      this.updateFileSelectionAsync(selection);
268  }.bind(this), updateDelay);
269};
270
271/**
272 * Updates the Ok button enabled state.
273 *
274 * @return {boolean} Whether button is enabled.
275 */
276FileSelectionHandler.prototype.updateOkButton = function() {
277  var selectable;
278  var dialogType = this.fileManager_.dialogType;
279
280  if (DialogType.isFolderDialog(dialogType)) {
281    // In SELECT_FOLDER mode, we allow to select current directory
282    // when nothing is selected.
283    selectable = this.selection.directoryCount <= 1 &&
284        this.selection.fileCount == 0;
285  } else if (dialogType == DialogType.SELECT_OPEN_FILE) {
286    selectable = (this.isFileSelectionAvailable() &&
287                  this.selection.directoryCount == 0 &&
288                  this.selection.fileCount == 1);
289  } else if (dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
290    selectable = (this.isFileSelectionAvailable() &&
291                  this.selection.directoryCount == 0 &&
292                  this.selection.fileCount >= 1);
293  } else if (dialogType == DialogType.SELECT_SAVEAS_FILE) {
294    if (this.fileManager_.isOnReadonlyDirectory()) {
295      selectable = false;
296    } else {
297      selectable = !!this.filenameInput_.value;
298    }
299  } else if (dialogType == DialogType.FULL_PAGE) {
300    // No "select" buttons on the full page UI.
301    selectable = true;
302  } else {
303    throw new Error('Unknown dialog type');
304  }
305
306  this.okButton_.disabled = !selectable;
307  return selectable;
308};
309
310/**
311  * Check if all the files in the current selection are available. The only
312  * case when files might be not available is when the selection contains
313  * uncached Drive files and the browser is offline.
314  *
315  * @return {boolean} True if all files in the current selection are
316  *                   available.
317  */
318FileSelectionHandler.prototype.isFileSelectionAvailable = function() {
319  var isDriveOffline =
320      this.fileManager_.volumeManager.getDriveConnectionState().type ===
321          VolumeManagerCommon.DriveConnectionType.OFFLINE;
322  return !this.fileManager_.isOnDrive() || !isDriveOffline ||
323      this.selection.allDriveFilesPresent;
324};
325
326/**
327 * Calculates async selection stats and updates secondary UI elements.
328 *
329 * @param {FileSelection} selection The selection object.
330 */
331FileSelectionHandler.prototype.updateFileSelectionAsync = function(selection) {
332  if (this.selection != selection) return;
333
334  // Update the file tasks.
335  if (this.fileManager_.dialogType === DialogType.FULL_PAGE &&
336      selection.directoryCount === 0 && selection.fileCount > 0) {
337    selection.createTasks(function() {
338      if (this.selection != selection)
339        return;
340      selection.tasks.display(this.taskItems_);
341      selection.tasks.updateMenuItem();
342    }.bind(this));
343  } else {
344    this.taskItems_.hidden = true;
345  }
346
347  // Update preview panels.
348  var wasVisible = this.previewPanel_.visible;
349  this.previewPanel_.setSelection(selection);
350
351  // Scroll to item
352  if (!wasVisible && this.selection.totalCount == 1) {
353    var list = this.fileManager_.getCurrentList();
354    list.scrollIndexIntoView(list.selectionModel.selectedIndex);
355  }
356
357  // Sync the commands availability.
358  if (this.fileManager_.commandHandler)
359    this.fileManager_.commandHandler.updateAvailability();
360
361  // Inform tests it's OK to click buttons now.
362  if (selection.totalCount > 0) {
363    util.testSendMessage('selection-change-complete');
364  }
365};
366