Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(656)

Side by Side Diff: chrome/browser/resources/file_manager/js/directory_model.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 * @param {boolean} showSpecialSearchRoots True if special-search roots are
23 * available. They should be hidden for the dialogs to save files.
24 * @constructor
25 */
26 function DirectoryModel(singleSelection, fileFilter, fileWatcher,
27 metadataCache, volumeManager,
28 showSpecialSearchRoots) {
29 this.fileListSelection_ = singleSelection ?
30 new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel();
31
32 this.runningScan_ = null;
33 this.pendingScan_ = null;
34 this.rescanTime_ = null;
35 this.scanFailures_ = 0;
36 this.showSpecialSearchRoots_ = showSpecialSearchRoots;
37
38 this.fileFilter_ = fileFilter;
39 this.fileFilter_.addEventListener('changed',
40 this.onFilterChanged_.bind(this));
41
42 this.currentFileListContext_ = new FileListContext(
43 fileFilter, metadataCache);
44 this.currentDirContents_ =
45 DirectoryContents.createForDirectory(this.currentFileListContext_, null);
46
47 this.volumeManager_ = volumeManager;
48 this.volumeManager_.volumeInfoList.addEventListener(
49 'splice', this.onVolumeInfoListUpdated_.bind(this));
50
51 this.fileWatcher_ = fileWatcher;
52 this.fileWatcher_.addEventListener(
53 'watcher-directory-changed',
54 this.onWatcherDirectoryChanged_.bind(this));
55 }
56
57 /**
58 * Fake entry to be used in currentDirEntry_ when current directory is
59 * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other".
60 * @type {Object}
61 * @const
62 * @private
63 */
64 DirectoryModel.fakeDriveEntry_ = {
65 fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT,
66 isDirectory: true
67 };
68
69 /**
70 * Fake entry representing a psuedo directory, which contains Drive files
71 * available offline. This entry works as a trigger to start a search for
72 * offline files.
73 * @type {Object}
74 * @const
75 * @private
76 */
77 DirectoryModel.fakeDriveOfflineEntry_ = {
78 fullPath: RootDirectory.DRIVE_OFFLINE,
79 isDirectory: true
80 };
81
82 /**
83 * Fake entry representing a pseudo directory, which contains shared-with-me
84 * Drive files. This entry works as a trigger to start a search for
85 * shared-with-me files.
86 * @type {Object}
87 * @const
88 * @private
89 */
90 DirectoryModel.fakeDriveSharedWithMeEntry_ = {
91 fullPath: RootDirectory.DRIVE_SHARED_WITH_ME,
92 isDirectory: true
93 };
94
95 /**
96 * Fake entry representing a pseudo directory, which contains Drive files
97 * accessed recently. This entry works as a trigger to start a metadata search
98 * implemented as DirectoryContentsDriveRecent.
99 * DirectoryModel is responsible to start the search when the UI tries to open
100 * this fake entry (e.g. changeDirectory()).
101 * @type {Object}
102 * @const
103 * @private
104 */
105 DirectoryModel.fakeDriveRecentEntry_ = {
106 fullPath: RootDirectory.DRIVE_RECENT,
107 isDirectory: true
108 };
109
110 /**
111 * List of fake entries for special searches.
112 *
113 * @type {Array.<Object>}
114 * @const
115 */
116 DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [
117 DirectoryModel.fakeDriveSharedWithMeEntry_,
118 DirectoryModel.fakeDriveRecentEntry_,
119 DirectoryModel.fakeDriveOfflineEntry_
120 ];
121
122 /**
123 * DirectoryModel extends cr.EventTarget.
124 */
125 DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype;
126
127 /**
128 * Disposes the directory model by removing file watchers.
129 */
130 DirectoryModel.prototype.dispose = function() {
131 this.fileWatcher_.dispose();
132 };
133
134 /**
135 * @return {cr.ui.ArrayDataModel} Files in the current directory.
136 */
137 DirectoryModel.prototype.getFileList = function() {
138 return this.currentFileListContext_.fileList;
139 };
140
141 /**
142 * Sort the file list.
143 * @param {string} sortField Sort field.
144 * @param {string} sortDirection "asc" or "desc".
145 */
146 DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) {
147 this.getFileList().sort(sortField, sortDirection);
148 };
149
150 /**
151 * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection
152 * in the fileList.
153 */
154 DirectoryModel.prototype.getFileListSelection = function() {
155 return this.fileListSelection_;
156 };
157
158 /**
159 * @return {RootType} Root type of current root.
160 */
161 DirectoryModel.prototype.getCurrentRootType = function() {
162 var entry = this.currentDirContents_.getDirectoryEntry();
163 return PathUtil.getRootType(entry ? entry.fullPath : '');
164 };
165
166 /**
167 * @return {string} Root path.
168 */
169 DirectoryModel.prototype.getCurrentRootPath = function() {
170 var entry = this.currentDirContents_.getDirectoryEntry();
171 return entry ? PathUtil.getRootPath(entry.fullPath) : '';
172 };
173
174 /**
175 * @return {string} Filesystem URL representing the mountpoint for the current
176 * contents.
177 */
178 DirectoryModel.prototype.getCurrentMountPointUrl = function() {
179 var rootPath = this.getCurrentRootPath();
180 // Special search roots are just showing a search results from DRIVE.
181 if (PathUtil.getRootType(rootPath) == RootType.DRIVE ||
182 PathUtil.isSpecialSearchRoot(rootPath))
183 return util.makeFilesystemUrl(RootDirectory.DRIVE);
184
185 return util.makeFilesystemUrl(rootPath);
186 };
187
188 /**
189 * @return {boolean} on True if offline.
190 */
191 DirectoryModel.prototype.isDriveOffline = function() {
192 var connection = this.volumeManager_.getDriveConnectionState();
193 return connection.type == util.DriveConnectionType.OFFLINE;
194 };
195
196 /**
197 * TODO(haruki): This actually checks the current root. Fix the method name and
198 * related code.
199 * @return {boolean} True if the root for the current directory is read only.
200 */
201 DirectoryModel.prototype.isReadOnly = function() {
202 return this.isPathReadOnly(this.getCurrentRootPath());
203 };
204
205 /**
206 * @return {boolean} True if the a scan is active.
207 */
208 DirectoryModel.prototype.isScanning = function() {
209 return this.currentDirContents_.isScanning();
210 };
211
212 /**
213 * @return {boolean} True if search is in progress.
214 */
215 DirectoryModel.prototype.isSearching = function() {
216 return this.currentDirContents_.isSearch();
217 };
218
219 /**
220 * @param {string} path Path to check.
221 * @return {boolean} True if the |path| is read only.
222 */
223 DirectoryModel.prototype.isPathReadOnly = function(path) {
224 // TODO(hidehiko): Migrate this into VolumeInfo.
225 switch (PathUtil.getRootType(path)) {
226 case RootType.REMOVABLE:
227 var volumeInfo = this.volumeManager_.getVolumeInfo(
228 PathUtil.getRootPath(path));
229 // Returns true if the volume is actually read only, or if an error
230 // is found during the mounting.
231 // TODO(hidehiko): Remove "error" check here, by removing error'ed volume
232 // info from VolumeManager.
233 return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error);
234 case RootType.ARCHIVE:
235 return true;
236 case RootType.DOWNLOADS:
237 return false;
238 case RootType.DRIVE:
239 // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the
240 // offline tab.
241 return this.isDriveOffline();
242 default:
243 return true;
244 }
245 };
246
247 /**
248 * Updates the selection by using the updateFunc and publish the change event.
249 * If updateFunc returns true, it force to dispatch the change event even if the
250 * selection index is not changed.
251 *
252 * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection
253 * Selection to be updated.
254 * @param {function(): boolean} updateFunc Function updating the selection.
255 * @private
256 */
257 DirectoryModel.prototype.updateSelectionAndPublishEvent_ =
258 function(selection, updateFunc) {
259 // Begin change.
260 selection.beginChange();
261
262 // If dispatchNeeded is true, we should ensure the change event is
263 // dispatched.
264 var dispatchNeeded = updateFunc();
265
266 // Check if the change event is dispatched in the endChange function
267 // or not.
268 var eventDispatched = function() { dispatchNeeded = false; };
269 selection.addEventListener('change', eventDispatched);
270 selection.endChange();
271 selection.removeEventListener('change', eventDispatched);
272
273 // If the change event have been already dispatched, dispatchNeeded is false.
274 if (dispatchNeeded) {
275 var event = new Event('change');
276 // The selection status (selected or not) is not changed because
277 // this event is caused by the change of selected item.
278 event.changes = [];
279 selection.dispatchEvent(event);
280 }
281 };
282
283 /**
284 * Invoked when a change in the directory is detected by the watcher.
285 * @private
286 */
287 DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
288 this.rescanSoon();
289 };
290
291 /**
292 * Invoked when filters are changed.
293 * @private
294 */
295 DirectoryModel.prototype.onFilterChanged_ = function() {
296 this.rescanSoon();
297 };
298
299 /**
300 * Returns the filter.
301 * @return {FileFilter} The file filter.
302 */
303 DirectoryModel.prototype.getFileFilter = function() {
304 return this.fileFilter_;
305 };
306
307 /**
308 * @return {DirectoryEntry} Current directory.
309 */
310 DirectoryModel.prototype.getCurrentDirEntry = function() {
311 return this.currentDirContents_.getDirectoryEntry();
312 };
313
314 /**
315 * @return {string} URL of the current directory. or null if unavailable.
316 */
317 DirectoryModel.prototype.getCurrentDirectoryURL = function() {
318 var entry = this.currentDirContents_.getDirectoryEntry();
319 if (!entry)
320 return null;
321 if (entry === DirectoryModel.fakeDriveOfflineEntry_)
322 return util.makeFilesystemUrl(entry.fullPath);
323 return entry.toURL();
324 };
325
326 /**
327 * @return {string} Path for the current directory, or empty string if the
328 * current directory is not yet set.
329 */
330 DirectoryModel.prototype.getCurrentDirPath = function() {
331 var entry = this.currentDirContents_.getDirectoryEntry();
332 return entry ? entry.fullPath : '';
333 };
334
335 /**
336 * @return {Array.<string>} File paths of selected files.
337 * @private
338 */
339 DirectoryModel.prototype.getSelectedPaths_ = function() {
340 var indexes = this.fileListSelection_.selectedIndexes;
341 var fileList = this.getFileList();
342 if (fileList) {
343 return indexes.map(function(i) {
344 return fileList.item(i).fullPath;
345 });
346 }
347 return [];
348 };
349
350 /**
351 * @param {Array.<string>} value List of file paths of selected files.
352 * @private
353 */
354 DirectoryModel.prototype.setSelectedPaths_ = function(value) {
355 var indexes = [];
356 var fileList = this.getFileList();
357
358 var safeKey = function(key) {
359 // The transformation must:
360 // 1. Never generate a reserved name ('__proto__')
361 // 2. Keep different keys different.
362 return '#' + key;
363 };
364
365 var hash = {};
366
367 for (var i = 0; i < value.length; i++)
368 hash[safeKey(value[i])] = 1;
369
370 for (var i = 0; i < fileList.length; i++) {
371 if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath)))
372 indexes.push(i);
373 }
374 this.fileListSelection_.selectedIndexes = indexes;
375 };
376
377 /**
378 * @return {string} Lead item file path.
379 * @private
380 */
381 DirectoryModel.prototype.getLeadPath_ = function() {
382 var index = this.fileListSelection_.leadIndex;
383 return index >= 0 && this.getFileList().item(index).fullPath;
384 };
385
386 /**
387 * @param {string} value The name of new lead index.
388 * @private
389 */
390 DirectoryModel.prototype.setLeadPath_ = function(value) {
391 var fileList = this.getFileList();
392 for (var i = 0; i < fileList.length; i++) {
393 if (fileList.item(i).fullPath === value) {
394 this.fileListSelection_.leadIndex = i;
395 return;
396 }
397 }
398 };
399
400 /**
401 * Schedule rescan with short delay.
402 */
403 DirectoryModel.prototype.rescanSoon = function() {
404 this.scheduleRescan(SHORT_RESCAN_INTERVAL);
405 };
406
407 /**
408 * Schedule rescan with delay. Designed to handle directory change
409 * notification.
410 */
411 DirectoryModel.prototype.rescanLater = function() {
412 this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL);
413 };
414
415 /**
416 * Schedule rescan with delay. If another rescan has been scheduled does
417 * nothing. File operation may cause a few notifications what should cause
418 * a single refresh.
419 * @param {number} delay Delay in ms after which the rescan will be performed.
420 */
421 DirectoryModel.prototype.scheduleRescan = function(delay) {
422 if (this.rescanTime_) {
423 if (this.rescanTime_ <= Date.now() + delay)
424 return;
425 clearTimeout(this.rescanTimeoutId_);
426 }
427
428 this.rescanTime_ = Date.now() + delay;
429 this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay);
430 };
431
432 /**
433 * Cancel a rescan on timeout if it is scheduled.
434 * @private
435 */
436 DirectoryModel.prototype.clearRescanTimeout_ = function() {
437 this.rescanTime_ = null;
438 if (this.rescanTimeoutId_) {
439 clearTimeout(this.rescanTimeoutId_);
440 this.rescanTimeoutId_ = null;
441 }
442 };
443
444 /**
445 * Rescan current directory. May be called indirectly through rescanLater or
446 * directly in order to reflect user action. Will first cache all the directory
447 * contents in an array, then seamlessly substitute the fileList contents,
448 * preserving the select element etc.
449 *
450 * This should be to scan the contents of current directory (or search).
451 */
452 DirectoryModel.prototype.rescan = function() {
453 this.clearRescanTimeout_();
454 if (this.runningScan_) {
455 this.pendingRescan_ = true;
456 return;
457 }
458
459 var dirContents = this.currentDirContents_.clone();
460 dirContents.setFileList([]);
461
462 var successCallback = (function() {
463 this.replaceDirectoryContents_(dirContents);
464 cr.dispatchSimpleEvent(this, 'rescan-completed');
465 }).bind(this);
466
467 this.scan_(dirContents,
468 successCallback, function() {}, function() {}, function() {});
469 };
470
471 /**
472 * Run scan on the current DirectoryContents. The active fileList is cleared and
473 * the entries are added directly.
474 *
475 * This should be used when changing directory or initiating a new search.
476 *
477 * @param {DirectoryContentes} newDirContents New DirectoryContents instance to
478 * replace currentDirContents_.
479 * @param {function()=} opt_callback Called on success.
480 * @private
481 */
482 DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
483 opt_callback) {
484 if (this.currentDirContents_.isScanning())
485 this.currentDirContents_.cancelScan();
486 this.currentDirContents_ = newDirContents;
487 this.clearRescanTimeout_();
488
489 if (this.pendingScan_)
490 this.pendingScan_ = false;
491
492 if (this.runningScan_) {
493 if (this.runningScan_.isScanning())
494 this.runningScan_.cancelScan();
495 this.runningScan_ = null;
496 }
497
498 var onDone = function() {
499 cr.dispatchSimpleEvent(this, 'scan-completed');
500 if (opt_callback)
501 opt_callback();
502 }.bind(this);
503
504 var onFailed = function() {
505 cr.dispatchSimpleEvent(this, 'scan-failed');
506 }.bind(this);
507
508 var onUpdated = function() {
509 cr.dispatchSimpleEvent(this, 'scan-updated');
510 }.bind(this);
511
512 var onCancelled = function() {
513 cr.dispatchSimpleEvent(this, 'scan-cancelled');
514 }.bind(this);
515
516 // Clear the table, and start scanning.
517 cr.dispatchSimpleEvent(this, 'scan-started');
518 var fileList = this.getFileList();
519 fileList.splice(0, fileList.length);
520 this.scan_(this.currentDirContents_,
521 onDone, onFailed, onUpdated, onCancelled);
522 };
523
524 /**
525 * Perform a directory contents scan. Should be called only from rescan() and
526 * clearAndScan_().
527 *
528 * @param {DirectoryContents} dirContents DirectoryContents instance on which
529 * the scan will be run.
530 * @param {function()} successCallback Callback on success.
531 * @param {function()} failureCallback Callback on failure.
532 * @param {function()} updatedCallback Callback on update. Only on the last
533 * update, {@code successCallback} is called instead of this.
534 * @param {function()} cancelledCallback Callback on cancel.
535 * @private
536 */
537 DirectoryModel.prototype.scan_ = function(
538 dirContents,
539 successCallback, failureCallback, updatedCallback, cancelledCallback) {
540 var self = this;
541
542 /**
543 * Runs pending scan if there is one.
544 *
545 * @return {boolean} Did pending scan exist.
546 */
547 var maybeRunPendingRescan = function() {
548 if (self.pendingRescan_) {
549 self.rescanSoon();
550 self.pendingRescan_ = false;
551 return true;
552 }
553 return false;
554 };
555
556 var onSuccess = function() {
557 self.runningScan_ = null;
558 successCallback();
559 self.scanFailures_ = 0;
560 maybeRunPendingRescan();
561 };
562
563 var onFailure = function() {
564 self.runningScan_ = null;
565 self.scanFailures_++;
566 failureCallback();
567
568 if (maybeRunPendingRescan())
569 return;
570
571 if (self.scanFailures_ <= 1)
572 self.rescanLater();
573 };
574
575 this.runningScan_ = dirContents;
576
577 dirContents.addEventListener('scan-completed', onSuccess);
578 dirContents.addEventListener('scan-updated', updatedCallback);
579 dirContents.addEventListener('scan-failed', onFailure);
580 dirContents.addEventListener('scan-cancelled', cancelledCallback);
581 dirContents.scan();
582 };
583
584 /**
585 * @param {DirectoryContents} dirContents DirectoryContents instance.
586 * @private
587 */
588 DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) {
589 cr.dispatchSimpleEvent(this, 'begin-update-files');
590 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
591 var selectedPaths = this.getSelectedPaths_();
592 var selectedIndices = this.fileListSelection_.selectedIndexes;
593
594 // Restore leadIndex in case leadName no longer exists.
595 var leadIndex = this.fileListSelection_.leadIndex;
596 var leadPath = this.getLeadPath_();
597
598 this.currentDirContents_ = dirContents;
599 dirContents.replaceContextFileList();
600
601 this.setSelectedPaths_(selectedPaths);
602 this.fileListSelection_.leadIndex = leadIndex;
603 this.setLeadPath_(leadPath);
604
605 // If nothing is selected after update, then select file next to the
606 // latest selection
607 var forceChangeEvent = false;
608 if (this.fileListSelection_.selectedIndexes.length == 0 &&
609 selectedIndices.length != 0) {
610 var maxIdx = Math.max.apply(null, selectedIndices);
611 this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2,
612 this.getFileList().length) - 1);
613 forceChangeEvent = true;
614 }
615 return forceChangeEvent;
616 }.bind(this));
617
618 cr.dispatchSimpleEvent(this, 'end-update-files');
619 };
620
621 /**
622 * Callback when an entry is changed.
623 * @param {util.EntryChangedKind} kind How the entry is changed.
624 * @param {Entry} entry The changed entry.
625 */
626 DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
627 // TODO(hidehiko): We should update directory model even the search result
628 // is shown.
629 var rootType = this.getCurrentRootType();
630 if ((rootType === RootType.DRIVE ||
631 rootType === RootType.DRIVE_SHARED_WITH_ME ||
632 rootType === RootType.DRIVE_RECENT ||
633 rootType === RootType.DRIVE_OFFLINE) &&
634 this.isSearching())
635 return;
636
637 if (kind == util.EntryChangedKind.CREATED) {
638 entry.getParent(function(parentEntry) {
639 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
640 // Do nothing if current directory changed during async operations.
641 return;
642 }
643 this.currentDirContents_.prefetchMetadata([entry], function() {
644 if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
645 // Do nothing if current directory changed during async operations.
646 return;
647 }
648
649 var index = this.findIndexByEntry_(entry);
650 if (index >= 0)
651 this.getFileList().splice(index, 1, entry);
652 else
653 this.getFileList().push(entry);
654 }.bind(this));
655 }.bind(this));
656 } else {
657 // This is the delete event.
658 var index = this.findIndexByEntry_(entry);
659 if (index >= 0)
660 this.getFileList().splice(index, 1);
661 }
662 };
663
664 /**
665 * @param {Entry} entry The entry to be searched.
666 * @return {number} The index in the fileList, or -1 if not found.
667 * @private
668 */
669 DirectoryModel.prototype.findIndexByEntry_ = function(entry) {
670 var fileList = this.getFileList();
671 for (var i = 0; i < fileList.length; i++) {
672 if (util.isSameEntry(fileList.item(i), entry))
673 return i;
674 }
675 return -1;
676 };
677
678 /**
679 * Called when rename is done successfully.
680 * Note: conceptually, DirectoryModel should work without this, because entries
681 * can be renamed by other systems anytime and Files.app should reflect it
682 * correctly.
683 * TODO(hidehiko): investigate more background, and remove this if possible.
684 *
685 * @param {Entry} oldEntry The old entry.
686 * @param {Entry} newEntry The new entry.
687 * @param {function()} opt_callback Called on completion.
688 */
689 DirectoryModel.prototype.onRenameEntry = function(
690 oldEntry, newEntry, opt_callback) {
691 this.currentDirContents_.prefetchMetadata([newEntry], function() {
692 // If the current directory is the old entry, then quietly change to the
693 // new one.
694 if (util.isSameEntry(oldEntry, this.getCurrentDirEntry()))
695 this.changeDirectory(newEntry.fullPath);
696
697 // Look for the old entry.
698 // If the entry doesn't exist in the list, it has been updated from
699 // outside (probably by directory rescan).
700 var index = this.findIndexByEntry_(oldEntry);
701 if (index >= 0) {
702 // Update the content list and selection status.
703 var wasSelected = this.fileListSelection_.getIndexSelected(index);
704 this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
705 this.fileListSelection_.setIndexSelected(index, false);
706 this.getFileList().splice(index, 1, newEntry);
707 if (wasSelected) {
708 // We re-search the index, because splice may trigger sorting so that
709 // index may be stale.
710 this.fileListSelection_.setIndexSelected(
711 this.findIndexByEntry_(newEntry), true);
712 }
713 return true;
714 }.bind(this));
715 }
716
717 // Run callback, finally.
718 if (opt_callback)
719 opt_callback();
720 }.bind(this));
721 };
722
723 /**
724 * Creates directory and updates the file list.
725 *
726 * @param {string} name Directory name.
727 * @param {function(DirectoryEntry)} successCallback Callback on success.
728 * @param {function(FileError)} errorCallback Callback on failure.
729 */
730 DirectoryModel.prototype.createDirectory = function(name, successCallback,
731 errorCallback) {
732 var entry = this.getCurrentDirEntry();
733 if (!entry) {
734 errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
735 return;
736 }
737
738 var onSuccess = function(newEntry) {
739 // Do not change anything or call the callback if current
740 // directory changed.
741 if (entry.fullPath != this.getCurrentDirPath())
742 return;
743
744 var existing = this.getFileList().slice().filter(
745 function(e) {return e.name == name;});
746
747 if (existing.length) {
748 this.selectEntry(name);
749 successCallback(existing[0]);
750 } else {
751 this.fileListSelection_.beginChange();
752 this.getFileList().splice(0, 0, newEntry);
753 this.selectEntry(name);
754 this.fileListSelection_.endChange();
755 successCallback(newEntry);
756 }
757 };
758
759 this.currentDirContents_.createDirectory(name, onSuccess.bind(this),
760 errorCallback);
761 };
762
763 /**
764 * Changes directory. Causes 'directory-change' event.
765 *
766 * @param {string} path New current directory path.
767 * @param {function(FileError)=} opt_errorCallback Executed if the change
768 * directory failed.
769 */
770 DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) {
771 if (PathUtil.isSpecialSearchRoot(path)) {
772 this.specialSearch(path, '');
773 return;
774 }
775
776 this.resolveDirectory(path, function(directoryEntry) {
777 this.changeDirectoryEntry_(directoryEntry);
778 }.bind(this), function(error) {
779 console.error('Error changing directory to ' + path + ': ', error);
780 if (opt_errorCallback)
781 opt_errorCallback(error);
782 });
783 };
784
785 /**
786 * Resolves absolute directory path. Handles Drive stub. If the drive is
787 * mounting, callbacks will be called after the mount is completed.
788 *
789 * @param {string} path Path to the directory.
790 * @param {function(DirectoryEntry)} successCallback Success callback.
791 * @param {function(FileError)} errorCallback Error callback.
792 */
793 DirectoryModel.prototype.resolveDirectory = function(
794 path, successCallback, errorCallback) {
795 if (PathUtil.getRootType(path) == RootType.DRIVE) {
796 if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) {
797 errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
798 return;
799 }
800 }
801
802 var onError = function(error) {
803 // Handle the special case, when in offline mode, and there are no cached
804 // contents on the C++ side. In such case, let's display the stub.
805 // The INVALID_STATE_ERR error code is returned from the drive filesystem
806 // in such situation.
807 //
808 // TODO(mtomasz, hashimoto): Consider rewriting this logic.
809 // crbug.com/253464.
810 if (PathUtil.getRootType(path) == RootType.DRIVE &&
811 error.code == FileError.INVALID_STATE_ERR) {
812 successCallback(DirectoryModel.fakeDriveEntry_);
813 return;
814 }
815 errorCallback(error);
816 }.bind(this);
817
818 this.volumeManager_.resolvePath(
819 path,
820 function(entry) {
821 if (entry.isFile) {
822 onError(util.createFileError(FileError.TYPE_MISMATCH_ERR));
823 return;
824 }
825 successCallback(entry);
826 },
827 onError);
828 };
829
830 /**
831 * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
832 * @param {function()=} opt_callback Executed if the directory loads
833 * successfully.
834 * @private
835 */
836 DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry,
837 opt_callback) {
838 var onScanComplete = function() {
839 if (opt_callback)
840 opt_callback();
841 // For tests that open the dialog to empty directories, everything
842 // is loaded at this point.
843 chrome.test.sendMessage('directory-change-complete');
844 };
845 this.clearAndScan_(
846 DirectoryContents.createForDirectory(this.currentFileListContext_,
847 dirEntry),
848 onScanComplete.bind(this));
849 };
850
851 /**
852 * Change the current directory to the directory represented by a
853 * DirectoryEntry.
854 *
855 * Dispatches the 'directory-changed' event when the directory is successfully
856 * changed.
857 *
858 * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
859 * @param {function()=} opt_callback Executed if the directory loads
860 * successfully.
861 * @private
862 */
863 DirectoryModel.prototype.changeDirectoryEntry_ = function(
864 dirEntry, opt_callback) {
865 this.fileWatcher_.changeWatchedDirectory(dirEntry, function() {
866 var previous = this.currentDirContents_.getDirectoryEntry();
867 this.clearSearch_();
868 this.changeDirectoryEntrySilent_(dirEntry, opt_callback);
869
870 var e = new Event('directory-changed');
871 e.previousDirEntry = previous;
872 e.newDirEntry = dirEntry;
873 this.dispatchEvent(e);
874 }.bind(this));
875 };
876
877 /**
878 * Creates an object which could say whether directory has changed while it has
879 * been active or not. Designed for long operations that should be cancelled
880 * if the used change current directory.
881 * @return {Object} Created object.
882 */
883 DirectoryModel.prototype.createDirectoryChangeTracker = function() {
884 var tracker = {
885 dm_: this,
886 active_: false,
887 hasChanged: false,
888
889 start: function() {
890 if (!this.active_) {
891 this.dm_.addEventListener('directory-changed',
892 this.onDirectoryChange_);
893 this.active_ = true;
894 this.hasChanged = false;
895 }
896 },
897
898 stop: function() {
899 if (this.active_) {
900 this.dm_.removeEventListener('directory-changed',
901 this.onDirectoryChange_);
902 this.active_ = false;
903 }
904 },
905
906 onDirectoryChange_: function(event) {
907 tracker.stop();
908 tracker.hasChanged = true;
909 }
910 };
911 return tracker;
912 };
913
914 /**
915 * Change the state of the model to reflect the specified path (either a
916 * file or directory).
917 * TODO(hidehiko): This logic should be merged with
918 * FileManager.setupCurrentDirectory_.
919 *
920 * @param {string} path The root path to use.
921 * @param {function(string, string, boolean)=} opt_pathResolveCallback Invoked
922 * as soon as the path has been resolved, and called with the base and leaf
923 * portions of the path name, and a flag indicating if the entry exists.
924 * Will be called even if another directory change happened while setupPath
925 * was in progress, but will pass |false| as |exist| parameter.
926 */
927 DirectoryModel.prototype.setupPath = function(path, opt_pathResolveCallback) {
928 var tracker = this.createDirectoryChangeTracker();
929 tracker.start();
930
931 var self = this;
932 var resolveCallback = function(directoryPath, fileName, exists) {
933 tracker.stop();
934 if (!opt_pathResolveCallback)
935 return;
936 opt_pathResolveCallback(directoryPath, fileName,
937 exists && !tracker.hasChanged);
938 };
939
940 var changeDirectoryEntry = function(directoryEntry, opt_callback) {
941 tracker.stop();
942 if (!tracker.hasChanged)
943 self.changeDirectoryEntry_(directoryEntry, opt_callback);
944 };
945
946 var EXISTS = true;
947
948 var changeToDefault = function(leafName) {
949 var def = PathUtil.DEFAULT_DIRECTORY;
950 self.resolveDirectory(def, function(directoryEntry) {
951 resolveCallback(def, leafName, !EXISTS);
952 changeDirectoryEntry(directoryEntry);
953 }, function(error) {
954 console.error('Failed to resolve default directory: ' + def, error);
955 resolveCallback('/', leafName, !EXISTS);
956 });
957 };
958
959 var noParentDirectory = function(leafName, error) {
960 console.warn('Can\'t resolve parent directory: ' + path, error);
961 changeToDefault(leafName);
962 };
963
964 if (DirectoryModel.isSystemDirectory(path)) {
965 changeToDefault('');
966 return;
967 }
968
969 this.resolveDirectory(path, function(directoryEntry) {
970 resolveCallback(directoryEntry.fullPath, '', !EXISTS);
971 changeDirectoryEntry(directoryEntry);
972 }, function(error) {
973 // Usually, leaf does not exist, because it's just a suggested file name.
974 var fileExists = error.code == FileError.TYPE_MISMATCH_ERR;
975 var nameDelimiter = path.lastIndexOf('/');
976 var parentDirectoryPath = path.substr(0, nameDelimiter);
977 var leafName = path.substr(nameDelimiter + 1);
978 if (fileExists || error.code == FileError.NOT_FOUND_ERR) {
979 if (DirectoryModel.isSystemDirectory(parentDirectoryPath)) {
980 changeToDefault(leafName);
981 return;
982 }
983 self.resolveDirectory(parentDirectoryPath,
984 function(parentDirectoryEntry) {
985 var fileName = path.substr(nameDelimiter + 1);
986 resolveCallback(parentDirectoryEntry.fullPath, fileName, fileExists);
987 changeDirectoryEntry(parentDirectoryEntry,
988 function() {
989 self.selectEntry(fileName);
990 });
991 }, noParentDirectory.bind(null, leafName));
992 } else {
993 // Unexpected errors.
994 console.error('Directory resolving error: ', error);
995 changeToDefault(leafName);
996 }
997 });
998 };
999
1000 /**
1001 * @param {string} name Filename.
1002 */
1003 DirectoryModel.prototype.selectEntry = function(name) {
1004 var fileList = this.getFileList();
1005 for (var i = 0; i < fileList.length; i++) {
1006 if (fileList.item(i).name == name) {
1007 this.selectIndex(i);
1008 return;
1009 }
1010 }
1011 };
1012
1013 /**
1014 * @param {Array.<string>} urls Array of URLs.
1015 */
1016 DirectoryModel.prototype.selectUrls = function(urls) {
1017 var fileList = this.getFileList();
1018 this.fileListSelection_.beginChange();
1019 this.fileListSelection_.unselectAll();
1020 for (var i = 0; i < fileList.length; i++) {
1021 if (urls.indexOf(fileList.item(i).toURL()) >= 0)
1022 this.fileListSelection_.setIndexSelected(i, true);
1023 }
1024 this.fileListSelection_.endChange();
1025 };
1026
1027 /**
1028 * @param {number} index Index of file.
1029 */
1030 DirectoryModel.prototype.selectIndex = function(index) {
1031 // this.focusCurrentList_();
1032 if (index >= this.getFileList().length)
1033 return;
1034
1035 // If a list bound with the model it will do scrollIndexIntoView(index).
1036 this.fileListSelection_.selectedIndex = index;
1037 };
1038
1039 /**
1040 * Called when VolumeInfoList is updated.
1041 *
1042 * @param {Event} event Event of VolumeInfoList's 'splice'.
1043 * @private
1044 */
1045 DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) {
1046 var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
1047 if (driveVolume && !driveVolume.error) {
1048 var currentDirEntry = this.getCurrentDirEntry();
1049 if (currentDirEntry) {
1050 if (currentDirEntry === DirectoryModel.fakeDriveEntry_) {
1051 // Replace the fake entry by real DirectoryEntry silently.
1052 this.volumeManager_.resolvePath(
1053 DirectoryModel.fakeDriveEntry_.fullPath,
1054 function(entry) {
1055 // If the current entry is still fake drive entry, replace it.
1056 if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_)
1057 this.changeDirectoryEntrySilent_(entry);
1058 },
1059 function(error) {});
1060 } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) {
1061 for (var i = 0; i < event.added.length; i++) {
1062 if (event.added[i].volumeType == util.VolumeType.DRIVE) {
1063 // If the Drive volume is newly mounted, rescan it.
1064 this.rescan();
1065 break;
1066 }
1067 }
1068 }
1069 }
1070 }
1071
1072 var rootPath = this.getCurrentRootPath();
1073 var rootType = PathUtil.getRootType(rootPath);
1074
1075 // If the path is on drive, reduce to the Drive's mount point.
1076 if (rootType === RootType.DRIVE)
1077 rootPath = RootDirectory.DRIVE;
1078
1079 // When the volume where we are is unmounted, fallback to
1080 // DEFAULT_DIRECTORY.
1081 // Note: during the initialization, rootType can be undefined.
1082 if (rootType && !this.volumeManager_.getVolumeInfo(rootPath))
1083 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY);
1084 };
1085
1086 /**
1087 * @param {string} path Path.
1088 * @return {boolean} If current directory is system.
1089 */
1090 DirectoryModel.isSystemDirectory = function(path) {
1091 path = path.replace(/\/+$/, '');
1092 return path === RootDirectory.REMOVABLE || path === RootDirectory.ARCHIVE;
1093 };
1094
1095 /**
1096 * Check if the root of the given path is mountable or not.
1097 *
1098 * @param {string} path Path.
1099 * @return {boolean} Return true, if the given path is under mountable root.
1100 * Otherwise, return false.
1101 */
1102 DirectoryModel.isMountableRoot = function(path) {
1103 var rootType = PathUtil.getRootType(path);
1104 switch (rootType) {
1105 case RootType.DOWNLOADS:
1106 return false;
1107 case RootType.ARCHIVE:
1108 case RootType.REMOVABLE:
1109 case RootType.DRIVE:
1110 return true;
1111 default:
1112 throw new Error('Unknown root type!');
1113 }
1114 };
1115
1116 /**
1117 * Performs search and displays results. The search type is dependent on the
1118 * current directory. If we are currently on drive, server side content search
1119 * over drive mount point. If the current directory is not on the drive, file
1120 * name search over current directory will be performed.
1121 *
1122 * @param {string} query Query that will be searched for.
1123 * @param {function(Event)} onSearchRescan Function that will be called when the
1124 * search directory is rescanned (i.e. search results are displayed).
1125 * @param {function()} onClearSearch Function to be called when search state
1126 * gets cleared.
1127 * TODO(olege): Change callbacks to events.
1128 */
1129 DirectoryModel.prototype.search = function(query,
1130 onSearchRescan,
1131 onClearSearch) {
1132 query = query.trimLeft();
1133
1134 this.clearSearch_();
1135
1136 var currentDirEntry = this.getCurrentDirEntry();
1137 if (!currentDirEntry) {
1138 // Not yet initialized. Do nothing.
1139 return;
1140 }
1141
1142 if (!query) {
1143 if (this.isSearching()) {
1144 var newDirContents = DirectoryContents.createForDirectory(
1145 this.currentFileListContext_,
1146 this.currentDirContents_.getLastNonSearchDirectoryEntry());
1147 this.clearAndScan_(newDirContents);
1148 }
1149 return;
1150 }
1151
1152 this.onSearchCompleted_ = onSearchRescan;
1153 this.onClearSearch_ = onClearSearch;
1154
1155 this.addEventListener('scan-completed', this.onSearchCompleted_);
1156
1157 // If we are offline, let's fallback to file name search inside dir.
1158 // A search initiated from directories in Drive or special search results
1159 // should trigger Drive search.
1160 var newDirContents;
1161 if (!this.isDriveOffline() &&
1162 PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) {
1163 // Drive search is performed over the whole drive, so pass drive root as
1164 // |directoryEntry|.
1165 newDirContents = DirectoryContents.createForDriveSearch(
1166 this.currentFileListContext_,
1167 currentDirEntry,
1168 this.currentDirContents_.getLastNonSearchDirectoryEntry(),
1169 query);
1170 } else {
1171 newDirContents = DirectoryContents.createForLocalSearch(
1172 this.currentFileListContext_, currentDirEntry, query);
1173 }
1174 this.clearAndScan_(newDirContents);
1175 };
1176
1177 /**
1178 * Performs special search and displays results. e.g. Drive files available
1179 * offline, shared-with-me files, recently modified files.
1180 * @param {string} path Path string representing special search. See fake
1181 * entries in PathUtil.RootDirectory.
1182 * @param {string=} opt_query Query string used for the search.
1183 */
1184 DirectoryModel.prototype.specialSearch = function(path, opt_query) {
1185 var query = opt_query || '';
1186
1187 this.clearSearch_();
1188
1189 this.onSearchCompleted_ = null;
1190 this.onClearSearch_ = null;
1191
1192 var onDriveDirectoryResolved = function(driveRoot) {
1193 if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) {
1194 // Drive root not available or not ready. onVolumeInfoListUpdated_()
1195 // handles the rescan if necessary.
1196 driveRoot = null;
1197 }
1198
1199 var specialSearchType = PathUtil.getRootType(path);
1200 var searchOption;
1201 var dirEntry;
1202 if (specialSearchType == RootType.DRIVE_OFFLINE) {
1203 dirEntry = DirectoryModel.fakeDriveOfflineEntry_;
1204 searchOption =
1205 DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE;
1206 } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) {
1207 dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_;
1208 searchOption =
1209 DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME;
1210 } else if (specialSearchType == RootType.DRIVE_RECENT) {
1211 dirEntry = DirectoryModel.fakeDriveRecentEntry_;
1212 searchOption =
1213 DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES;
1214
1215 } else {
1216 // Unknown path.
1217 this.changeDirectory(PathUtil.DEFAULT_DIRECTORY);
1218 return;
1219 }
1220
1221 var newDirContents = DirectoryContents.createForDriveMetadataSearch(
1222 this.currentFileListContext_,
1223 dirEntry, driveRoot, query, searchOption);
1224 var previous = this.currentDirContents_.getDirectoryEntry();
1225 this.clearAndScan_(newDirContents);
1226
1227 var e = new Event('directory-changed');
1228 e.previousDirEntry = previous;
1229 e.newDirEntry = dirEntry;
1230 this.dispatchEvent(e);
1231 }.bind(this);
1232
1233 this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath,
1234 onDriveDirectoryResolved /* success */,
1235 function() {} /* failed */);
1236 };
1237
1238 /**
1239 * In case the search was active, remove listeners and send notifications on
1240 * its canceling.
1241 * @private
1242 */
1243 DirectoryModel.prototype.clearSearch_ = function() {
1244 if (!this.isSearching())
1245 return;
1246
1247 if (this.onSearchCompleted_) {
1248 this.removeEventListener('scan-completed', this.onSearchCompleted_);
1249 this.onSearchCompleted_ = null;
1250 }
1251
1252 if (this.onClearSearch_) {
1253 this.onClearSearch_();
1254 this.onClearSearch_ = null;
1255 }
1256 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698