| OLD | NEW |
| (Empty) |
| 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 // If directory files changes too often, don't rescan directory more than once | |
| 8 // per specified interval | |
| 9 var SIMULTANEOUS_RESCAN_INTERVAL = 1000; | |
| 10 // Used for operations that require almost instant rescan. | |
| 11 var SHORT_RESCAN_INTERVAL = 100; | |
| 12 | |
| 13 /** | |
| 14 * Data model of the file manager. | |
| 15 * | |
| 16 * @param {boolean} singleSelection True if only one file could be selected | |
| 17 * at the time. | |
| 18 * @param {FileFilter} fileFilter Instance of FileFilter. | |
| 19 * @param {FileWatcher} fileWatcher Instance of FileWatcher. | |
| 20 * @param {MetadataCache} metadataCache The metadata cache service. | |
| 21 * @param {VolumeManagerWrapper} volumeManager The volume manager. | |
| 22 * @constructor | |
| 23 */ | |
| 24 function DirectoryModel(singleSelection, fileFilter, fileWatcher, | |
| 25 metadataCache, volumeManager) { | |
| 26 this.fileListSelection_ = singleSelection ? | |
| 27 new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel(); | |
| 28 | |
| 29 this.runningScan_ = null; | |
| 30 this.pendingScan_ = null; | |
| 31 this.rescanTime_ = null; | |
| 32 this.scanFailures_ = 0; | |
| 33 this.changeDirectorySequence_ = 0; | |
| 34 | |
| 35 this.fileFilter_ = fileFilter; | |
| 36 this.fileFilter_.addEventListener('changed', | |
| 37 this.onFilterChanged_.bind(this)); | |
| 38 | |
| 39 this.currentFileListContext_ = new FileListContext( | |
| 40 fileFilter, metadataCache); | |
| 41 this.currentDirContents_ = | |
| 42 DirectoryContents.createForDirectory(this.currentFileListContext_, null); | |
| 43 | |
| 44 this.metadataCache_ = metadataCache; | |
| 45 | |
| 46 this.volumeManager_ = volumeManager; | |
| 47 this.volumeManager_.volumeInfoList.addEventListener( | |
| 48 'splice', this.onVolumeInfoListUpdated_.bind(this)); | |
| 49 | |
| 50 this.fileWatcher_ = fileWatcher; | |
| 51 this.fileWatcher_.addEventListener( | |
| 52 'watcher-directory-changed', | |
| 53 this.onWatcherDirectoryChanged_.bind(this)); | |
| 54 } | |
| 55 | |
| 56 /** | |
| 57 * DirectoryModel extends cr.EventTarget. | |
| 58 */ | |
| 59 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype; | |
| 60 | |
| 61 /** | |
| 62 * Disposes the directory model by removing file watchers. | |
| 63 */ | |
| 64 DirectoryModel.prototype.dispose = function() { | |
| 65 this.fileWatcher_.dispose(); | |
| 66 }; | |
| 67 | |
| 68 /** | |
| 69 * @return {cr.ui.ArrayDataModel} Files in the current directory. | |
| 70 */ | |
| 71 DirectoryModel.prototype.getFileList = function() { | |
| 72 return this.currentFileListContext_.fileList; | |
| 73 }; | |
| 74 | |
| 75 /** | |
| 76 * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection | |
| 77 * in the fileList. | |
| 78 */ | |
| 79 DirectoryModel.prototype.getFileListSelection = function() { | |
| 80 return this.fileListSelection_; | |
| 81 }; | |
| 82 | |
| 83 /** | |
| 84 * @return {?RootType} Root type of current root, or null if not found. | |
| 85 */ | |
| 86 DirectoryModel.prototype.getCurrentRootType = function() { | |
| 87 var entry = this.currentDirContents_.getDirectoryEntry(); | |
| 88 if (!entry) | |
| 89 return null; | |
| 90 | |
| 91 var locationInfo = this.volumeManager_.getLocationInfo(entry); | |
| 92 if (!locationInfo) | |
| 93 return null; | |
| 94 | |
| 95 return locationInfo.rootType; | |
| 96 }; | |
| 97 | |
| 98 /** | |
| 99 * @return {boolean} True if the current directory is read only. If there is | |
| 100 * no entry set, then returns true. | |
| 101 */ | |
| 102 DirectoryModel.prototype.isReadOnly = function() { | |
| 103 var currentDirEntry = this.getCurrentDirEntry(); | |
| 104 if (currentDirEntry) { | |
| 105 var locationInfo = this.volumeManager_.getLocationInfo(currentDirEntry); | |
| 106 if (locationInfo) | |
| 107 return locationInfo.isReadOnly; | |
| 108 } | |
| 109 return true; | |
| 110 }; | |
| 111 | |
| 112 /** | |
| 113 * @return {boolean} True if the a scan is active. | |
| 114 */ | |
| 115 DirectoryModel.prototype.isScanning = function() { | |
| 116 return this.currentDirContents_.isScanning(); | |
| 117 }; | |
| 118 | |
| 119 /** | |
| 120 * @return {boolean} True if search is in progress. | |
| 121 */ | |
| 122 DirectoryModel.prototype.isSearching = function() { | |
| 123 return this.currentDirContents_.isSearch(); | |
| 124 }; | |
| 125 | |
| 126 /** | |
| 127 * Updates the selection by using the updateFunc and publish the change event. | |
| 128 * If updateFunc returns true, it force to dispatch the change event even if the | |
| 129 * selection index is not changed. | |
| 130 * | |
| 131 * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection | |
| 132 * Selection to be updated. | |
| 133 * @param {function(): boolean} updateFunc Function updating the selection. | |
| 134 * @private | |
| 135 */ | |
| 136 DirectoryModel.prototype.updateSelectionAndPublishEvent_ = | |
| 137 function(selection, updateFunc) { | |
| 138 // Begin change. | |
| 139 selection.beginChange(); | |
| 140 | |
| 141 // If dispatchNeeded is true, we should ensure the change event is | |
| 142 // dispatched. | |
| 143 var dispatchNeeded = updateFunc(); | |
| 144 | |
| 145 // Check if the change event is dispatched in the endChange function | |
| 146 // or not. | |
| 147 var eventDispatched = function() { dispatchNeeded = false; }; | |
| 148 selection.addEventListener('change', eventDispatched); | |
| 149 selection.endChange(); | |
| 150 selection.removeEventListener('change', eventDispatched); | |
| 151 | |
| 152 // If the change event have been already dispatched, dispatchNeeded is false. | |
| 153 if (dispatchNeeded) { | |
| 154 var event = new Event('change'); | |
| 155 // The selection status (selected or not) is not changed because | |
| 156 // this event is caused by the change of selected item. | |
| 157 event.changes = []; | |
| 158 selection.dispatchEvent(event); | |
| 159 } | |
| 160 }; | |
| 161 | |
| 162 /** | |
| 163 * Invoked when a change in the directory is detected by the watcher. | |
| 164 * @private | |
| 165 */ | |
| 166 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() { | |
| 167 this.rescanSoon(); | |
| 168 }; | |
| 169 | |
| 170 /** | |
| 171 * Invoked when filters are changed. | |
| 172 * @private | |
| 173 */ | |
| 174 DirectoryModel.prototype.onFilterChanged_ = function() { | |
| 175 this.rescanSoon(); | |
| 176 }; | |
| 177 | |
| 178 /** | |
| 179 * Returns the filter. | |
| 180 * @return {FileFilter} The file filter. | |
| 181 */ | |
| 182 DirectoryModel.prototype.getFileFilter = function() { | |
| 183 return this.fileFilter_; | |
| 184 }; | |
| 185 | |
| 186 /** | |
| 187 * @return {DirectoryEntry} Current directory. | |
| 188 */ | |
| 189 DirectoryModel.prototype.getCurrentDirEntry = function() { | |
| 190 return this.currentDirContents_.getDirectoryEntry(); | |
| 191 }; | |
| 192 | |
| 193 /** | |
| 194 * @return {Array.<Entry>} Array of selected entries. | |
| 195 * @private | |
| 196 */ | |
| 197 DirectoryModel.prototype.getSelectedEntries_ = function() { | |
| 198 var indexes = this.fileListSelection_.selectedIndexes; | |
| 199 var fileList = this.getFileList(); | |
| 200 if (fileList) { | |
| 201 return indexes.map(function(i) { | |
| 202 return fileList.item(i); | |
| 203 }); | |
| 204 } | |
| 205 return []; | |
| 206 }; | |
| 207 | |
| 208 /** | |
| 209 * @param {Array.<Entry>} value List of selected entries. | |
| 210 * @private | |
| 211 */ | |
| 212 DirectoryModel.prototype.setSelectedEntries_ = function(value) { | |
| 213 var indexes = []; | |
| 214 var fileList = this.getFileList(); | |
| 215 var urls = util.entriesToURLs(value); | |
| 216 | |
| 217 for (var i = 0; i < fileList.length; i++) { | |
| 218 if (urls.indexOf(fileList.item(i).toURL()) !== -1) | |
| 219 indexes.push(i); | |
| 220 } | |
| 221 this.fileListSelection_.selectedIndexes = indexes; | |
| 222 }; | |
| 223 | |
| 224 /** | |
| 225 * @return {Entry} Lead entry. | |
| 226 * @private | |
| 227 */ | |
| 228 DirectoryModel.prototype.getLeadEntry_ = function() { | |
| 229 var index = this.fileListSelection_.leadIndex; | |
| 230 return index >= 0 && this.getFileList().item(index); | |
| 231 }; | |
| 232 | |
| 233 /** | |
| 234 * @param {Entry} value The new lead entry. | |
| 235 * @private | |
| 236 */ | |
| 237 DirectoryModel.prototype.setLeadEntry_ = function(value) { | |
| 238 var fileList = this.getFileList(); | |
| 239 for (var i = 0; i < fileList.length; i++) { | |
| 240 if (util.isSameEntry(fileList.item(i), value)) { | |
| 241 this.fileListSelection_.leadIndex = i; | |
| 242 return; | |
| 243 } | |
| 244 } | |
| 245 }; | |
| 246 | |
| 247 /** | |
| 248 * Schedule rescan with short delay. | |
| 249 */ | |
| 250 DirectoryModel.prototype.rescanSoon = function() { | |
| 251 this.scheduleRescan(SHORT_RESCAN_INTERVAL); | |
| 252 }; | |
| 253 | |
| 254 /** | |
| 255 * Schedule rescan with delay. Designed to handle directory change | |
| 256 * notification. | |
| 257 */ | |
| 258 DirectoryModel.prototype.rescanLater = function() { | |
| 259 this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL); | |
| 260 }; | |
| 261 | |
| 262 /** | |
| 263 * Schedule rescan with delay. If another rescan has been scheduled does | |
| 264 * nothing. File operation may cause a few notifications what should cause | |
| 265 * a single refresh. | |
| 266 * @param {number} delay Delay in ms after which the rescan will be performed. | |
| 267 */ | |
| 268 DirectoryModel.prototype.scheduleRescan = function(delay) { | |
| 269 if (this.rescanTime_) { | |
| 270 if (this.rescanTime_ <= Date.now() + delay) | |
| 271 return; | |
| 272 clearTimeout(this.rescanTimeoutId_); | |
| 273 } | |
| 274 | |
| 275 this.rescanTime_ = Date.now() + delay; | |
| 276 this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay); | |
| 277 }; | |
| 278 | |
| 279 /** | |
| 280 * Cancel a rescan on timeout if it is scheduled. | |
| 281 * @private | |
| 282 */ | |
| 283 DirectoryModel.prototype.clearRescanTimeout_ = function() { | |
| 284 this.rescanTime_ = null; | |
| 285 if (this.rescanTimeoutId_) { | |
| 286 clearTimeout(this.rescanTimeoutId_); | |
| 287 this.rescanTimeoutId_ = null; | |
| 288 } | |
| 289 }; | |
| 290 | |
| 291 /** | |
| 292 * Rescan current directory. May be called indirectly through rescanLater or | |
| 293 * directly in order to reflect user action. Will first cache all the directory | |
| 294 * contents in an array, then seamlessly substitute the fileList contents, | |
| 295 * preserving the select element etc. | |
| 296 * | |
| 297 * This should be to scan the contents of current directory (or search). | |
| 298 */ | |
| 299 DirectoryModel.prototype.rescan = function() { | |
| 300 this.clearRescanTimeout_(); | |
| 301 if (this.runningScan_) { | |
| 302 this.pendingRescan_ = true; | |
| 303 return; | |
| 304 } | |
| 305 | |
| 306 var dirContents = this.currentDirContents_.clone(); | |
| 307 dirContents.setFileList([]); | |
| 308 | |
| 309 var successCallback = (function() { | |
| 310 this.replaceDirectoryContents_(dirContents); | |
| 311 cr.dispatchSimpleEvent(this, 'rescan-completed'); | |
| 312 }).bind(this); | |
| 313 | |
| 314 this.scan_(dirContents, | |
| 315 successCallback, function() {}, function() {}, function() {}); | |
| 316 }; | |
| 317 | |
| 318 /** | |
| 319 * Run scan on the current DirectoryContents. The active fileList is cleared and | |
| 320 * the entries are added directly. | |
| 321 * | |
| 322 * This should be used when changing directory or initiating a new search. | |
| 323 * | |
| 324 * @param {DirectoryContentes} newDirContents New DirectoryContents instance to | |
| 325 * replace currentDirContents_. | |
| 326 * @param {function()=} opt_callback Called on success. | |
| 327 * @private | |
| 328 */ | |
| 329 DirectoryModel.prototype.clearAndScan_ = function(newDirContents, | |
| 330 opt_callback) { | |
| 331 if (this.currentDirContents_.isScanning()) | |
| 332 this.currentDirContents_.cancelScan(); | |
| 333 this.currentDirContents_ = newDirContents; | |
| 334 this.clearRescanTimeout_(); | |
| 335 | |
| 336 if (this.pendingScan_) | |
| 337 this.pendingScan_ = false; | |
| 338 | |
| 339 if (this.runningScan_) { | |
| 340 if (this.runningScan_.isScanning()) | |
| 341 this.runningScan_.cancelScan(); | |
| 342 this.runningScan_ = null; | |
| 343 } | |
| 344 | |
| 345 var onDone = function() { | |
| 346 cr.dispatchSimpleEvent(this, 'scan-completed'); | |
| 347 if (opt_callback) | |
| 348 opt_callback(); | |
| 349 }.bind(this); | |
| 350 | |
| 351 var onFailed = function() { | |
| 352 cr.dispatchSimpleEvent(this, 'scan-failed'); | |
| 353 }.bind(this); | |
| 354 | |
| 355 var onUpdated = function() { | |
| 356 cr.dispatchSimpleEvent(this, 'scan-updated'); | |
| 357 }.bind(this); | |
| 358 | |
| 359 var onCancelled = function() { | |
| 360 cr.dispatchSimpleEvent(this, 'scan-cancelled'); | |
| 361 }.bind(this); | |
| 362 | |
| 363 // Clear the table, and start scanning. | |
| 364 cr.dispatchSimpleEvent(this, 'scan-started'); | |
| 365 var fileList = this.getFileList(); | |
| 366 fileList.splice(0, fileList.length); | |
| 367 this.scan_(this.currentDirContents_, | |
| 368 onDone, onFailed, onUpdated, onCancelled); | |
| 369 }; | |
| 370 | |
| 371 /** | |
| 372 * Perform a directory contents scan. Should be called only from rescan() and | |
| 373 * clearAndScan_(). | |
| 374 * | |
| 375 * @param {DirectoryContents} dirContents DirectoryContents instance on which | |
| 376 * the scan will be run. | |
| 377 * @param {function()} successCallback Callback on success. | |
| 378 * @param {function()} failureCallback Callback on failure. | |
| 379 * @param {function()} updatedCallback Callback on update. Only on the last | |
| 380 * update, {@code successCallback} is called instead of this. | |
| 381 * @param {function()} cancelledCallback Callback on cancel. | |
| 382 * @private | |
| 383 */ | |
| 384 DirectoryModel.prototype.scan_ = function( | |
| 385 dirContents, | |
| 386 successCallback, failureCallback, updatedCallback, cancelledCallback) { | |
| 387 var self = this; | |
| 388 | |
| 389 /** | |
| 390 * Runs pending scan if there is one. | |
| 391 * | |
| 392 * @return {boolean} Did pending scan exist. | |
| 393 */ | |
| 394 var maybeRunPendingRescan = function() { | |
| 395 if (this.pendingRescan_) { | |
| 396 this.rescanSoon(); | |
| 397 this.pendingRescan_ = false; | |
| 398 return true; | |
| 399 } | |
| 400 return false; | |
| 401 }.bind(this); | |
| 402 | |
| 403 var onSuccess = function() { | |
| 404 // Record metric for Downloads directory. | |
| 405 if (!dirContents.isSearch()) { | |
| 406 var locationInfo = | |
| 407 this.volumeManager_.getLocationInfo(dirContents.getDirectoryEntry()); | |
| 408 if (locationInfo.volumeInfo.volumeType === util.VolumeType.DOWNLOADS && | |
| 409 locationInfo.isRootEntry) { | |
| 410 metrics.recordMediumCount('DownloadsCount', | |
| 411 dirContents.fileList_.length); | |
| 412 } | |
| 413 } | |
| 414 | |
| 415 this.runningScan_ = null; | |
| 416 successCallback(); | |
| 417 this.scanFailures_ = 0; | |
| 418 maybeRunPendingRescan(); | |
| 419 }.bind(this); | |
| 420 | |
| 421 var onFailure = function() { | |
| 422 this.runningScan_ = null; | |
| 423 this.scanFailures_++; | |
| 424 failureCallback(); | |
| 425 | |
| 426 if (maybeRunPendingRescan()) | |
| 427 return; | |
| 428 | |
| 429 if (this.scanFailures_ <= 1) | |
| 430 this.rescanLater(); | |
| 431 }.bind(this); | |
| 432 | |
| 433 this.runningScan_ = dirContents; | |
| 434 | |
| 435 dirContents.addEventListener('scan-completed', onSuccess); | |
| 436 dirContents.addEventListener('scan-updated', updatedCallback); | |
| 437 dirContents.addEventListener('scan-failed', onFailure); | |
| 438 dirContents.addEventListener('scan-cancelled', cancelledCallback); | |
| 439 dirContents.scan(); | |
| 440 }; | |
| 441 | |
| 442 /** | |
| 443 * @param {DirectoryContents} dirContents DirectoryContents instance. | |
| 444 * @private | |
| 445 */ | |
| 446 DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) { | |
| 447 cr.dispatchSimpleEvent(this, 'begin-update-files'); | |
| 448 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() { | |
| 449 var selectedEntries = this.getSelectedEntries_(); | |
| 450 var selectedIndices = this.fileListSelection_.selectedIndexes; | |
| 451 | |
| 452 // Restore leadIndex in case leadName no longer exists. | |
| 453 var leadIndex = this.fileListSelection_.leadIndex; | |
| 454 var leadEntry = this.getLeadEntry_(); | |
| 455 | |
| 456 this.currentDirContents_ = dirContents; | |
| 457 dirContents.replaceContextFileList(); | |
| 458 | |
| 459 this.setSelectedEntries_(selectedEntries); | |
| 460 this.fileListSelection_.leadIndex = leadIndex; | |
| 461 this.setLeadEntry_(leadEntry); | |
| 462 | |
| 463 // If nothing is selected after update, then select file next to the | |
| 464 // latest selection | |
| 465 var forceChangeEvent = false; | |
| 466 if (this.fileListSelection_.selectedIndexes.length == 0 && | |
| 467 selectedIndices.length != 0) { | |
| 468 var maxIdx = Math.max.apply(null, selectedIndices); | |
| 469 this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2, | |
| 470 this.getFileList().length) - 1); | |
| 471 forceChangeEvent = true; | |
| 472 } | |
| 473 return forceChangeEvent; | |
| 474 }.bind(this)); | |
| 475 | |
| 476 cr.dispatchSimpleEvent(this, 'end-update-files'); | |
| 477 }; | |
| 478 | |
| 479 /** | |
| 480 * Callback when an entry is changed. | |
| 481 * @param {util.EntryChangedKind} kind How the entry is changed. | |
| 482 * @param {Entry} entry The changed entry. | |
| 483 */ | |
| 484 DirectoryModel.prototype.onEntryChanged = function(kind, entry) { | |
| 485 // TODO(hidehiko): We should update directory model even the search result | |
| 486 // is shown. | |
| 487 var rootType = this.getCurrentRootType(); | |
| 488 if ((rootType === RootType.DRIVE || | |
| 489 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
| 490 rootType === RootType.DRIVE_RECENT || | |
| 491 rootType === RootType.DRIVE_OFFLINE) && | |
| 492 this.isSearching()) | |
| 493 return; | |
| 494 | |
| 495 if (kind == util.EntryChangedKind.CREATED) { | |
| 496 // Refresh the cache. | |
| 497 this.metadataCache_.clear([entry], '*'); | |
| 498 entry.getParent(function(parentEntry) { | |
| 499 if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) { | |
| 500 // Do nothing if current directory changed during async operations. | |
| 501 return; | |
| 502 } | |
| 503 this.currentDirContents_.prefetchMetadata([entry], function() { | |
| 504 if (!util.isSameEntry(this.getCurrentDirEntry(), parentEntry)) { | |
| 505 // Do nothing if current directory changed during async operations. | |
| 506 return; | |
| 507 } | |
| 508 | |
| 509 var index = this.findIndexByEntry_(entry); | |
| 510 if (index >= 0) | |
| 511 this.getFileList().replaceItem(this.getFileList().item(index), entry); | |
| 512 else | |
| 513 this.getFileList().push(entry); | |
| 514 }.bind(this)); | |
| 515 }.bind(this)); | |
| 516 } else { | |
| 517 // This is the delete event. | |
| 518 var index = this.findIndexByEntry_(entry); | |
| 519 if (index >= 0) | |
| 520 this.getFileList().splice(index, 1); | |
| 521 } | |
| 522 }; | |
| 523 | |
| 524 /** | |
| 525 * @param {Entry} entry The entry to be searched. | |
| 526 * @return {number} The index in the fileList, or -1 if not found. | |
| 527 * @private | |
| 528 */ | |
| 529 DirectoryModel.prototype.findIndexByEntry_ = function(entry) { | |
| 530 var fileList = this.getFileList(); | |
| 531 for (var i = 0; i < fileList.length; i++) { | |
| 532 if (util.isSameEntry(fileList.item(i), entry)) | |
| 533 return i; | |
| 534 } | |
| 535 return -1; | |
| 536 }; | |
| 537 | |
| 538 /** | |
| 539 * Called when rename is done successfully. | |
| 540 * Note: conceptually, DirectoryModel should work without this, because entries | |
| 541 * can be renamed by other systems anytime and Files.app should reflect it | |
| 542 * correctly. | |
| 543 * TODO(hidehiko): investigate more background, and remove this if possible. | |
| 544 * | |
| 545 * @param {Entry} oldEntry The old entry. | |
| 546 * @param {Entry} newEntry The new entry. | |
| 547 * @param {function()} opt_callback Called on completion. | |
| 548 */ | |
| 549 DirectoryModel.prototype.onRenameEntry = function( | |
| 550 oldEntry, newEntry, opt_callback) { | |
| 551 this.currentDirContents_.prefetchMetadata([newEntry], function() { | |
| 552 // If the current directory is the old entry, then quietly change to the | |
| 553 // new one. | |
| 554 if (util.isSameEntry(oldEntry, this.getCurrentDirEntry())) | |
| 555 this.changeDirectoryEntry(newEntry); | |
| 556 | |
| 557 // Replace the old item with the new item. | |
| 558 // If the entry doesn't exist in the list, it has been updated from | |
| 559 // outside (probably by directory rescan) and is just ignored. | |
| 560 this.getFileList().replaceItem(oldEntry, newEntry); | |
| 561 | |
| 562 // Run callback, finally. | |
| 563 if (opt_callback) | |
| 564 opt_callback(); | |
| 565 }.bind(this)); | |
| 566 }; | |
| 567 | |
| 568 /** | |
| 569 * Creates directory and updates the file list. | |
| 570 * | |
| 571 * @param {string} name Directory name. | |
| 572 * @param {function(DirectoryEntry)} successCallback Callback on success. | |
| 573 * @param {function(FileError)} errorCallback Callback on failure. | |
| 574 */ | |
| 575 DirectoryModel.prototype.createDirectory = function(name, | |
| 576 successCallback, | |
| 577 errorCallback) { | |
| 578 // Obtain and check the current directory. | |
| 579 var entry = this.getCurrentDirEntry(); | |
| 580 if (!entry || this.isSearching()) { | |
| 581 errorCallback(util.createDOMError( | |
| 582 util.FileError.INVALID_MODIFICATION_ERR)); | |
| 583 return; | |
| 584 } | |
| 585 | |
| 586 var tracker = this.createDirectoryChangeTracker(); | |
| 587 tracker.start(); | |
| 588 | |
| 589 new Promise(entry.getDirectory.bind( | |
| 590 entry, name, {create: true, exclusive: true})). | |
| 591 | |
| 592 then(function(newEntry) { | |
| 593 // Refresh the cache. | |
| 594 this.metadataCache_.clear([newEntry], '*'); | |
| 595 return new Promise(function(onFulfilled, onRejected) { | |
| 596 this.metadataCache_.get([newEntry], | |
| 597 'filesystem', | |
| 598 onFulfilled.bind(null, newEntry)); | |
| 599 }.bind(this)); | |
| 600 }.bind(this)). | |
| 601 | |
| 602 then(function(newEntry) { | |
| 603 // Do not change anything or call the callback if current | |
| 604 // directory changed. | |
| 605 tracker.stop(); | |
| 606 if (tracker.hasChanged) | |
| 607 return; | |
| 608 | |
| 609 // If target directory is already in the list, just select it. | |
| 610 var existing = this.getFileList().slice().filter( | |
| 611 function(e) { return e.name === name; }); | |
| 612 if (existing.length) { | |
| 613 this.selectEntry(newEntry); | |
| 614 successCallback(existing[0]); | |
| 615 } else { | |
| 616 this.fileListSelection_.beginChange(); | |
| 617 this.getFileList().splice(0, 0, newEntry); | |
| 618 this.selectEntry(newEntry); | |
| 619 this.fileListSelection_.endChange(); | |
| 620 successCallback(newEntry); | |
| 621 } | |
| 622 }.bind(this), function(reason) { | |
| 623 tracker.stop(); | |
| 624 errorCallback(reason); | |
| 625 }); | |
| 626 }; | |
| 627 | |
| 628 /** | |
| 629 * Change the current directory to the directory represented by | |
| 630 * a DirectoryEntry or a fake entry. | |
| 631 * | |
| 632 * Dispatches the 'directory-changed' event when the directory is successfully | |
| 633 * changed. | |
| 634 * | |
| 635 * @param {DirectoryEntry|Object} dirEntry The entry of the new directory to | |
| 636 * be opened. | |
| 637 * @param {function()=} opt_callback Executed if the directory loads | |
| 638 * successfully. | |
| 639 */ | |
| 640 DirectoryModel.prototype.changeDirectoryEntry = function( | |
| 641 dirEntry, opt_callback) { | |
| 642 // Increment the sequence value. | |
| 643 this.changeDirectorySequence_++; | |
| 644 this.clearSearch_(); | |
| 645 | |
| 646 var promise = new Promise( | |
| 647 function(onFulfilled, onRejected) { | |
| 648 this.fileWatcher_.changeWatchedDirectory(dirEntry, onFulfilled); | |
| 649 }.bind(this)). | |
| 650 | |
| 651 then(function(sequence) { | |
| 652 return new Promise(function(onFulfilled, onRejected) { | |
| 653 if (this.changeDirectorySequence_ !== sequence) | |
| 654 return; | |
| 655 | |
| 656 var newDirectoryContents = this.createDirectoryContents_( | |
| 657 this.currentFileListContext_, dirEntry, ''); | |
| 658 if (!newDirectoryContents) | |
| 659 return; | |
| 660 | |
| 661 var previousDirEntry = this.currentDirContents_.getDirectoryEntry(); | |
| 662 this.clearAndScan_(newDirectoryContents, opt_callback); | |
| 663 | |
| 664 // For tests that open the dialog to empty directories, everything is | |
| 665 // loaded at this point. | |
| 666 util.testSendMessage('directory-change-complete'); | |
| 667 | |
| 668 var event = new Event('directory-changed'); | |
| 669 event.previousDirEntry = previousDirEntry; | |
| 670 event.newDirEntry = dirEntry; | |
| 671 this.dispatchEvent(event); | |
| 672 }.bind(this)); | |
| 673 }.bind(this, this.changeDirectorySequence_)); | |
| 674 }; | |
| 675 | |
| 676 /** | |
| 677 * Clears the selection in the file list. | |
| 678 */ | |
| 679 DirectoryModel.prototype.clearSelection = function() { | |
| 680 this.setSelectedEntries_([]); | |
| 681 }; | |
| 682 | |
| 683 /** | |
| 684 * Creates an object which could say whether directory has changed while it has | |
| 685 * been active or not. Designed for long operations that should be cancelled | |
| 686 * if the used change current directory. | |
| 687 * @return {Object} Created object. | |
| 688 */ | |
| 689 DirectoryModel.prototype.createDirectoryChangeTracker = function() { | |
| 690 var tracker = { | |
| 691 dm_: this, | |
| 692 active_: false, | |
| 693 hasChanged: false, | |
| 694 | |
| 695 start: function() { | |
| 696 if (!this.active_) { | |
| 697 this.dm_.addEventListener('directory-changed', | |
| 698 this.onDirectoryChange_); | |
| 699 this.active_ = true; | |
| 700 this.hasChanged = false; | |
| 701 } | |
| 702 }, | |
| 703 | |
| 704 stop: function() { | |
| 705 if (this.active_) { | |
| 706 this.dm_.removeEventListener('directory-changed', | |
| 707 this.onDirectoryChange_); | |
| 708 this.active_ = false; | |
| 709 } | |
| 710 }, | |
| 711 | |
| 712 onDirectoryChange_: function(event) { | |
| 713 tracker.stop(); | |
| 714 tracker.hasChanged = true; | |
| 715 } | |
| 716 }; | |
| 717 return tracker; | |
| 718 }; | |
| 719 | |
| 720 /** | |
| 721 * @param {Entry} entry Entry to be selected. | |
| 722 */ | |
| 723 DirectoryModel.prototype.selectEntry = function(entry) { | |
| 724 var fileList = this.getFileList(); | |
| 725 for (var i = 0; i < fileList.length; i++) { | |
| 726 if (fileList.item(i).toURL() === entry.toURL()) { | |
| 727 this.selectIndex(i); | |
| 728 return; | |
| 729 } | |
| 730 } | |
| 731 }; | |
| 732 | |
| 733 /** | |
| 734 * @param {Array.<string>} entries Array of entries. | |
| 735 */ | |
| 736 DirectoryModel.prototype.selectEntries = function(entries) { | |
| 737 // URLs are needed here, since we are comparing Entries by URLs. | |
| 738 var urls = util.entriesToURLs(entries); | |
| 739 var fileList = this.getFileList(); | |
| 740 this.fileListSelection_.beginChange(); | |
| 741 this.fileListSelection_.unselectAll(); | |
| 742 for (var i = 0; i < fileList.length; i++) { | |
| 743 if (urls.indexOf(fileList.item(i).toURL()) >= 0) | |
| 744 this.fileListSelection_.setIndexSelected(i, true); | |
| 745 } | |
| 746 this.fileListSelection_.endChange(); | |
| 747 }; | |
| 748 | |
| 749 /** | |
| 750 * @param {number} index Index of file. | |
| 751 */ | |
| 752 DirectoryModel.prototype.selectIndex = function(index) { | |
| 753 // this.focusCurrentList_(); | |
| 754 if (index >= this.getFileList().length) | |
| 755 return; | |
| 756 | |
| 757 // If a list bound with the model it will do scrollIndexIntoView(index). | |
| 758 this.fileListSelection_.selectedIndex = index; | |
| 759 }; | |
| 760 | |
| 761 /** | |
| 762 * Handles update of VolumeInfoList. | |
| 763 * @param {Event} event Event of VolumeInfoList's 'splice'. | |
| 764 * @private | |
| 765 */ | |
| 766 DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) { | |
| 767 // When the volume where we are is unmounted, fallback to the default volume's | |
| 768 // root. If current directory path is empty, stop the fallback | |
| 769 // since the current directory is initializing now. | |
| 770 if (this.getCurrentDirEntry() && | |
| 771 !this.volumeManager_.getVolumeInfo(this.getCurrentDirEntry())) { | |
| 772 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) { | |
| 773 this.changeDirectoryEntry(displayRoot); | |
| 774 }.bind(this)); | |
| 775 } | |
| 776 }; | |
| 777 | |
| 778 /** | |
| 779 * Creates directory contents for the entry and query. | |
| 780 * | |
| 781 * @param {FileListContext} context File list context. | |
| 782 * @param {DirectoryEntry} entry Current directory. | |
| 783 * @param {string=} opt_query Search query string. | |
| 784 * @return {DirectoryContents} Directory contents. | |
| 785 * @private | |
| 786 */ | |
| 787 DirectoryModel.prototype.createDirectoryContents_ = | |
| 788 function(context, entry, opt_query) { | |
| 789 var query = (opt_query || '').trimLeft(); | |
| 790 var locationInfo = this.volumeManager_.getLocationInfo(entry); | |
| 791 if (!locationInfo) | |
| 792 return null; | |
| 793 var canUseDriveSearch = this.volumeManager_.getDriveConnectionState().type !== | |
| 794 util.DriveConnectionType.OFFLINE && | |
| 795 locationInfo.isDriveBased; | |
| 796 | |
| 797 if (query && canUseDriveSearch) { | |
| 798 // Drive search. | |
| 799 return DirectoryContents.createForDriveSearch(context, entry, query); | |
| 800 } else if (query) { | |
| 801 // Local search. | |
| 802 return DirectoryContents.createForLocalSearch(context, entry, query); | |
| 803 } if (locationInfo.isSpecialSearchRoot) { | |
| 804 // Drive special search. | |
| 805 var searchType; | |
| 806 switch (locationInfo.rootType) { | |
| 807 case RootType.DRIVE_OFFLINE: | |
| 808 searchType = | |
| 809 DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE; | |
| 810 break; | |
| 811 case RootType.DRIVE_SHARED_WITH_ME: | |
| 812 searchType = | |
| 813 DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME; | |
| 814 break; | |
| 815 case RootType.DRIVE_RECENT: | |
| 816 searchType = | |
| 817 DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES; | |
| 818 break; | |
| 819 default: | |
| 820 // Unknown special search entry. | |
| 821 throw new Error('Unknown special search type.'); | |
| 822 } | |
| 823 return DirectoryContents.createForDriveMetadataSearch( | |
| 824 context, | |
| 825 entry, | |
| 826 searchType); | |
| 827 } else { | |
| 828 // Local fetch or search. | |
| 829 return DirectoryContents.createForDirectory(context, entry); | |
| 830 } | |
| 831 }; | |
| 832 | |
| 833 /** | |
| 834 * Performs search and displays results. The search type is dependent on the | |
| 835 * current directory. If we are currently on drive, server side content search | |
| 836 * over drive mount point. If the current directory is not on the drive, file | |
| 837 * name search over current directory will be performed. | |
| 838 * | |
| 839 * @param {string} query Query that will be searched for. | |
| 840 * @param {function(Event)} onSearchRescan Function that will be called when the | |
| 841 * search directory is rescanned (i.e. search results are displayed). | |
| 842 * @param {function()} onClearSearch Function to be called when search state | |
| 843 * gets cleared. | |
| 844 * TODO(olege): Change callbacks to events. | |
| 845 */ | |
| 846 DirectoryModel.prototype.search = function(query, | |
| 847 onSearchRescan, | |
| 848 onClearSearch) { | |
| 849 this.clearSearch_(); | |
| 850 var currentDirEntry = this.getCurrentDirEntry(); | |
| 851 if (!currentDirEntry) { | |
| 852 // Not yet initialized. Do nothing. | |
| 853 return; | |
| 854 } | |
| 855 | |
| 856 if (!(query || '').trimLeft()) { | |
| 857 if (this.isSearching()) { | |
| 858 var newDirContents = this.createDirectoryContents_( | |
| 859 this.currentFileListContext_, | |
| 860 currentDirEntry); | |
| 861 this.clearAndScan_(newDirContents); | |
| 862 } | |
| 863 return; | |
| 864 } | |
| 865 | |
| 866 var newDirContents = this.createDirectoryContents_( | |
| 867 this.currentFileListContext_, currentDirEntry, query); | |
| 868 if (!newDirContents) | |
| 869 return; | |
| 870 | |
| 871 this.onSearchCompleted_ = onSearchRescan; | |
| 872 this.onClearSearch_ = onClearSearch; | |
| 873 this.addEventListener('scan-completed', this.onSearchCompleted_); | |
| 874 this.clearAndScan_(newDirContents); | |
| 875 }; | |
| 876 | |
| 877 /** | |
| 878 * In case the search was active, remove listeners and send notifications on | |
| 879 * its canceling. | |
| 880 * @private | |
| 881 */ | |
| 882 DirectoryModel.prototype.clearSearch_ = function() { | |
| 883 if (!this.isSearching()) | |
| 884 return; | |
| 885 | |
| 886 if (this.onSearchCompleted_) { | |
| 887 this.removeEventListener('scan-completed', this.onSearchCompleted_); | |
| 888 this.onSearchCompleted_ = null; | |
| 889 } | |
| 890 | |
| 891 if (this.onClearSearch_) { | |
| 892 this.onClearSearch_(); | |
| 893 this.onClearSearch_ = null; | |
| 894 } | |
| 895 }; | |
| OLD | NEW |