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