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 |