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