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