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