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