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