- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / navigation_list_model.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * Entry of NavigationListModel. This constructor should be called only from
9  * the helper methods (NavigationModelItem.create).
10  *
11  * @param {string} path Path.
12  * @param {DirectoryEntry} entry Entry. Can be null.
13  * @constructor
14  */
15 function NavigationModelItem(path, entry) {
16   this.path_ = path;
17   this.entry_ = entry;
18   this.resolvingQueue_ = new AsyncUtil.Queue();
19
20   Object.seal(this);
21 }
22
23 NavigationModelItem.prototype = {
24   get path() { return this.path_; },
25 };
26
27 /**
28  * Returns the cached entry of the item. This may return NULL if the target is
29  * not available on the filesystem, is not resolved or is under resolving the
30  * entry.
31  *
32  * @return {Entry} Cached entry.
33  */
34 NavigationModelItem.prototype.getCachedEntry = function() {
35   return this.entry_;
36 };
37
38 /**
39  * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance.
40  * @param {string} path Path.
41  * @param {DirectoryEntry} entry Entry. Can be null.
42  * @param {function(FileError)} errorCallback Called when the resolving is
43  *     failed with the error.
44  * @return {NavigationModelItem} Created NavigationModelItem.
45  */
46 NavigationModelItem.create = function(
47     volumeManager, path, entry, errorCallback) {
48   var item = new NavigationModelItem(path, entry);
49
50   // If the given entry is null, try to resolve path to get an entry.
51   if (!entry) {
52     item.resolvingQueue_.run(function(continueCallback) {
53       volumeManager.resolvePath(
54           path,
55           function(entry) {
56             if (entry.isDirectory)
57               item.entry_ = entry;
58             else
59               errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
60             continueCallback();
61           },
62           function(error) {
63             errorCallback(error);
64             continueCallback();
65           });
66     });
67   }
68   return item;
69 };
70
71 /**
72  * Retrieves the entry. If the entry is being retrieved, waits until it
73  * finishes.
74  * @param {function(Entry)} callback Called with the resolved entry. The entry
75  *     may be NULL if resolving is failed.
76  */
77 NavigationModelItem.prototype.getEntryAsync = function(callback) {
78   // If resolving the entry is running, wait until it finishes.
79   this.resolvingQueue_.run(function(continueCallback) {
80     callback(this.entry_);
81     continueCallback();
82   }.bind(this));
83 };
84
85 /**
86  * Returns if this item is a shortcut or a volume root.
87  * @return {boolean} True if a shortcut, false if a volume root.
88  */
89 NavigationModelItem.prototype.isShortcut = function() {
90   return !PathUtil.isRootPath(this.path_);
91 };
92
93 /**
94  * A navigation list model. This model combines the 2 lists.
95  * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance.
96  * @param {cr.ui.ArrayDataModel} shortcutListModel The list of folder shortcut.
97  * @constructor
98  * @extends {cr.EventTarget}
99  */
100 function NavigationListModel(volumeManager, shortcutListModel) {
101   cr.EventTarget.call(this);
102
103   this.volumeManager_ = volumeManager;
104   this.shortcutListModel_ = shortcutListModel;
105
106   var volumeInfoToModelItem = function(volumeInfo) {
107     if (volumeInfo.volumeType == util.VolumeType.DRIVE) {
108       // For drive volume, we assign the path to "My Drive".
109       return NavigationModelItem.create(
110           this.volumeManager_,
111           volumeInfo.mountPath + '/root',
112           null,
113           function() {});
114     } else {
115       return NavigationModelItem.create(
116           this.volumeManager_,
117           volumeInfo.mountPath,
118           volumeInfo.root,
119           function() {});
120     }
121   }.bind(this);
122
123   var pathToModelItem = function(path) {
124     var item = NavigationModelItem.create(
125         this.volumeManager_,
126         path,
127         null,  // Entry will be resolved.
128         function(error) {
129           if (error.code == FileError.NOT_FOUND_ERR)
130             this.onItemNotFoundError(item);
131          }.bind(this));
132     return item;
133   }.bind(this);
134
135   /**
136    * Type of updated list.
137    * @enum {number}
138    * @const
139    */
140   var ListType = {
141     VOLUME_LIST: 1,
142     SHORTCUT_LIST: 2
143   };
144   Object.freeze(ListType);
145
146   // Generates this.volumeList_ and this.shortcutList_ from the models.
147   this.volumeList_ =
148       this.volumeManager_.volumeInfoList.slice().map(volumeInfoToModelItem);
149
150   this.shortcutList_ = [];
151   for (var i = 0; i < this.shortcutListModel_.length; i++) {
152     var shortcutPath = this.shortcutListModel_.item(i);
153     var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ?
154         RootDirectory.DRIVE :
155         PathUtil.getRootPath(shortcutPath);
156     var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath);
157     var isMounted = volumeInfo && !volumeInfo.error;
158     if (isMounted)
159       this.shortcutList_.push(pathToModelItem(shortcutPath));
160   }
161
162   // Generates a combined 'permuted' event from an event of either list.
163   var permutedHandler = function(listType, event) {
164     var permutation;
165
166     // Build the volumeList.
167     if (listType == ListType.VOLUME_LIST) {
168       // The volume is mounted or unmounted.
169       var newList = [];
170
171       // Use the old instances if they just move.
172       for (var i = 0; i < event.permutation.length; i++) {
173         if (event.permutation[i] >= 0)
174           newList[event.permutation[i]] = this.volumeList_[i];
175       }
176
177       // Create missing instances.
178       for (var i = 0; i < event.newLength; i++) {
179         if (!newList[i]) {
180           newList[i] = volumeInfoToModelItem(
181               this.volumeManager_.volumeInfoList.item(i));
182         }
183       }
184       this.volumeList_ = newList;
185
186       permutation = event.permutation.slice();
187     } else {
188       // volumeList part has not been changed, so the permutation should be
189       // idenetity mapping.
190       permutation = [];
191       for (var i = 0; i < this.volumeList_.length; i++)
192         permutation[i] = i;
193     }
194
195     // Build the shortcutList. Even if the event is for the volumeInfoList
196     // update, the short cut path may be unmounted or newly mounted. So, here
197     // shortcutList will always be re-built.
198     // Currently this code may be redundant, as shortcut folder is supported
199     // only on Drive File System and we can assume single-profile, but
200     // multi-profile will be supported later.
201     // The shortcut list is sorted in case-insensitive lexicographical order.
202     // So we just can traverse the two list linearly.
203     var modelIndex = 0;
204     var oldListIndex = 0;
205     var newList = [];
206     while (modelIndex < this.shortcutListModel_.length &&
207            oldListIndex < this.shortcutList_.length) {
208       var shortcutPath = this.shortcutListModel_.item(modelIndex);
209       var cmp = this.shortcutListModel_.compare(
210           shortcutPath, this.shortcutList_[oldListIndex].path);
211       if (cmp > 0) {
212         // The shortcut at shortcutList_[oldListIndex] is removed.
213         permutation.push(-1);
214         oldListIndex++;
215         continue;
216       }
217
218       // Check if the volume where the shortcutPath is is mounted or not.
219       var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ?
220           RootDirectory.DRIVE :
221           PathUtil.getRootPath(shortcutPath);
222       var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath);
223       var isMounted = volumeInfo && !volumeInfo.error;
224       if (cmp == 0) {
225         // There exists an old NavigationModelItem instance.
226         if (isMounted) {
227           // Reuse the old instance.
228           permutation.push(newList.length + this.volumeList_.length);
229           newList.push(this.shortcutList_[oldListIndex]);
230         } else {
231           permutation.push(-1);
232         }
233         oldListIndex++;
234       } else {
235         // We needs to create a new instance for the shortcut path.
236         if (isMounted)
237           newList.push(pathToModelItem(shortcutPath));
238       }
239       modelIndex++;
240     }
241
242     // Add remaining (new) shortcuts if necessary.
243     for (; modelIndex < this.shortcutListModel_.length; modelIndex++) {
244       var shortcutPath = this.shortcutListModel_.item(modelIndex);
245       var mountPath = PathUtil.isDriveBasedPath(shortcutPath) ?
246           RootDirectory.DRIVE :
247           PathUtil.getRootPath(shortcutPath);
248       var volumeInfo = this.volumeManager_.getVolumeInfo(mountPath);
249       var isMounted = volumeInfo && !volumeInfo.error;
250       if (isMounted)
251         newList.push(pathToModelItem(shortcutPath));
252     }
253
254     // Fill remaining permutation if necessary.
255     for (; oldListIndex < this.shortcutList_.length; oldListIndex++)
256       permutation.push(-1);
257
258     this.shortcutList_ = newList;
259
260     // Dispatch permuted event.
261     var permutedEvent = new Event('permuted');
262     permutedEvent.newLength =
263         this.volumeList_.length + this.shortcutList_.length;
264     permutedEvent.permutation = permutation;
265     this.dispatchEvent(permutedEvent);
266   };
267
268   this.volumeManager_.volumeInfoList.addEventListener(
269       'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST));
270   this.shortcutListModel_.addEventListener(
271       'permuted', permutedHandler.bind(this, ListType.SHORTCUT_LIST));
272
273   // 'change' event is just ignored, because it is not fired neither in
274   // the folder shortcut list nor in the volume info list.
275   // 'splice' and 'sorted' events are not implemented, since they are not used
276   // in list.js.
277 }
278
279 /**
280  * NavigationList inherits cr.EventTarget.
281  */
282 NavigationListModel.prototype = {
283   __proto__: cr.EventTarget.prototype,
284   get length() { return this.length_(); },
285   get folderShortcutList() { return this.shortcutList_; }
286 };
287
288 /**
289  * Returns the item at the given index.
290  * @param {number} index The index of the entry to get.
291  * @return {?string} The path at the given index.
292  */
293 NavigationListModel.prototype.item = function(index) {
294   var offset = this.volumeList_.length;
295   if (index < offset)
296     return this.volumeList_[index];
297   return this.shortcutList_[index - offset];
298 };
299
300 /**
301  * Returns the number of items in the model.
302  * @return {number} The length of the model.
303  * @private
304  */
305 NavigationListModel.prototype.length_ = function() {
306   return this.volumeList_.length + this.shortcutList_.length;
307 };
308
309 /**
310  * Returns the first matching item.
311  * @param {NavigationModelItem} modelItem The entry to find.
312  * @param {number=} opt_fromIndex If provided, then the searching start at
313  *     the {@code opt_fromIndex}.
314  * @return {number} The index of the first found element or -1 if not found.
315  */
316 NavigationListModel.prototype.indexOf = function(modelItem, opt_fromIndex) {
317   for (var i = opt_fromIndex || 0; i < this.length; i++) {
318     if (modelItem === this.item(i))
319       return i;
320   }
321   return -1;
322 };
323
324 /**
325  * Called when one od the items is not found on the filesystem.
326  * @param {NavigationModelItem} modelItem The entry which is not found.
327  */
328 NavigationListModel.prototype.onItemNotFoundError = function(modelItem) {
329   var index = this.indexOf(modelItem);
330   if (index === -1) {
331     // Invalid modelItem.
332   } else if (index < this.volumeList_.length) {
333     // The item is in the volume list.
334     // Not implemented.
335     // TODO(yoshiki): Implement it when necessary.
336   } else {
337     // The item is in the folder shortcut list.
338     if (this.isDriveMounted())
339       this.shortcutListModel_.remove(modelItem.path);
340   }
341 };
342
343 /**
344  * Returns if the drive is mounted or not.
345  * @return {boolean} True if the drive is mounted, false otherwise.
346  */
347 NavigationListModel.prototype.isDriveMounted = function() {
348   return !!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
349 };