| 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 /** | |
| 8 * FileManager constructor. | |
| 9 * | |
| 10 * FileManager objects encapsulate the functionality of the file selector | |
| 11 * dialogs, as well as the full screen file manager application (though the | |
| 12 * latter is not yet implemented). | |
| 13 * | |
| 14 * @constructor | |
| 15 */ | |
| 16 function FileManager() { | |
| 17 this.initializeQueue_ = new AsyncUtil.Group(); | |
| 18 | |
| 19 /** | |
| 20 * Current list type. | |
| 21 * @type {ListType} | |
| 22 * @private | |
| 23 */ | |
| 24 this.listType_ = null; | |
| 25 | |
| 26 /** | |
| 27 * True while a user is pressing <Tab>. | |
| 28 * This is used for identifying the trigger causing the filelist to | |
| 29 * be focused. | |
| 30 * @type {boolean} | |
| 31 * @private | |
| 32 */ | |
| 33 this.pressingTab_ = false; | |
| 34 | |
| 35 /** | |
| 36 * SelectionHandler. | |
| 37 * @type {SelectionHandler} | |
| 38 * @private | |
| 39 */ | |
| 40 this.selectionHandler_ = null; | |
| 41 | |
| 42 /** | |
| 43 * VolumeInfo of the current volume. | |
| 44 * @type {VolumeInfo} | |
| 45 * @private | |
| 46 */ | |
| 47 this.currentVolumeInfo_ = null; | |
| 48 } | |
| 49 | |
| 50 FileManager.prototype = { | |
| 51 __proto__: cr.EventTarget.prototype, | |
| 52 get directoryModel() { | |
| 53 return this.directoryModel_; | |
| 54 }, | |
| 55 get navigationList() { | |
| 56 return this.navigationList_; | |
| 57 }, | |
| 58 get document() { | |
| 59 return this.document_; | |
| 60 }, | |
| 61 get fileTransferController() { | |
| 62 return this.fileTransferController_; | |
| 63 }, | |
| 64 get backgroundPage() { | |
| 65 return this.backgroundPage_; | |
| 66 }, | |
| 67 get volumeManager() { | |
| 68 return this.volumeManager_; | |
| 69 }, | |
| 70 get ui() { | |
| 71 return this.ui_; | |
| 72 } | |
| 73 }; | |
| 74 | |
| 75 /** | |
| 76 * List of dialog types. | |
| 77 * | |
| 78 * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except | |
| 79 * FULL_PAGE which is specific to this code. | |
| 80 * | |
| 81 * @enum {string} | |
| 82 */ | |
| 83 var DialogType = { | |
| 84 SELECT_FOLDER: 'folder', | |
| 85 SELECT_UPLOAD_FOLDER: 'upload-folder', | |
| 86 SELECT_SAVEAS_FILE: 'saveas-file', | |
| 87 SELECT_OPEN_FILE: 'open-file', | |
| 88 SELECT_OPEN_MULTI_FILE: 'open-multi-file', | |
| 89 FULL_PAGE: 'full-page' | |
| 90 }; | |
| 91 | |
| 92 /** | |
| 93 * @param {string} type Dialog type. | |
| 94 * @return {boolean} Whether the type is modal. | |
| 95 */ | |
| 96 DialogType.isModal = function(type) { | |
| 97 return type == DialogType.SELECT_FOLDER || | |
| 98 type == DialogType.SELECT_UPLOAD_FOLDER || | |
| 99 type == DialogType.SELECT_SAVEAS_FILE || | |
| 100 type == DialogType.SELECT_OPEN_FILE || | |
| 101 type == DialogType.SELECT_OPEN_MULTI_FILE; | |
| 102 }; | |
| 103 | |
| 104 /** | |
| 105 * @param {string} type Dialog type. | |
| 106 * @return {boolean} Whether the type is open dialog. | |
| 107 */ | |
| 108 DialogType.isOpenDialog = function(type) { | |
| 109 return type == DialogType.SELECT_OPEN_FILE || | |
| 110 type == DialogType.SELECT_OPEN_MULTI_FILE || | |
| 111 type == DialogType.SELECT_FOLDER || | |
| 112 type == DialogType.SELECT_UPLOAD_FOLDER; | |
| 113 }; | |
| 114 | |
| 115 /** | |
| 116 * @param {string} type Dialog type. | |
| 117 * @return {boolean} Whether the type is folder selection dialog. | |
| 118 */ | |
| 119 DialogType.isFolderDialog = function(type) { | |
| 120 return type == DialogType.SELECT_FOLDER || | |
| 121 type == DialogType.SELECT_UPLOAD_FOLDER; | |
| 122 }; | |
| 123 | |
| 124 /** | |
| 125 * Bottom margin of the list and tree for transparent preview panel. | |
| 126 * @const | |
| 127 */ | |
| 128 var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52; | |
| 129 | |
| 130 // Anonymous "namespace". | |
| 131 (function() { | |
| 132 | |
| 133 // Private variables and helper functions. | |
| 134 | |
| 135 /** | |
| 136 * Number of milliseconds in a day. | |
| 137 */ | |
| 138 var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; | |
| 139 | |
| 140 /** | |
| 141 * Some UI elements react on a single click and standard double click handling | |
| 142 * leads to confusing results. We ignore a second click if it comes soon | |
| 143 * after the first. | |
| 144 */ | |
| 145 var DOUBLE_CLICK_TIMEOUT = 200; | |
| 146 | |
| 147 /** | |
| 148 * Update the element to display the information about remaining space for | |
| 149 * the storage. | |
| 150 * @param {!Element} spaceInnerBar Block element for a percentage bar | |
| 151 * representing the remaining space. | |
| 152 * @param {!Element} spaceInfoLabel Inline element to contain the message. | |
| 153 * @param {!Element} spaceOuterBar Block element around the percentage bar. | |
| 154 */ | |
| 155 var updateSpaceInfo = function( | |
| 156 sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) { | |
| 157 spaceInnerBar.removeAttribute('pending'); | |
| 158 if (sizeStatsResult) { | |
| 159 var sizeStr = util.bytesToString(sizeStatsResult.remainingSize); | |
| 160 spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr); | |
| 161 | |
| 162 var usedSpace = | |
| 163 sizeStatsResult.totalSize - sizeStatsResult.remainingSize; | |
| 164 spaceInnerBar.style.width = | |
| 165 (100 * usedSpace / sizeStatsResult.totalSize) + '%'; | |
| 166 | |
| 167 spaceOuterBar.hidden = false; | |
| 168 } else { | |
| 169 spaceOuterBar.hidden = true; | |
| 170 spaceInfoLabel.textContent = str('FAILED_SPACE_INFO'); | |
| 171 } | |
| 172 }; | |
| 173 | |
| 174 // Public statics. | |
| 175 | |
| 176 FileManager.ListType = { | |
| 177 DETAIL: 'detail', | |
| 178 THUMBNAIL: 'thumb' | |
| 179 }; | |
| 180 | |
| 181 FileManager.prototype.initPreferences_ = function(callback) { | |
| 182 var group = new AsyncUtil.Group(); | |
| 183 | |
| 184 // DRIVE preferences should be initialized before creating DirectoryModel | |
| 185 // to rebuild the roots list. | |
| 186 group.add(this.getPreferences_.bind(this)); | |
| 187 | |
| 188 // Get startup preferences. | |
| 189 this.viewOptions_ = {}; | |
| 190 group.add(function(done) { | |
| 191 util.platform.getPreference(this.startupPrefName_, function(value) { | |
| 192 // Load the global default options. | |
| 193 try { | |
| 194 this.viewOptions_ = JSON.parse(value); | |
| 195 } catch (ignore) {} | |
| 196 // Override with window-specific options. | |
| 197 if (window.appState && window.appState.viewOptions) { | |
| 198 for (var key in window.appState.viewOptions) { | |
| 199 if (window.appState.viewOptions.hasOwnProperty(key)) | |
| 200 this.viewOptions_[key] = window.appState.viewOptions[key]; | |
| 201 } | |
| 202 } | |
| 203 done(); | |
| 204 }.bind(this)); | |
| 205 }.bind(this)); | |
| 206 | |
| 207 group.run(callback); | |
| 208 }; | |
| 209 | |
| 210 /** | |
| 211 * One time initialization for the file system and related things. | |
| 212 * | |
| 213 * @param {function()} callback Completion callback. | |
| 214 * @private | |
| 215 */ | |
| 216 FileManager.prototype.initFileSystemUI_ = function(callback) { | |
| 217 this.table_.startBatchUpdates(); | |
| 218 this.grid_.startBatchUpdates(); | |
| 219 | |
| 220 this.initFileList_(); | |
| 221 this.setupCurrentDirectory_(); | |
| 222 | |
| 223 // PyAuto tests monitor this state by polling this variable | |
| 224 this.__defineGetter__('workerInitialized_', function() { | |
| 225 return this.metadataCache_.isInitialized(); | |
| 226 }.bind(this)); | |
| 227 | |
| 228 this.initDateTimeFormatters_(); | |
| 229 | |
| 230 var self = this; | |
| 231 | |
| 232 // Get the 'allowRedeemOffers' preference before launching | |
| 233 // FileListBannerController. | |
| 234 this.getPreferences_(function(pref) { | |
| 235 /** @type {boolean} */ | |
| 236 var showOffers = pref['allowRedeemOffers']; | |
| 237 self.bannersController_ = new FileListBannerController( | |
| 238 self.directoryModel_, self.volumeManager_, self.document_, | |
| 239 showOffers); | |
| 240 self.bannersController_.addEventListener('relayout', | |
| 241 self.onResize_.bind(self)); | |
| 242 }); | |
| 243 | |
| 244 var dm = this.directoryModel_; | |
| 245 dm.addEventListener('directory-changed', | |
| 246 this.onDirectoryChanged_.bind(this)); | |
| 247 dm.addEventListener('begin-update-files', function() { | |
| 248 self.currentList_.startBatchUpdates(); | |
| 249 }); | |
| 250 dm.addEventListener('end-update-files', function() { | |
| 251 self.restoreItemBeingRenamed_(); | |
| 252 self.currentList_.endBatchUpdates(); | |
| 253 }); | |
| 254 dm.addEventListener('scan-started', this.onScanStarted_.bind(this)); | |
| 255 dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this)); | |
| 256 dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this)); | |
| 257 dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this)); | |
| 258 dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this)); | |
| 259 dm.addEventListener('rescan-completed', | |
| 260 this.onRescanCompleted_.bind(this)); | |
| 261 | |
| 262 this.directoryTree_.addEventListener('change', function() { | |
| 263 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); | |
| 264 }.bind(this)); | |
| 265 | |
| 266 var stateChangeHandler = | |
| 267 this.onPreferencesChanged_.bind(this); | |
| 268 chrome.fileBrowserPrivate.onPreferencesChanged.addListener( | |
| 269 stateChangeHandler); | |
| 270 stateChangeHandler(); | |
| 271 | |
| 272 var driveConnectionChangedHandler = | |
| 273 this.onDriveConnectionChanged_.bind(this); | |
| 274 this.volumeManager_.addEventListener('drive-connection-changed', | |
| 275 driveConnectionChangedHandler); | |
| 276 driveConnectionChangedHandler(); | |
| 277 | |
| 278 // Set the initial focus. | |
| 279 this.refocus(); | |
| 280 // Set it as a fallback when there is no focus. | |
| 281 this.document_.addEventListener('focusout', function(e) { | |
| 282 setTimeout(function() { | |
| 283 // When there is no focus, the active element is the <body>. | |
| 284 if (this.document_.activeElement == this.document_.body) | |
| 285 this.refocus(); | |
| 286 }.bind(this), 0); | |
| 287 }.bind(this)); | |
| 288 | |
| 289 this.initDataTransferOperations_(); | |
| 290 | |
| 291 this.initContextMenus_(); | |
| 292 this.initCommands_(); | |
| 293 | |
| 294 this.updateFileTypeFilter_(); | |
| 295 | |
| 296 this.selectionHandler_.onFileSelectionChanged(); | |
| 297 | |
| 298 this.table_.endBatchUpdates(); | |
| 299 this.grid_.endBatchUpdates(); | |
| 300 | |
| 301 callback(); | |
| 302 }; | |
| 303 | |
| 304 /** | |
| 305 * If |item| in the directory tree is behind the preview panel, scrolls up the | |
| 306 * parent view and make the item visible. This should be called when: | |
| 307 * - the selected item is changed in the directory tree. | |
| 308 * - the visibility of the the preview panel is changed. | |
| 309 * | |
| 310 * @private | |
| 311 */ | |
| 312 FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ = | |
| 313 function() { | |
| 314 var selectedSubTree = this.directoryTree_.selectedItem; | |
| 315 if (!selectedSubTree) | |
| 316 return; | |
| 317 var item = selectedSubTree.rowElement; | |
| 318 var parentView = this.directoryTree_; | |
| 319 | |
| 320 var itemRect = item.getBoundingClientRect(); | |
| 321 if (!itemRect) | |
| 322 return; | |
| 323 | |
| 324 var listRect = parentView.getBoundingClientRect(); | |
| 325 if (!listRect) | |
| 326 return; | |
| 327 | |
| 328 var previewPanel = this.dialogDom_.querySelector('.preview-panel'); | |
| 329 var previewPanelRect = previewPanel.getBoundingClientRect(); | |
| 330 var panelHeight = previewPanelRect ? previewPanelRect.height : 0; | |
| 331 | |
| 332 var itemBottom = itemRect.bottom; | |
| 333 var listBottom = listRect.bottom - panelHeight; | |
| 334 | |
| 335 if (itemBottom > listBottom) { | |
| 336 var scrollOffset = itemBottom - listBottom; | |
| 337 parentView.scrollTop += scrollOffset; | |
| 338 } | |
| 339 }; | |
| 340 | |
| 341 /** | |
| 342 * @private | |
| 343 */ | |
| 344 FileManager.prototype.initDateTimeFormatters_ = function() { | |
| 345 var use12hourClock = !this.preferences_['use24hourClock']; | |
| 346 this.table_.setDateTimeFormat(use12hourClock); | |
| 347 }; | |
| 348 | |
| 349 /** | |
| 350 * @private | |
| 351 */ | |
| 352 FileManager.prototype.initDataTransferOperations_ = function() { | |
| 353 this.fileOperationManager_ = | |
| 354 this.backgroundPage_.background.fileOperationManager; | |
| 355 | |
| 356 // CopyManager are required for 'Delete' operation in | |
| 357 // Open and Save dialogs. But drag-n-drop and copy-paste are not needed. | |
| 358 if (this.dialogType != DialogType.FULL_PAGE) return; | |
| 359 | |
| 360 // TODO(hidehiko): Extract FileOperationManager related code from | |
| 361 // FileManager to simplify it. | |
| 362 this.onCopyProgressBound_ = this.onCopyProgress_.bind(this); | |
| 363 this.fileOperationManager_.addEventListener( | |
| 364 'copy-progress', this.onCopyProgressBound_); | |
| 365 | |
| 366 this.onEntryChangedBound_ = this.onEntryChanged_.bind(this); | |
| 367 this.fileOperationManager_.addEventListener( | |
| 368 'entry-changed', this.onEntryChangedBound_); | |
| 369 | |
| 370 var controller = this.fileTransferController_ = | |
| 371 new FileTransferController(this.document_, | |
| 372 this.fileOperationManager_, | |
| 373 this.metadataCache_, | |
| 374 this.directoryModel_, | |
| 375 this.volumeManager_, | |
| 376 this.ui_.multiProfileShareDialog); | |
| 377 controller.attachDragSource(this.table_.list); | |
| 378 controller.attachFileListDropTarget(this.table_.list); | |
| 379 controller.attachDragSource(this.grid_); | |
| 380 controller.attachFileListDropTarget(this.grid_); | |
| 381 controller.attachTreeDropTarget(this.directoryTree_); | |
| 382 controller.attachNavigationListDropTarget(this.navigationList_, true); | |
| 383 controller.attachCopyPasteHandlers(); | |
| 384 controller.addEventListener('selection-copied', | |
| 385 this.blinkSelection.bind(this)); | |
| 386 controller.addEventListener('selection-cut', | |
| 387 this.blinkSelection.bind(this)); | |
| 388 controller.addEventListener('source-not-found', | |
| 389 this.onSourceNotFound_.bind(this)); | |
| 390 }; | |
| 391 | |
| 392 /** | |
| 393 * Handles an error that the source entry of file operation is not found. | |
| 394 * @private | |
| 395 */ | |
| 396 FileManager.prototype.onSourceNotFound_ = function(event) { | |
| 397 // Ensure this.sourceNotFoundErrorCount_ is integer. | |
| 398 this.sourceNotFoundErrorCount_ = ~~this.sourceNotFoundErrorCount_; | |
| 399 var item = new ProgressCenterItem(); | |
| 400 item.id = 'source-not-found-' + this.sourceNotFoundErrorCount_; | |
| 401 if (event.progressType === ProgressItemType.COPY) | |
| 402 item.message = strf('COPY_SOURCE_NOT_FOUND_ERROR', event.fileName); | |
| 403 else if (event.progressType === ProgressItemType.MOVE) | |
| 404 item.message = strf('MOVE_SOURCE_NOT_FOUND_ERROR', event.fileName); | |
| 405 item.state = ProgressItemState.ERROR; | |
| 406 this.backgroundPage_.background.progressCenter.updateItem(item); | |
| 407 this.sourceNotFoundErrorCount_++; | |
| 408 }; | |
| 409 | |
| 410 /** | |
| 411 * One-time initialization of context menus. | |
| 412 * @private | |
| 413 */ | |
| 414 FileManager.prototype.initContextMenus_ = function() { | |
| 415 this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu'); | |
| 416 cr.ui.Menu.decorate(this.fileContextMenu_); | |
| 417 | |
| 418 cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_); | |
| 419 cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'), | |
| 420 this.fileContextMenu_); | |
| 421 cr.ui.contextMenuHandler.setContextMenu( | |
| 422 this.document_.querySelector('.drive-welcome.page'), | |
| 423 this.fileContextMenu_); | |
| 424 | |
| 425 this.rootsContextMenu_ = | |
| 426 this.dialogDom_.querySelector('#roots-context-menu'); | |
| 427 cr.ui.Menu.decorate(this.rootsContextMenu_); | |
| 428 this.navigationList_.setContextMenu(this.rootsContextMenu_); | |
| 429 | |
| 430 this.directoryTreeContextMenu_ = | |
| 431 this.dialogDom_.querySelector('#directory-tree-context-menu'); | |
| 432 cr.ui.Menu.decorate(this.directoryTreeContextMenu_); | |
| 433 this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_; | |
| 434 | |
| 435 this.textContextMenu_ = | |
| 436 this.dialogDom_.querySelector('#text-context-menu'); | |
| 437 cr.ui.Menu.decorate(this.textContextMenu_); | |
| 438 | |
| 439 this.gearButton_ = this.dialogDom_.querySelector('#gear-button'); | |
| 440 this.gearButton_.addEventListener('menushow', | |
| 441 this.refreshRemainingSpace_.bind(this, | |
| 442 false /* Without loading caption. */)); | |
| 443 chrome.fileBrowserPrivate.onDesktopChanged.addListener(function() { | |
| 444 this.updateVisitDesktopMenus_(); | |
| 445 this.ui_.updateProfileBadge(); | |
| 446 }.bind(this)); | |
| 447 chrome.fileBrowserPrivate.onProfileAdded.addListener( | |
| 448 this.updateVisitDesktopMenus_.bind(this)); | |
| 449 this.updateVisitDesktopMenus_(); | |
| 450 | |
| 451 this.dialogDom_.querySelector('#gear-menu').menuItemSelector = | |
| 452 'menuitem, hr'; | |
| 453 cr.ui.decorate(this.gearButton_, cr.ui.MenuButton); | |
| 454 | |
| 455 if (this.dialogType == DialogType.FULL_PAGE) { | |
| 456 // This is to prevent the buttons from stealing focus on mouse down. | |
| 457 var preventFocus = function(event) { | |
| 458 event.preventDefault(); | |
| 459 }; | |
| 460 | |
| 461 var minimizeButton = this.dialogDom_.querySelector('#minimize-button'); | |
| 462 minimizeButton.addEventListener('click', this.onMinimize.bind(this)); | |
| 463 minimizeButton.addEventListener('mousedown', preventFocus); | |
| 464 | |
| 465 var maximizeButton = this.dialogDom_.querySelector('#maximize-button'); | |
| 466 maximizeButton.addEventListener('click', this.onMaximize.bind(this)); | |
| 467 maximizeButton.addEventListener('mousedown', preventFocus); | |
| 468 | |
| 469 var closeButton = this.dialogDom_.querySelector('#close-button'); | |
| 470 closeButton.addEventListener('click', this.onClose.bind(this)); | |
| 471 closeButton.addEventListener('mousedown', preventFocus); | |
| 472 } | |
| 473 | |
| 474 this.syncButton.checkable = true; | |
| 475 this.hostedButton.checkable = true; | |
| 476 this.detailViewButton_.checkable = true; | |
| 477 this.thumbnailViewButton_.checkable = true; | |
| 478 | |
| 479 if (util.platform.runningInBrowser()) { | |
| 480 // Suppresses the default context menu. | |
| 481 this.dialogDom_.addEventListener('contextmenu', function(e) { | |
| 482 e.preventDefault(); | |
| 483 e.stopPropagation(); | |
| 484 }); | |
| 485 } | |
| 486 }; | |
| 487 | |
| 488 FileManager.prototype.onMinimize = function() { | |
| 489 chrome.app.window.current().minimize(); | |
| 490 }; | |
| 491 | |
| 492 FileManager.prototype.onMaximize = function() { | |
| 493 var appWindow = chrome.app.window.current(); | |
| 494 if (appWindow.isMaximized()) | |
| 495 appWindow.restore(); | |
| 496 else | |
| 497 appWindow.maximize(); | |
| 498 }; | |
| 499 | |
| 500 FileManager.prototype.onClose = function() { | |
| 501 window.close(); | |
| 502 }; | |
| 503 | |
| 504 /** | |
| 505 * One-time initialization of commands. | |
| 506 * @private | |
| 507 */ | |
| 508 FileManager.prototype.initCommands_ = function() { | |
| 509 this.commandHandler = new CommandHandler(this); | |
| 510 | |
| 511 // TODO(hirono): Move the following block to the UI part. | |
| 512 var commandButtons = this.dialogDom_.querySelectorAll('button[command]'); | |
| 513 for (var j = 0; j < commandButtons.length; j++) | |
| 514 CommandButton.decorate(commandButtons[j]); | |
| 515 | |
| 516 var inputs = this.dialogDom_.querySelectorAll( | |
| 517 'input[type=text], input[type=search], textarea'); | |
| 518 for (var i = 0; i < inputs.length; i++) { | |
| 519 cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_); | |
| 520 this.registerInputCommands_(inputs[i]); | |
| 521 } | |
| 522 | |
| 523 cr.ui.contextMenuHandler.setContextMenu(this.renameInput_, | |
| 524 this.textContextMenu_); | |
| 525 this.registerInputCommands_(this.renameInput_); | |
| 526 this.document_.addEventListener('command', | |
| 527 this.setNoHover_.bind(this, true)); | |
| 528 }; | |
| 529 | |
| 530 /** | |
| 531 * Registers cut, copy, paste and delete commands on input element. | |
| 532 * | |
| 533 * @param {Node} node Text input element to register on. | |
| 534 * @private | |
| 535 */ | |
| 536 FileManager.prototype.registerInputCommands_ = function(node) { | |
| 537 CommandUtil.forceDefaultHandler(node, 'cut'); | |
| 538 CommandUtil.forceDefaultHandler(node, 'copy'); | |
| 539 CommandUtil.forceDefaultHandler(node, 'paste'); | |
| 540 CommandUtil.forceDefaultHandler(node, 'delete'); | |
| 541 node.addEventListener('keydown', function(e) { | |
| 542 var key = util.getKeyModifiers(e) + e.keyCode; | |
| 543 if (key === '190' /* '/' */ || key === '191' /* '.' */) { | |
| 544 // If this key event is propagated, this is handled search command, | |
| 545 // which calls 'preventDefault' method. | |
| 546 e.stopPropagation(); | |
| 547 } | |
| 548 }); | |
| 549 }; | |
| 550 | |
| 551 /** | |
| 552 * Entry point of the initialization. | |
| 553 * This method is called from main.js. | |
| 554 */ | |
| 555 FileManager.prototype.initializeCore = function() { | |
| 556 this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral'); | |
| 557 this.initializeQueue_.add(this.initBackgroundPage_.bind(this), | |
| 558 [], 'initBackgroundPage'); | |
| 559 this.initializeQueue_.add(this.initPreferences_.bind(this), | |
| 560 ['initGeneral'], 'initPreferences'); | |
| 561 this.initializeQueue_.add(this.initVolumeManager_.bind(this), | |
| 562 ['initGeneral', 'initBackgroundPage'], | |
| 563 'initVolumeManager'); | |
| 564 | |
| 565 this.initializeQueue_.run(); | |
| 566 window.addEventListener('pagehide', this.onUnload_.bind(this)); | |
| 567 }; | |
| 568 | |
| 569 FileManager.prototype.initializeUI = function(dialogDom, callback) { | |
| 570 this.dialogDom_ = dialogDom; | |
| 571 this.document_ = this.dialogDom_.ownerDocument; | |
| 572 | |
| 573 this.initializeQueue_.add( | |
| 574 this.initEssentialUI_.bind(this), | |
| 575 ['initGeneral', 'initBackgroundPage'], | |
| 576 'initEssentialUI'); | |
| 577 this.initializeQueue_.add(this.initAdditionalUI_.bind(this), | |
| 578 ['initEssentialUI'], 'initAdditionalUI'); | |
| 579 this.initializeQueue_.add( | |
| 580 this.initFileSystemUI_.bind(this), | |
| 581 ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI'); | |
| 582 | |
| 583 // Run again just in case if all pending closures have completed and the | |
| 584 // queue has stopped and monitor the completion. | |
| 585 this.initializeQueue_.run(callback); | |
| 586 }; | |
| 587 | |
| 588 /** | |
| 589 * Initializes general purpose basic things, which are used by other | |
| 590 * initializing methods. | |
| 591 * | |
| 592 * @param {function()} callback Completion callback. | |
| 593 * @private | |
| 594 */ | |
| 595 FileManager.prototype.initGeneral_ = function(callback) { | |
| 596 // Initialize the application state. | |
| 597 // TODO(mtomasz): Unify window.appState with location.search format. | |
| 598 if (window.appState) { | |
| 599 this.params_ = window.appState.params || {}; | |
| 600 this.initCurrentDirectoryURL_ = window.appState.currentDirectoryURL; | |
| 601 this.initSelectionURL_ = window.appState.selectionURL; | |
| 602 this.initTargetName_ = window.appState.targetName; | |
| 603 } else { | |
| 604 // Used by the select dialog only. | |
| 605 this.params_ = location.search ? | |
| 606 JSON.parse(decodeURIComponent(location.search.substr(1))) : | |
| 607 {}; | |
| 608 this.initCurrentDirectoryURL_ = this.params_.currentDirectoryURL; | |
| 609 this.initSelectionURL_ = this.params_.selectionURL; | |
| 610 this.initTargetName_ = this.params_.targetName; | |
| 611 } | |
| 612 | |
| 613 // Initialize the member variables that depend this.params_. | |
| 614 this.dialogType = this.params_.type || DialogType.FULL_PAGE; | |
| 615 this.startupPrefName_ = 'file-manager-' + this.dialogType; | |
| 616 this.fileTypes_ = this.params_.typeList || []; | |
| 617 | |
| 618 callback(); | |
| 619 }; | |
| 620 | |
| 621 /** | |
| 622 * Initialize the background page. | |
| 623 * @param {function()} callback Completion callback. | |
| 624 * @private | |
| 625 */ | |
| 626 FileManager.prototype.initBackgroundPage_ = function(callback) { | |
| 627 chrome.runtime.getBackgroundPage(function(backgroundPage) { | |
| 628 this.backgroundPage_ = backgroundPage; | |
| 629 this.backgroundPage_.background.ready(function() { | |
| 630 loadTimeData.data = this.backgroundPage_.background.stringData; | |
| 631 callback(); | |
| 632 }.bind(this)); | |
| 633 }.bind(this)); | |
| 634 }; | |
| 635 | |
| 636 /** | |
| 637 * Initializes the VolumeManager instance. | |
| 638 * @param {function()} callback Completion callback. | |
| 639 * @private | |
| 640 */ | |
| 641 FileManager.prototype.initVolumeManager_ = function(callback) { | |
| 642 // Auto resolving to local path does not work for folders (e.g., dialog for | |
| 643 // loading unpacked extensions). | |
| 644 var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type); | |
| 645 | |
| 646 // If this condition is false, VolumeManagerWrapper hides all drive | |
| 647 // related event and data, even if Drive is enabled on preference. | |
| 648 // In other words, even if Drive is disabled on preference but Files.app | |
| 649 // should show Drive when it is re-enabled, then the value should be set to | |
| 650 // true. | |
| 651 // Note that the Drive enabling preference change is listened by | |
| 652 // DriveIntegrationService, so here we don't need to take care about it. | |
| 653 var driveEnabled = | |
| 654 !noLocalPathResolution || !this.params_.shouldReturnLocalPath; | |
| 655 this.volumeManager_ = new VolumeManagerWrapper( | |
| 656 driveEnabled, this.backgroundPage_); | |
| 657 callback(); | |
| 658 }; | |
| 659 | |
| 660 /** | |
| 661 * One time initialization of the Files.app's essential UI elements. These | |
| 662 * elements will be shown to the user. Only visible elements should be | |
| 663 * initialized here. Any heavy operation should be avoided. Files.app's | |
| 664 * window is shown at the end of this routine. | |
| 665 * | |
| 666 * @param {function()} callback Completion callback. | |
| 667 * @private | |
| 668 */ | |
| 669 FileManager.prototype.initEssentialUI_ = function(callback) { | |
| 670 // Record stats of dialog types. New values must NOT be inserted into the | |
| 671 // array enumerating the types. It must be in sync with | |
| 672 // FileDialogType enum in tools/metrics/histograms/histogram.xml. | |
| 673 metrics.recordEnum('Create', this.dialogType, | |
| 674 [DialogType.SELECT_FOLDER, | |
| 675 DialogType.SELECT_UPLOAD_FOLDER, | |
| 676 DialogType.SELECT_SAVEAS_FILE, | |
| 677 DialogType.SELECT_OPEN_FILE, | |
| 678 DialogType.SELECT_OPEN_MULTI_FILE, | |
| 679 DialogType.FULL_PAGE]); | |
| 680 | |
| 681 // Create the metadata cache. | |
| 682 this.metadataCache_ = MetadataCache.createFull(this.volumeManager_); | |
| 683 | |
| 684 // Create the root view of FileManager. | |
| 685 this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType); | |
| 686 this.fileTypeSelector_ = this.ui_.fileTypeSelector; | |
| 687 this.okButton_ = this.ui_.okButton; | |
| 688 this.cancelButton_ = this.ui_.cancelButton; | |
| 689 | |
| 690 // Show the window as soon as the UI pre-initialization is done. | |
| 691 if (this.dialogType == DialogType.FULL_PAGE && | |
| 692 !util.platform.runningInBrowser()) { | |
| 693 chrome.app.window.current().show(); | |
| 694 setTimeout(callback, 100); // Wait until the animation is finished. | |
| 695 } else { | |
| 696 callback(); | |
| 697 } | |
| 698 }; | |
| 699 | |
| 700 /** | |
| 701 * One-time initialization of dialogs. | |
| 702 * @private | |
| 703 */ | |
| 704 FileManager.prototype.initDialogs_ = function() { | |
| 705 // Initialize the dialog. | |
| 706 this.ui_.initDialogs(); | |
| 707 FileManagerDialogBase.setFileManager(this); | |
| 708 | |
| 709 // Obtains the dialog instances from FileManagerUI. | |
| 710 // TODO(hirono): Remove the properties from the FileManager class. | |
| 711 this.error = this.ui_.errorDialog; | |
| 712 this.alert = this.ui_.alertDialog; | |
| 713 this.confirm = this.ui_.confirmDialog; | |
| 714 this.prompt = this.ui_.promptDialog; | |
| 715 this.shareDialog_ = this.ui_.shareDialog; | |
| 716 this.defaultTaskPicker = this.ui_.defaultTaskPicker; | |
| 717 this.suggestAppsDialog = this.ui_.suggestAppsDialog; | |
| 718 }; | |
| 719 | |
| 720 /** | |
| 721 * One-time initialization of various DOM nodes. Loads the additional DOM | |
| 722 * elements visible to the user. Initialize here elements, which are expensive | |
| 723 * or hidden in the beginning. | |
| 724 * | |
| 725 * @param {function()} callback Completion callback. | |
| 726 * @private | |
| 727 */ | |
| 728 FileManager.prototype.initAdditionalUI_ = function(callback) { | |
| 729 this.initDialogs_(); | |
| 730 this.ui_.initAdditionalUI(); | |
| 731 | |
| 732 this.dialogDom_.addEventListener('drop', function(e) { | |
| 733 // Prevent opening an URL by dropping it onto the page. | |
| 734 e.preventDefault(); | |
| 735 }); | |
| 736 | |
| 737 this.dialogDom_.addEventListener('click', | |
| 738 this.onExternalLinkClick_.bind(this)); | |
| 739 // Cache nodes we'll be manipulating. | |
| 740 var dom = this.dialogDom_; | |
| 741 | |
| 742 this.filenameInput_ = dom.querySelector('#filename-input-box input'); | |
| 743 this.taskItems_ = dom.querySelector('#tasks'); | |
| 744 | |
| 745 this.table_ = dom.querySelector('.detail-table'); | |
| 746 this.grid_ = dom.querySelector('.thumbnail-grid'); | |
| 747 this.spinner_ = dom.querySelector('#list-container > .spinner-layer'); | |
| 748 this.showSpinner_(true); | |
| 749 | |
| 750 var fullPage = this.dialogType == DialogType.FULL_PAGE; | |
| 751 FileTable.decorate(this.table_, this.metadataCache_, fullPage); | |
| 752 FileGrid.decorate(this.grid_, this.metadataCache_, this.volumeManager_); | |
| 753 | |
| 754 this.previewPanel_ = new PreviewPanel( | |
| 755 dom.querySelector('.preview-panel'), | |
| 756 DialogType.isOpenDialog(this.dialogType) ? | |
| 757 PreviewPanel.VisibilityType.ALWAYS_VISIBLE : | |
| 758 PreviewPanel.VisibilityType.AUTO, | |
| 759 this.metadataCache_, | |
| 760 this.volumeManager_); | |
| 761 this.previewPanel_.addEventListener( | |
| 762 PreviewPanel.Event.VISIBILITY_CHANGE, | |
| 763 this.onPreviewPanelVisibilityChange_.bind(this)); | |
| 764 this.previewPanel_.initialize(); | |
| 765 | |
| 766 this.previewPanel_.breadcrumbs.addEventListener( | |
| 767 'pathclick', this.onBreadcrumbClick_.bind(this)); | |
| 768 | |
| 769 // Initialize progress center panel. | |
| 770 this.progressCenterPanel_ = new ProgressCenterPanel( | |
| 771 dom.querySelector('#progress-center')); | |
| 772 this.backgroundPage_.background.progressCenter.addPanel( | |
| 773 this.progressCenterPanel_); | |
| 774 | |
| 775 this.document_.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
| 776 this.document_.addEventListener('keyup', this.onKeyUp_.bind(this)); | |
| 777 | |
| 778 this.renameInput_ = this.document_.createElement('input'); | |
| 779 this.renameInput_.className = 'rename'; | |
| 780 | |
| 781 this.renameInput_.addEventListener( | |
| 782 'keydown', this.onRenameInputKeyDown_.bind(this)); | |
| 783 this.renameInput_.addEventListener( | |
| 784 'blur', this.onRenameInputBlur_.bind(this)); | |
| 785 | |
| 786 // TODO(hirono): Rename the handler after creating the DialogFooter class. | |
| 787 this.filenameInput_.addEventListener( | |
| 788 'input', this.onFilenameInputInput_.bind(this)); | |
| 789 this.filenameInput_.addEventListener( | |
| 790 'keydown', this.onFilenameInputKeyDown_.bind(this)); | |
| 791 this.filenameInput_.addEventListener( | |
| 792 'focus', this.onFilenameInputFocus_.bind(this)); | |
| 793 | |
| 794 this.listContainer_ = this.dialogDom_.querySelector('#list-container'); | |
| 795 this.listContainer_.addEventListener( | |
| 796 'keydown', this.onListKeyDown_.bind(this)); | |
| 797 this.listContainer_.addEventListener( | |
| 798 'keypress', this.onListKeyPress_.bind(this)); | |
| 799 this.listContainer_.addEventListener( | |
| 800 'mousemove', this.onListMouseMove_.bind(this)); | |
| 801 | |
| 802 this.okButton_.addEventListener('click', this.onOk_.bind(this)); | |
| 803 this.onCancelBound_ = this.onCancel_.bind(this); | |
| 804 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
| 805 | |
| 806 this.decorateSplitter( | |
| 807 this.dialogDom_.querySelector('#navigation-list-splitter')); | |
| 808 this.decorateSplitter( | |
| 809 this.dialogDom_.querySelector('#middlebar-splitter')); | |
| 810 | |
| 811 this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container'); | |
| 812 | |
| 813 this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings'); | |
| 814 this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
| 815 this, 'cellularDisabled', false /* not inverted */)); | |
| 816 | |
| 817 this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings'); | |
| 818 this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind( | |
| 819 this, 'hostedFilesDisabled', true /* inverted */)); | |
| 820 | |
| 821 this.detailViewButton_ = | |
| 822 this.dialogDom_.querySelector('#detail-view'); | |
| 823 this.detailViewButton_.addEventListener('activate', | |
| 824 this.onDetailViewButtonClick_.bind(this)); | |
| 825 | |
| 826 this.thumbnailViewButton_ = | |
| 827 this.dialogDom_.querySelector('#thumbnail-view'); | |
| 828 this.thumbnailViewButton_.addEventListener('activate', | |
| 829 this.onThumbnailViewButtonClick_.bind(this)); | |
| 830 | |
| 831 cr.ui.ComboButton.decorate(this.taskItems_); | |
| 832 this.taskItems_.showMenu = function(shouldSetFocus) { | |
| 833 // Prevent the empty menu from opening. | |
| 834 if (!this.menu.length) | |
| 835 return; | |
| 836 cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus); | |
| 837 }; | |
| 838 this.taskItems_.addEventListener('select', | |
| 839 this.onTaskItemClicked_.bind(this)); | |
| 840 | |
| 841 this.dialogDom_.ownerDocument.defaultView.addEventListener( | |
| 842 'resize', this.onResize_.bind(this)); | |
| 843 | |
| 844 this.filePopup_ = null; | |
| 845 | |
| 846 this.searchBoxWrapper_ = this.ui_.searchBox.element; | |
| 847 this.searchBox_ = this.ui_.searchBox.inputElement; | |
| 848 this.searchBox_.addEventListener( | |
| 849 'input', this.onSearchBoxUpdate_.bind(this)); | |
| 850 this.ui_.searchBox.clearButton.addEventListener( | |
| 851 'click', this.onSearchClearButtonClick_.bind(this)); | |
| 852 | |
| 853 this.lastSearchQuery_ = ''; | |
| 854 | |
| 855 this.autocompleteList_ = this.ui_.searchBox.autocompleteList; | |
| 856 this.autocompleteList_.requestSuggestions = | |
| 857 this.requestAutocompleteSuggestions_.bind(this); | |
| 858 | |
| 859 // Instead, open the suggested item when Enter key is pressed or | |
| 860 // mouse-clicked. | |
| 861 this.autocompleteList_.handleEnterKeydown = function(event) { | |
| 862 this.openAutocompleteSuggestion_(); | |
| 863 this.lastAutocompleteQuery_ = ''; | |
| 864 this.autocompleteList_.suggestions = []; | |
| 865 }.bind(this); | |
| 866 this.autocompleteList_.addEventListener('mousedown', function(event) { | |
| 867 this.openAutocompleteSuggestion_(); | |
| 868 this.lastAutocompleteQuery_ = ''; | |
| 869 this.autocompleteList_.suggestions = []; | |
| 870 }.bind(this)); | |
| 871 | |
| 872 this.defaultActionMenuItem_ = | |
| 873 this.dialogDom_.querySelector('#default-action'); | |
| 874 | |
| 875 this.openWithCommand_ = | |
| 876 this.dialogDom_.querySelector('#open-with'); | |
| 877 | |
| 878 this.driveBuyMoreStorageCommand_ = | |
| 879 this.dialogDom_.querySelector('#drive-buy-more-space'); | |
| 880 | |
| 881 this.defaultActionMenuItem_.addEventListener('activate', | |
| 882 this.dispatchSelectionAction_.bind(this)); | |
| 883 | |
| 884 this.initFileTypeFilter_(); | |
| 885 | |
| 886 util.addIsFocusedMethod(); | |
| 887 | |
| 888 // Populate the static localized strings. | |
| 889 i18nTemplate.process(this.document_, loadTimeData); | |
| 890 | |
| 891 // Arrange the file list. | |
| 892 this.table_.normalizeColumns(); | |
| 893 this.table_.redraw(); | |
| 894 | |
| 895 callback(); | |
| 896 }; | |
| 897 | |
| 898 /** | |
| 899 * @param {Event} event Click event. | |
| 900 * @private | |
| 901 */ | |
| 902 FileManager.prototype.onBreadcrumbClick_ = function(event) { | |
| 903 this.directoryModel_.changeDirectoryEntry(event.entry); | |
| 904 }; | |
| 905 | |
| 906 /** | |
| 907 * Constructs table and grid (heavy operation). | |
| 908 * @private | |
| 909 **/ | |
| 910 FileManager.prototype.initFileList_ = function() { | |
| 911 // Always sharing the data model between the detail/thumb views confuses | |
| 912 // them. Instead we maintain this bogus data model, and hook it up to the | |
| 913 // view that is not in use. | |
| 914 this.emptyDataModel_ = new cr.ui.ArrayDataModel([]); | |
| 915 this.emptySelectionModel_ = new cr.ui.ListSelectionModel(); | |
| 916 | |
| 917 var singleSelection = | |
| 918 this.dialogType == DialogType.SELECT_OPEN_FILE || | |
| 919 this.dialogType == DialogType.SELECT_FOLDER || | |
| 920 this.dialogType == DialogType.SELECT_UPLOAD_FOLDER || | |
| 921 this.dialogType == DialogType.SELECT_SAVEAS_FILE; | |
| 922 | |
| 923 this.fileFilter_ = new FileFilter( | |
| 924 this.metadataCache_, | |
| 925 false /* Don't show dot files by default. */); | |
| 926 | |
| 927 this.fileWatcher_ = new FileWatcher(this.metadataCache_); | |
| 928 this.fileWatcher_.addEventListener( | |
| 929 'watcher-metadata-changed', | |
| 930 this.onWatcherMetadataChanged_.bind(this)); | |
| 931 | |
| 932 this.directoryModel_ = new DirectoryModel( | |
| 933 singleSelection, | |
| 934 this.fileFilter_, | |
| 935 this.fileWatcher_, | |
| 936 this.metadataCache_, | |
| 937 this.volumeManager_); | |
| 938 | |
| 939 this.folderShortcutsModel_ = new FolderShortcutsDataModel( | |
| 940 this.volumeManager_); | |
| 941 | |
| 942 this.selectionHandler_ = new FileSelectionHandler(this); | |
| 943 | |
| 944 var dataModel = this.directoryModel_.getFileList(); | |
| 945 | |
| 946 this.table_.setupCompareFunctions(dataModel); | |
| 947 | |
| 948 dataModel.addEventListener('permuted', | |
| 949 this.updateStartupPrefs_.bind(this)); | |
| 950 | |
| 951 this.directoryModel_.getFileListSelection().addEventListener('change', | |
| 952 this.selectionHandler_.onFileSelectionChanged.bind( | |
| 953 this.selectionHandler_)); | |
| 954 | |
| 955 this.initList_(this.grid_); | |
| 956 this.initList_(this.table_.list); | |
| 957 | |
| 958 var fileListFocusBound = this.onFileListFocus_.bind(this); | |
| 959 this.table_.list.addEventListener('focus', fileListFocusBound); | |
| 960 this.grid_.addEventListener('focus', fileListFocusBound); | |
| 961 | |
| 962 var dragStartBound = this.onDragStart_.bind(this); | |
| 963 this.table_.list.addEventListener('dragstart', dragStartBound); | |
| 964 this.grid_.addEventListener('dragstart', dragStartBound); | |
| 965 | |
| 966 var dragEndBound = this.onDragEnd_.bind(this); | |
| 967 this.table_.list.addEventListener('dragend', dragEndBound); | |
| 968 this.grid_.addEventListener('dragend', dragEndBound); | |
| 969 // This event is published by DragSelector because drag end event is not | |
| 970 // published at the end of drag selection. | |
| 971 this.table_.list.addEventListener('dragselectionend', dragEndBound); | |
| 972 this.grid_.addEventListener('dragselectionend', dragEndBound); | |
| 973 | |
| 974 // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just | |
| 975 // attach the directory model. | |
| 976 this.initNavigationList_(); | |
| 977 | |
| 978 this.table_.addEventListener('column-resize-end', | |
| 979 this.updateStartupPrefs_.bind(this)); | |
| 980 | |
| 981 // Restore preferences. | |
| 982 this.directoryModel_.getFileList().sort( | |
| 983 this.viewOptions_.sortField || 'modificationTime', | |
| 984 this.viewOptions_.sortDirection || 'desc'); | |
| 985 if (this.viewOptions_.columns) { | |
| 986 var cm = this.table_.columnModel; | |
| 987 for (var i = 0; i < cm.totalSize; i++) { | |
| 988 if (this.viewOptions_.columns[i] > 0) | |
| 989 cm.setWidth(i, this.viewOptions_.columns[i]); | |
| 990 } | |
| 991 } | |
| 992 this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL); | |
| 993 | |
| 994 this.textSearchState_ = {text: '', date: new Date()}; | |
| 995 this.closeOnUnmount_ = (this.params_.action == 'auto-open'); | |
| 996 | |
| 997 if (this.closeOnUnmount_) { | |
| 998 this.volumeManager_.addEventListener('externally-unmounted', | |
| 999 this.onExternallyUnmounted_.bind(this)); | |
| 1000 } | |
| 1001 | |
| 1002 // Update metadata to change 'Today' and 'Yesterday' dates. | |
| 1003 var today = new Date(); | |
| 1004 today.setHours(0); | |
| 1005 today.setMinutes(0); | |
| 1006 today.setSeconds(0); | |
| 1007 today.setMilliseconds(0); | |
| 1008 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
| 1009 today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000); | |
| 1010 }; | |
| 1011 | |
| 1012 /** | |
| 1013 * @private | |
| 1014 */ | |
| 1015 FileManager.prototype.initNavigationList_ = function() { | |
| 1016 this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree'); | |
| 1017 DirectoryTree.decorate(this.directoryTree_, | |
| 1018 this.directoryModel_, | |
| 1019 this.volumeManager_); | |
| 1020 | |
| 1021 this.navigationList_ = this.dialogDom_.querySelector('#navigation-list'); | |
| 1022 NavigationList.decorate(this.navigationList_, | |
| 1023 this.volumeManager_, | |
| 1024 this.directoryModel_); | |
| 1025 this.navigationList_.fileManager = this; | |
| 1026 this.navigationList_.dataModel = new NavigationListModel( | |
| 1027 this.volumeManager_, this.folderShortcutsModel_); | |
| 1028 }; | |
| 1029 | |
| 1030 /** | |
| 1031 * @private | |
| 1032 */ | |
| 1033 FileManager.prototype.updateMiddleBarVisibility_ = function() { | |
| 1034 var entry = this.directoryModel_.getCurrentDirEntry(); | |
| 1035 if (!entry) | |
| 1036 return; | |
| 1037 | |
| 1038 var driveVolume = this.volumeManager_.getVolumeInfo(entry); | |
| 1039 var visible = driveVolume && !driveVolume.error && | |
| 1040 driveVolume.volumeType === util.VolumeType.DRIVE; | |
| 1041 this.dialogDom_. | |
| 1042 querySelector('.dialog-middlebar-contents').hidden = !visible; | |
| 1043 this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible; | |
| 1044 this.onResize_(); | |
| 1045 }; | |
| 1046 | |
| 1047 /** | |
| 1048 * @private | |
| 1049 */ | |
| 1050 FileManager.prototype.updateStartupPrefs_ = function() { | |
| 1051 var sortStatus = this.directoryModel_.getFileList().sortStatus; | |
| 1052 var prefs = { | |
| 1053 sortField: sortStatus.field, | |
| 1054 sortDirection: sortStatus.direction, | |
| 1055 columns: [], | |
| 1056 listType: this.listType_ | |
| 1057 }; | |
| 1058 var cm = this.table_.columnModel; | |
| 1059 for (var i = 0; i < cm.totalSize; i++) { | |
| 1060 prefs.columns.push(cm.getWidth(i)); | |
| 1061 } | |
| 1062 // Save the global default. | |
| 1063 util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs)); | |
| 1064 | |
| 1065 // Save the window-specific preference. | |
| 1066 if (window.appState) { | |
| 1067 window.appState.viewOptions = prefs; | |
| 1068 util.saveAppState(); | |
| 1069 } | |
| 1070 }; | |
| 1071 | |
| 1072 FileManager.prototype.refocus = function() { | |
| 1073 var targetElement; | |
| 1074 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) | |
| 1075 targetElement = this.filenameInput_; | |
| 1076 else | |
| 1077 targetElement = this.currentList_; | |
| 1078 | |
| 1079 // Hack: if the tabIndex is disabled, we can assume a modal dialog is | |
| 1080 // shown. Focus to a button on the dialog instead. | |
| 1081 if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1) | |
| 1082 targetElement = document.querySelector('button:not([tabIndex="-1"])'); | |
| 1083 | |
| 1084 if (targetElement) | |
| 1085 targetElement.focus(); | |
| 1086 }; | |
| 1087 | |
| 1088 /** | |
| 1089 * File list focus handler. Used to select the top most element on the list | |
| 1090 * if nothing was selected. | |
| 1091 * | |
| 1092 * @private | |
| 1093 */ | |
| 1094 FileManager.prototype.onFileListFocus_ = function() { | |
| 1095 // If the file list is focused by <Tab>, select the first item if no item | |
| 1096 // is selected. | |
| 1097 if (this.pressingTab_) { | |
| 1098 if (this.getSelection() && this.getSelection().totalCount == 0) | |
| 1099 this.directoryModel_.selectIndex(0); | |
| 1100 } | |
| 1101 }; | |
| 1102 | |
| 1103 /** | |
| 1104 * Index of selected item in the typeList of the dialog params. | |
| 1105 * | |
| 1106 * @return {number} 1-based index of selected type or 0 if no type selected. | |
| 1107 * @private | |
| 1108 */ | |
| 1109 FileManager.prototype.getSelectedFilterIndex_ = function() { | |
| 1110 var index = Number(this.fileTypeSelector_.selectedIndex); | |
| 1111 if (index < 0) // Nothing selected. | |
| 1112 return 0; | |
| 1113 if (this.params_.includeAllFiles) // Already 1-based. | |
| 1114 return index; | |
| 1115 return index + 1; // Convert to 1-based; | |
| 1116 }; | |
| 1117 | |
| 1118 FileManager.prototype.setListType = function(type) { | |
| 1119 if (type && type == this.listType_) | |
| 1120 return; | |
| 1121 | |
| 1122 this.table_.list.startBatchUpdates(); | |
| 1123 this.grid_.startBatchUpdates(); | |
| 1124 | |
| 1125 // TODO(dzvorygin): style.display and dataModel setting order shouldn't | |
| 1126 // cause any UI bugs. Currently, the only right way is first to set display | |
| 1127 // style and only then set dataModel. | |
| 1128 | |
| 1129 if (type == FileManager.ListType.DETAIL) { | |
| 1130 this.table_.dataModel = this.directoryModel_.getFileList(); | |
| 1131 this.table_.selectionModel = this.directoryModel_.getFileListSelection(); | |
| 1132 this.table_.hidden = false; | |
| 1133 this.grid_.hidden = true; | |
| 1134 this.grid_.selectionModel = this.emptySelectionModel_; | |
| 1135 this.grid_.dataModel = this.emptyDataModel_; | |
| 1136 this.table_.hidden = false; | |
| 1137 /** @type {cr.ui.List} */ | |
| 1138 this.currentList_ = this.table_.list; | |
| 1139 this.detailViewButton_.setAttribute('checked', ''); | |
| 1140 this.thumbnailViewButton_.removeAttribute('checked'); | |
| 1141 this.detailViewButton_.setAttribute('disabled', ''); | |
| 1142 this.thumbnailViewButton_.removeAttribute('disabled'); | |
| 1143 } else if (type == FileManager.ListType.THUMBNAIL) { | |
| 1144 this.grid_.dataModel = this.directoryModel_.getFileList(); | |
| 1145 this.grid_.selectionModel = this.directoryModel_.getFileListSelection(); | |
| 1146 this.grid_.hidden = false; | |
| 1147 this.table_.hidden = true; | |
| 1148 this.table_.selectionModel = this.emptySelectionModel_; | |
| 1149 this.table_.dataModel = this.emptyDataModel_; | |
| 1150 this.grid_.hidden = false; | |
| 1151 /** @type {cr.ui.List} */ | |
| 1152 this.currentList_ = this.grid_; | |
| 1153 this.thumbnailViewButton_.setAttribute('checked', ''); | |
| 1154 this.detailViewButton_.removeAttribute('checked'); | |
| 1155 this.thumbnailViewButton_.setAttribute('disabled', ''); | |
| 1156 this.detailViewButton_.removeAttribute('disabled'); | |
| 1157 } else { | |
| 1158 throw new Error('Unknown list type: ' + type); | |
| 1159 } | |
| 1160 | |
| 1161 this.listType_ = type; | |
| 1162 this.updateStartupPrefs_(); | |
| 1163 this.onResize_(); | |
| 1164 | |
| 1165 this.table_.list.endBatchUpdates(); | |
| 1166 this.grid_.endBatchUpdates(); | |
| 1167 }; | |
| 1168 | |
| 1169 /** | |
| 1170 * Initialize the file list table or grid. | |
| 1171 * | |
| 1172 * @param {cr.ui.List} list The list. | |
| 1173 * @private | |
| 1174 */ | |
| 1175 FileManager.prototype.initList_ = function(list) { | |
| 1176 // Overriding the default role 'list' to 'listbox' for better accessibility | |
| 1177 // on ChromeOS. | |
| 1178 list.setAttribute('role', 'listbox'); | |
| 1179 list.addEventListener('click', this.onDetailClick_.bind(this)); | |
| 1180 list.id = 'file-list'; | |
| 1181 }; | |
| 1182 | |
| 1183 /** | |
| 1184 * @private | |
| 1185 */ | |
| 1186 FileManager.prototype.onCopyProgress_ = function(event) { | |
| 1187 if (event.reason == 'ERROR' && | |
| 1188 event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR && | |
| 1189 event.error.data.toDrive && | |
| 1190 event.error.data.name == util.FileError.QUOTA_EXCEEDED_ERR) { | |
| 1191 this.alert.showHtml( | |
| 1192 strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'), | |
| 1193 strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE', | |
| 1194 decodeURIComponent( | |
| 1195 event.error.data.sourceFileUrl.split('/').pop()), | |
| 1196 str('GOOGLE_DRIVE_BUY_STORAGE_URL'))); | |
| 1197 } | |
| 1198 }; | |
| 1199 | |
| 1200 /** | |
| 1201 * Handler of file manager operations. Called when an entry has been | |
| 1202 * changed. | |
| 1203 * This updates directory model to reflect operation result immediately (not | |
| 1204 * waiting for directory update event). Also, preloads thumbnails for the | |
| 1205 * images of new entries. | |
| 1206 * See also FileOperationManager.EventRouter. | |
| 1207 * | |
| 1208 * @param {Event} event An event for the entry change. | |
| 1209 * @private | |
| 1210 */ | |
| 1211 FileManager.prototype.onEntryChanged_ = function(event) { | |
| 1212 var kind = event.kind; | |
| 1213 var entry = event.entry; | |
| 1214 this.directoryModel_.onEntryChanged(kind, entry); | |
| 1215 this.selectionHandler_.onFileSelectionChanged(); | |
| 1216 | |
| 1217 if (kind === util.EntryChangedKind.CREATED && FileType.isImage(entry)) { | |
| 1218 // Preload a thumbnail if the new copied entry an image. | |
| 1219 var locationInfo = this.volumeManager_.getLocationInfo(entry); | |
| 1220 if (!locationInfo) | |
| 1221 return; | |
| 1222 this.metadataCache_.get(entry, 'thumbnail|drive', function(metadata) { | |
| 1223 var thumbnailLoader_ = new ThumbnailLoader( | |
| 1224 entry, | |
| 1225 ThumbnailLoader.LoaderType.CANVAS, | |
| 1226 metadata, | |
| 1227 undefined, // Media type. | |
| 1228 // TODO(mtomasz): Use Entry instead of paths. | |
| 1229 locationInfo.isDriveBased ? | |
| 1230 ThumbnailLoader.UseEmbedded.USE_EMBEDDED : | |
| 1231 ThumbnailLoader.UseEmbedded.NO_EMBEDDED, | |
| 1232 10); // Very low priority. | |
| 1233 thumbnailLoader_.loadDetachedImage(function(success) {}); | |
| 1234 }); | |
| 1235 } | |
| 1236 }; | |
| 1237 | |
| 1238 /** | |
| 1239 * Fills the file type list or hides it. | |
| 1240 * @private | |
| 1241 */ | |
| 1242 FileManager.prototype.initFileTypeFilter_ = function() { | |
| 1243 if (this.params_.includeAllFiles) { | |
| 1244 var option = this.document_.createElement('option'); | |
| 1245 option.innerText = str('ALL_FILES_FILTER'); | |
| 1246 this.fileTypeSelector_.appendChild(option); | |
| 1247 option.value = 0; | |
| 1248 } | |
| 1249 | |
| 1250 for (var i = 0; i !== this.fileTypes_.length; i++) { | |
| 1251 var fileType = this.fileTypes_[i]; | |
| 1252 var option = this.document_.createElement('option'); | |
| 1253 var description = fileType.description; | |
| 1254 if (!description) { | |
| 1255 // See if all the extensions in the group have the same description. | |
| 1256 for (var j = 0; j !== fileType.extensions.length; j++) { | |
| 1257 var currentDescription = FileType.typeToString( | |
| 1258 FileType.getTypeForName('.' + fileType.extensions[j])); | |
| 1259 if (!description) // Set the first time. | |
| 1260 description = currentDescription; | |
| 1261 else if (description != currentDescription) { | |
| 1262 // No single description, fall through to the extension list. | |
| 1263 description = null; | |
| 1264 break; | |
| 1265 } | |
| 1266 } | |
| 1267 | |
| 1268 if (!description) | |
| 1269 // Convert ['jpg', 'png'] to '*.jpg, *.png'. | |
| 1270 description = fileType.extensions.map(function(s) { | |
| 1271 return '*.' + s; | |
| 1272 }).join(', '); | |
| 1273 } | |
| 1274 option.innerText = description; | |
| 1275 | |
| 1276 option.value = i + 1; | |
| 1277 | |
| 1278 if (fileType.selected) | |
| 1279 option.selected = true; | |
| 1280 | |
| 1281 this.fileTypeSelector_.appendChild(option); | |
| 1282 } | |
| 1283 | |
| 1284 var options = this.fileTypeSelector_.querySelectorAll('option'); | |
| 1285 if (options.length >= 2) { | |
| 1286 // There is in fact no choice, show the selector. | |
| 1287 this.fileTypeSelector_.hidden = false; | |
| 1288 | |
| 1289 this.fileTypeSelector_.addEventListener('change', | |
| 1290 this.updateFileTypeFilter_.bind(this)); | |
| 1291 } | |
| 1292 }; | |
| 1293 | |
| 1294 /** | |
| 1295 * Filters file according to the selected file type. | |
| 1296 * @private | |
| 1297 */ | |
| 1298 FileManager.prototype.updateFileTypeFilter_ = function() { | |
| 1299 this.fileFilter_.removeFilter('fileType'); | |
| 1300 var selectedIndex = this.getSelectedFilterIndex_(); | |
| 1301 if (selectedIndex > 0) { // Specific filter selected. | |
| 1302 var regexp = new RegExp('.*(' + | |
| 1303 this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i'); | |
| 1304 var filter = function(entry) { | |
| 1305 return entry.isDirectory || regexp.test(entry.name); | |
| 1306 }; | |
| 1307 this.fileFilter_.addFilter('fileType', filter); | |
| 1308 } | |
| 1309 }; | |
| 1310 | |
| 1311 /** | |
| 1312 * Resize details and thumb views to fit the new window size. | |
| 1313 * @private | |
| 1314 */ | |
| 1315 FileManager.prototype.onResize_ = function() { | |
| 1316 if (this.listType_ == FileManager.ListType.THUMBNAIL) | |
| 1317 this.grid_.relayout(); | |
| 1318 else | |
| 1319 this.table_.relayout(); | |
| 1320 | |
| 1321 // May not be available during initialization. | |
| 1322 if (this.directoryTree_) | |
| 1323 this.directoryTree_.relayout(); | |
| 1324 | |
| 1325 // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before | |
| 1326 // file system is available. | |
| 1327 if (this.navigationList_) | |
| 1328 this.navigationList_.redraw(); | |
| 1329 | |
| 1330 this.previewPanel_.breadcrumbs.truncate(); | |
| 1331 }; | |
| 1332 | |
| 1333 /** | |
| 1334 * Handles local metadata changes in the currect directory. | |
| 1335 * @param {Event} event Change event. | |
| 1336 * @private | |
| 1337 */ | |
| 1338 FileManager.prototype.onWatcherMetadataChanged_ = function(event) { | |
| 1339 this.updateMetadataInUI_( | |
| 1340 event.metadataType, event.entries, event.properties); | |
| 1341 }; | |
| 1342 | |
| 1343 /** | |
| 1344 * Resize details and thumb views to fit the new window size. | |
| 1345 * @private | |
| 1346 */ | |
| 1347 FileManager.prototype.onPreviewPanelVisibilityChange_ = function() { | |
| 1348 // This method may be called on initialization. Some object may not be | |
| 1349 // initialized. | |
| 1350 | |
| 1351 var panelHeight = this.previewPanel_.visible ? | |
| 1352 this.previewPanel_.height : 0; | |
| 1353 if (this.grid_) | |
| 1354 this.grid_.setBottomMarginForPanel(panelHeight); | |
| 1355 if (this.table_) | |
| 1356 this.table_.setBottomMarginForPanel(panelHeight); | |
| 1357 | |
| 1358 if (this.directoryTree_) { | |
| 1359 this.directoryTree_.setBottomMarginForPanel(panelHeight); | |
| 1360 this.ensureDirectoryTreeItemNotBehindPreviewPanel_(); | |
| 1361 } | |
| 1362 }; | |
| 1363 | |
| 1364 /** | |
| 1365 * Invoked when the drag is started on the list or the grid. | |
| 1366 * @private | |
| 1367 */ | |
| 1368 FileManager.prototype.onDragStart_ = function() { | |
| 1369 // On open file dialog, the preview panel is always shown. | |
| 1370 if (DialogType.isOpenDialog(this.dialogType)) | |
| 1371 return; | |
| 1372 this.previewPanel_.visibilityType = | |
| 1373 PreviewPanel.VisibilityType.ALWAYS_HIDDEN; | |
| 1374 }; | |
| 1375 | |
| 1376 /** | |
| 1377 * Invoked when the drag is ended on the list or the grid. | |
| 1378 * @private | |
| 1379 */ | |
| 1380 FileManager.prototype.onDragEnd_ = function() { | |
| 1381 // On open file dialog, the preview panel is always shown. | |
| 1382 if (DialogType.isOpenDialog(this.dialogType)) | |
| 1383 return; | |
| 1384 this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO; | |
| 1385 }; | |
| 1386 | |
| 1387 /** | |
| 1388 * Sets up the current directory during initialization. | |
| 1389 * @private | |
| 1390 */ | |
| 1391 FileManager.prototype.setupCurrentDirectory_ = function() { | |
| 1392 var tracker = this.directoryModel_.createDirectoryChangeTracker(); | |
| 1393 var queue = new AsyncUtil.Queue(); | |
| 1394 | |
| 1395 // Wait until the volume manager is initialized. | |
| 1396 queue.run(function(callback) { | |
| 1397 tracker.start(); | |
| 1398 this.volumeManager_.ensureInitialized(callback); | |
| 1399 }.bind(this)); | |
| 1400 | |
| 1401 var nextCurrentDirEntry; | |
| 1402 var selectionEntry; | |
| 1403 | |
| 1404 // Resolve the selectionURL to selectionEntry or to currentDirectoryEntry | |
| 1405 // in case of being a display root. | |
| 1406 queue.run(function(callback) { | |
| 1407 if (!this.initSelectionURL_) { | |
| 1408 callback(); | |
| 1409 return; | |
| 1410 } | |
| 1411 webkitResolveLocalFileSystemURL( | |
| 1412 this.initSelectionURL_, | |
| 1413 function(inEntry) { | |
| 1414 var locationInfo = this.volumeManager_.getLocationInfo(inEntry); | |
| 1415 // If location information is not available, then the volume is | |
| 1416 // no longer (or never) available. | |
| 1417 if (!locationInfo) { | |
| 1418 callback(); | |
| 1419 return; | |
| 1420 } | |
| 1421 // If the selection is root, then use it as a current directory | |
| 1422 // instead. This is because, selecting a root entry is done as | |
| 1423 // opening it. | |
| 1424 if (locationInfo.isRootEntry) | |
| 1425 nextCurrentDirEntry = inEntry; | |
| 1426 else | |
| 1427 selectionEntry = inEntry; | |
| 1428 callback(); | |
| 1429 }.bind(this), callback); | |
| 1430 }.bind(this)); | |
| 1431 // Resolve the currentDirectoryURL to currentDirectoryEntry (if not done | |
| 1432 // by the previous step). | |
| 1433 queue.run(function(callback) { | |
| 1434 if (nextCurrentDirEntry || !this.initCurrentDirectoryURL_) { | |
| 1435 callback(); | |
| 1436 return; | |
| 1437 } | |
| 1438 webkitResolveLocalFileSystemURL( | |
| 1439 this.initCurrentDirectoryURL_, | |
| 1440 function(inEntry) { | |
| 1441 var locationInfo = this.volumeManager_.getLocationInfo(inEntry); | |
| 1442 if (!locationInfo) { | |
| 1443 callback(); | |
| 1444 return; | |
| 1445 } | |
| 1446 nextCurrentDirEntry = inEntry; | |
| 1447 callback(); | |
| 1448 }.bind(this), callback); | |
| 1449 // TODO(mtomasz): Implement reopening on special search, when fake | |
| 1450 // entries are converted to directory providers. | |
| 1451 }.bind(this)); | |
| 1452 | |
| 1453 // If the directory to be changed to is not available, then first fallback | |
| 1454 // to the parent of the selection entry. | |
| 1455 queue.run(function(callback) { | |
| 1456 if (nextCurrentDirEntry || !selectionEntry) { | |
| 1457 callback(); | |
| 1458 return; | |
| 1459 } | |
| 1460 selectionEntry.getParent(function(inEntry) { | |
| 1461 nextCurrentDirEntry = inEntry; | |
| 1462 callback(); | |
| 1463 }.bind(this)); | |
| 1464 }.bind(this)); | |
| 1465 | |
| 1466 // If the directory to be changed to is still not resolved, then fallback | |
| 1467 // to the default display root. | |
| 1468 queue.run(function(callback) { | |
| 1469 if (nextCurrentDirEntry) { | |
| 1470 callback(); | |
| 1471 return; | |
| 1472 } | |
| 1473 this.volumeManager_.getDefaultDisplayRoot(function(displayRoot) { | |
| 1474 nextCurrentDirEntry = displayRoot; | |
| 1475 callback(); | |
| 1476 }.bind(this)); | |
| 1477 }.bind(this)); | |
| 1478 | |
| 1479 // If selection failed to be resolved (eg. didn't exist, in case of saving | |
| 1480 // a file, or in case of a fallback of the current directory, then try to | |
| 1481 // resolve again using the target name. | |
| 1482 queue.run(function(callback) { | |
| 1483 if (selectionEntry || !nextCurrentDirEntry || !this.initTargetName_) { | |
| 1484 callback(); | |
| 1485 return; | |
| 1486 } | |
| 1487 // Try to resolve as a file first. If it fails, then as a directory. | |
| 1488 nextCurrentDirEntry.getFile( | |
| 1489 this.initTargetName_, | |
| 1490 {}, | |
| 1491 function(targetEntry) { | |
| 1492 selectionEntry = targetEntry; | |
| 1493 callback(); | |
| 1494 }, function() { | |
| 1495 // Failed to resolve as a file | |
| 1496 nextCurrentDirEntry.getDirectory( | |
| 1497 this.initTargetName_, | |
| 1498 {}, | |
| 1499 function(targetEntry) { | |
| 1500 selectionEntry = targetEntry; | |
| 1501 callback(); | |
| 1502 }, function() { | |
| 1503 // Failed to resolve as either file or directory. | |
| 1504 callback(); | |
| 1505 }); | |
| 1506 }.bind(this)); | |
| 1507 }.bind(this)); | |
| 1508 | |
| 1509 | |
| 1510 // Finalize. | |
| 1511 queue.run(function(callback) { | |
| 1512 // Check directory change. | |
| 1513 tracker.stop(); | |
| 1514 if (tracker.hasChanged) { | |
| 1515 callback(); | |
| 1516 return; | |
| 1517 } | |
| 1518 // Finish setup current directory. | |
| 1519 this.finishSetupCurrentDirectory_( | |
| 1520 nextCurrentDirEntry, | |
| 1521 selectionEntry, | |
| 1522 this.initTargetName_); | |
| 1523 callback(); | |
| 1524 }.bind(this)); | |
| 1525 }; | |
| 1526 | |
| 1527 /** | |
| 1528 * @param {DirectoryEntry} directoryEntry Directory to be opened. | |
| 1529 * @param {Entry=} opt_selectionEntry Entry to be selected. | |
| 1530 * @param {string=} opt_suggestedName Suggested name for a non-existing\ | |
| 1531 * selection. | |
| 1532 * @private | |
| 1533 */ | |
| 1534 FileManager.prototype.finishSetupCurrentDirectory_ = function( | |
| 1535 directoryEntry, opt_selectionEntry, opt_suggestedName) { | |
| 1536 // Open the directory, and select the selection (if passed). | |
| 1537 if (util.isFakeEntry(directoryEntry)) { | |
| 1538 this.directoryModel_.specialSearch(directoryEntry, ''); | |
| 1539 } else { | |
| 1540 this.directoryModel_.changeDirectoryEntry(directoryEntry, function() { | |
| 1541 if (opt_selectionEntry) | |
| 1542 this.directoryModel_.selectEntry(opt_selectionEntry); | |
| 1543 }.bind(this)); | |
| 1544 } | |
| 1545 | |
| 1546 if (this.dialogType === DialogType.FULL_PAGE) { | |
| 1547 // In the FULL_PAGE mode if the restored URL points to a file we might | |
| 1548 // have to invoke a task after selecting it. | |
| 1549 if (this.params_.action === 'select') | |
| 1550 return; | |
| 1551 | |
| 1552 var task = null; | |
| 1553 // Handle restoring after crash, or the gallery action. | |
| 1554 // TODO(mtomasz): Use the gallery action instead of just the gallery | |
| 1555 // field. | |
| 1556 if (this.params_.gallery || | |
| 1557 this.params_.action === 'gallery' || | |
| 1558 this.params_.action === 'gallery-video') { | |
| 1559 if (!opt_selectionEntry) { | |
| 1560 // Non-existent file or a directory. | |
| 1561 // Reloading while the Gallery is open with empty or multiple | |
| 1562 // selection. Open the Gallery when the directory is scanned. | |
| 1563 task = function() { | |
| 1564 new FileTasks(this, this.params_).openGallery([]); | |
| 1565 }.bind(this); | |
| 1566 } else { | |
| 1567 // The file or the directory exists. | |
| 1568 task = function() { | |
| 1569 new FileTasks(this, this.params_).openGallery([opt_selectionEntry]); | |
| 1570 }.bind(this); | |
| 1571 } | |
| 1572 } else { | |
| 1573 // TODO(mtomasz): Implement remounting archives after crash. | |
| 1574 // See: crbug.com/333139 | |
| 1575 } | |
| 1576 | |
| 1577 // If there is a task to be run, run it after the scan is completed. | |
| 1578 if (task) { | |
| 1579 var listener = function() { | |
| 1580 if (!util.isSameEntry(this.directoryModel_.getCurrentDirEntry(), | |
| 1581 directoryEntry)) { | |
| 1582 // Opened on a different URL. Probably fallbacked. Therefore, | |
| 1583 // do not invoke a task. | |
| 1584 return; | |
| 1585 } | |
| 1586 this.directoryModel_.removeEventListener( | |
| 1587 'scan-completed', listener); | |
| 1588 task(); | |
| 1589 }.bind(this); | |
| 1590 this.directoryModel_.addEventListener('scan-completed', listener); | |
| 1591 } | |
| 1592 } else if (this.dialogType === DialogType.SELECT_SAVEAS_FILE) { | |
| 1593 this.filenameInput_.value = opt_suggestedName || ''; | |
| 1594 this.selectTargetNameInFilenameInput_(); | |
| 1595 } | |
| 1596 }; | |
| 1597 | |
| 1598 /** | |
| 1599 * @private | |
| 1600 */ | |
| 1601 FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() { | |
| 1602 var entries = this.directoryModel_.getFileList().slice(); | |
| 1603 var directoryEntry = this.directoryModel_.getCurrentDirEntry(); | |
| 1604 if (!directoryEntry) | |
| 1605 return; | |
| 1606 // We don't pass callback here. When new metadata arrives, we have an | |
| 1607 // observer registered to update the UI. | |
| 1608 | |
| 1609 // TODO(dgozman): refresh content metadata only when modificationTime | |
| 1610 // changed. | |
| 1611 var isFakeEntry = util.isFakeEntry(directoryEntry); | |
| 1612 var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries); | |
| 1613 if (!isFakeEntry) | |
| 1614 this.metadataCache_.clearRecursively(directoryEntry, '*'); | |
| 1615 this.metadataCache_.get(getEntries, 'filesystem', null); | |
| 1616 | |
| 1617 if (this.isOnDrive()) | |
| 1618 this.metadataCache_.get(getEntries, 'drive', null); | |
| 1619 | |
| 1620 var visibleItems = this.currentList_.items; | |
| 1621 var visibleEntries = []; | |
| 1622 for (var i = 0; i < visibleItems.length; i++) { | |
| 1623 var index = this.currentList_.getIndexOfListItem(visibleItems[i]); | |
| 1624 var entry = this.directoryModel_.getFileList().item(index); | |
| 1625 // The following check is a workaround for the bug in list: sometimes item | |
| 1626 // does not have listIndex, and therefore is not found in the list. | |
| 1627 if (entry) visibleEntries.push(entry); | |
| 1628 } | |
| 1629 this.metadataCache_.get(visibleEntries, 'thumbnail', null); | |
| 1630 }; | |
| 1631 | |
| 1632 /** | |
| 1633 * @private | |
| 1634 */ | |
| 1635 FileManager.prototype.dailyUpdateModificationTime_ = function() { | |
| 1636 var entries = this.directoryModel_.getFileList().slice(); | |
| 1637 this.metadataCache_.get( | |
| 1638 entries, | |
| 1639 'filesystem', | |
| 1640 this.updateMetadataInUI_.bind(this, 'filesystem', entries)); | |
| 1641 | |
| 1642 setTimeout(this.dailyUpdateModificationTime_.bind(this), | |
| 1643 MILLISECONDS_IN_DAY); | |
| 1644 }; | |
| 1645 | |
| 1646 /** | |
| 1647 * @param {string} type Type of metadata changed. | |
| 1648 * @param {Array.<Entry>} entries Array of entries. | |
| 1649 * @param {Object.<string, Object>} props Map from entry URLs to metadata | |
| 1650 * props. | |
| 1651 * @private | |
| 1652 */ | |
| 1653 FileManager.prototype.updateMetadataInUI_ = function( | |
| 1654 type, entries, properties) { | |
| 1655 if (this.listType_ == FileManager.ListType.DETAIL) | |
| 1656 this.table_.updateListItemsMetadata(type, properties); | |
| 1657 else | |
| 1658 this.grid_.updateListItemsMetadata(type, properties); | |
| 1659 // TODO: update bottom panel thumbnails. | |
| 1660 }; | |
| 1661 | |
| 1662 /** | |
| 1663 * Restore the item which is being renamed while refreshing the file list. Do | |
| 1664 * nothing if no item is being renamed or such an item disappeared. | |
| 1665 * | |
| 1666 * While refreshing file list it gets repopulated with new file entries. | |
| 1667 * There is not a big difference whether DOM items stay the same or not. | |
| 1668 * Except for the item that the user is renaming. | |
| 1669 * | |
| 1670 * @private | |
| 1671 */ | |
| 1672 FileManager.prototype.restoreItemBeingRenamed_ = function() { | |
| 1673 if (!this.isRenamingInProgress()) | |
| 1674 return; | |
| 1675 | |
| 1676 var dm = this.directoryModel_; | |
| 1677 var leadIndex = dm.getFileListSelection().leadIndex; | |
| 1678 if (leadIndex < 0) | |
| 1679 return; | |
| 1680 | |
| 1681 var leadEntry = dm.getFileList().item(leadIndex); | |
| 1682 if (!util.isSameEntry(this.renameInput_.currentEntry, leadEntry)) | |
| 1683 return; | |
| 1684 | |
| 1685 var leadListItem = this.findListItemForNode_(this.renameInput_); | |
| 1686 if (this.currentList_ == this.table_.list) { | |
| 1687 this.table_.updateFileMetadata(leadListItem, leadEntry); | |
| 1688 } | |
| 1689 this.currentList_.restoreLeadItem(leadListItem); | |
| 1690 }; | |
| 1691 | |
| 1692 /** | |
| 1693 * TODO(mtomasz): Move this to a utility function working on the root type. | |
| 1694 * @return {boolean} True if the current directory content is from Google | |
| 1695 * Drive. | |
| 1696 */ | |
| 1697 FileManager.prototype.isOnDrive = function() { | |
| 1698 var rootType = this.directoryModel_.getCurrentRootType(); | |
| 1699 return rootType === RootType.DRIVE || | |
| 1700 rootType === RootType.DRIVE_SHARED_WITH_ME || | |
| 1701 rootType === RootType.DRIVE_RECENT || | |
| 1702 rootType === RootType.DRIVE_OFFLINE; | |
| 1703 }; | |
| 1704 | |
| 1705 /** | |
| 1706 * Overrides default handling for clicks on hyperlinks. | |
| 1707 * In a packaged apps links with targer='_blank' open in a new tab by | |
| 1708 * default, other links do not open at all. | |
| 1709 * | |
| 1710 * @param {Event} event Click event. | |
| 1711 * @private | |
| 1712 */ | |
| 1713 FileManager.prototype.onExternalLinkClick_ = function(event) { | |
| 1714 if (event.target.tagName != 'A' || !event.target.href) | |
| 1715 return; | |
| 1716 | |
| 1717 if (this.dialogType != DialogType.FULL_PAGE) | |
| 1718 this.onCancel_(); | |
| 1719 }; | |
| 1720 | |
| 1721 /** | |
| 1722 * Task combobox handler. | |
| 1723 * | |
| 1724 * @param {Object} event Event containing task which was clicked. | |
| 1725 * @private | |
| 1726 */ | |
| 1727 FileManager.prototype.onTaskItemClicked_ = function(event) { | |
| 1728 var selection = this.getSelection(); | |
| 1729 if (!selection.tasks) return; | |
| 1730 | |
| 1731 if (event.item.task) { | |
| 1732 // Task field doesn't exist on change-default dropdown item. | |
| 1733 selection.tasks.execute(event.item.task.taskId); | |
| 1734 } else { | |
| 1735 var extensions = []; | |
| 1736 | |
| 1737 for (var i = 0; i < selection.entries.length; i++) { | |
| 1738 var match = /\.(\w+)$/g.exec(selection.entries[i].toURL()); | |
| 1739 if (match) { | |
| 1740 var ext = match[1].toUpperCase(); | |
| 1741 if (extensions.indexOf(ext) == -1) { | |
| 1742 extensions.push(ext); | |
| 1743 } | |
| 1744 } | |
| 1745 } | |
| 1746 | |
| 1747 var format = ''; | |
| 1748 | |
| 1749 if (extensions.length == 1) { | |
| 1750 format = extensions[0]; | |
| 1751 } | |
| 1752 | |
| 1753 // Change default was clicked. We should open "change default" dialog. | |
| 1754 selection.tasks.showTaskPicker(this.defaultTaskPicker, | |
| 1755 loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'), | |
| 1756 strf('CHANGE_DEFAULT_CAPTION', format), | |
| 1757 this.onDefaultTaskDone_.bind(this)); | |
| 1758 } | |
| 1759 }; | |
| 1760 | |
| 1761 /** | |
| 1762 * Sets the given task as default, when this task is applicable. | |
| 1763 * | |
| 1764 * @param {Object} task Task to set as default. | |
| 1765 * @private | |
| 1766 */ | |
| 1767 FileManager.prototype.onDefaultTaskDone_ = function(task) { | |
| 1768 // TODO(dgozman): move this method closer to tasks. | |
| 1769 var selection = this.getSelection(); | |
| 1770 chrome.fileBrowserPrivate.setDefaultTask( | |
| 1771 task.taskId, | |
| 1772 util.entriesToURLs(selection.entries), | |
| 1773 selection.mimeTypes); | |
| 1774 selection.tasks = new FileTasks(this); | |
| 1775 selection.tasks.init(selection.entries, selection.mimeTypes); | |
| 1776 selection.tasks.display(this.taskItems_); | |
| 1777 this.refreshCurrentDirectoryMetadata_(); | |
| 1778 this.selectionHandler_.onFileSelectionChanged(); | |
| 1779 }; | |
| 1780 | |
| 1781 /** | |
| 1782 * @private | |
| 1783 */ | |
| 1784 FileManager.prototype.onPreferencesChanged_ = function() { | |
| 1785 var self = this; | |
| 1786 this.getPreferences_(function(prefs) { | |
| 1787 self.initDateTimeFormatters_(); | |
| 1788 self.refreshCurrentDirectoryMetadata_(); | |
| 1789 | |
| 1790 if (prefs.cellularDisabled) | |
| 1791 self.syncButton.setAttribute('checked', ''); | |
| 1792 else | |
| 1793 self.syncButton.removeAttribute('checked'); | |
| 1794 | |
| 1795 if (self.hostedButton.hasAttribute('checked') != | |
| 1796 prefs.hostedFilesDisabled && self.isOnDrive()) { | |
| 1797 self.directoryModel_.rescan(); | |
| 1798 } | |
| 1799 | |
| 1800 if (!prefs.hostedFilesDisabled) | |
| 1801 self.hostedButton.setAttribute('checked', ''); | |
| 1802 else | |
| 1803 self.hostedButton.removeAttribute('checked'); | |
| 1804 }, | |
| 1805 true /* refresh */); | |
| 1806 }; | |
| 1807 | |
| 1808 FileManager.prototype.onDriveConnectionChanged_ = function() { | |
| 1809 var connection = this.volumeManager_.getDriveConnectionState(); | |
| 1810 if (this.commandHandler) | |
| 1811 this.commandHandler.updateAvailability(); | |
| 1812 if (this.dialogContainer_) | |
| 1813 this.dialogContainer_.setAttribute('connection', connection.type); | |
| 1814 this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR); | |
| 1815 this.suggestAppsDialog.onDriveConnectionChanged(connection.type); | |
| 1816 }; | |
| 1817 | |
| 1818 /** | |
| 1819 * Tells whether the current directory is read only. | |
| 1820 * TODO(mtomasz): Remove and use EntryLocation directly. | |
| 1821 * @return {boolean} True if read only, false otherwise. | |
| 1822 */ | |
| 1823 FileManager.prototype.isOnReadonlyDirectory = function() { | |
| 1824 return this.directoryModel_.isReadOnly(); | |
| 1825 }; | |
| 1826 | |
| 1827 /** | |
| 1828 * @param {Event} Unmount event. | |
| 1829 * @private | |
| 1830 */ | |
| 1831 FileManager.prototype.onExternallyUnmounted_ = function(event) { | |
| 1832 if (event.volumeInfo === this.currentVolumeInfo_) { | |
| 1833 if (this.closeOnUnmount_) { | |
| 1834 // If the file manager opened automatically when a usb drive inserted, | |
| 1835 // user have never changed current volume (that implies the current | |
| 1836 // directory is still on the device) then close this window. | |
| 1837 window.close(); | |
| 1838 } | |
| 1839 } | |
| 1840 }; | |
| 1841 | |
| 1842 /** | |
| 1843 * Shows a modal-like file viewer/editor on top of the File Manager UI. | |
| 1844 * | |
| 1845 * @param {HTMLElement} popup Popup element. | |
| 1846 * @param {function()} closeCallback Function to call after the popup is | |
| 1847 * closed. | |
| 1848 */ | |
| 1849 FileManager.prototype.openFilePopup = function(popup, closeCallback) { | |
| 1850 this.closeFilePopup(); | |
| 1851 this.filePopup_ = popup; | |
| 1852 this.filePopupCloseCallback_ = closeCallback; | |
| 1853 this.dialogDom_.insertBefore( | |
| 1854 this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area')); | |
| 1855 this.filePopup_.focus(); | |
| 1856 this.document_.body.setAttribute('overlay-visible', ''); | |
| 1857 this.document_.querySelector('#iframe-drag-area').hidden = false; | |
| 1858 }; | |
| 1859 | |
| 1860 /** | |
| 1861 * Closes the modal-like file viewer/editor popup. | |
| 1862 */ | |
| 1863 FileManager.prototype.closeFilePopup = function() { | |
| 1864 if (this.filePopup_) { | |
| 1865 this.document_.body.removeAttribute('overlay-visible'); | |
| 1866 this.document_.querySelector('#iframe-drag-area').hidden = true; | |
| 1867 // The window resize would not be processed properly while the relevant | |
| 1868 // divs had 'display:none', force resize after the layout fired. | |
| 1869 setTimeout(this.onResize_.bind(this), 0); | |
| 1870 if (this.filePopup_.contentWindow && | |
| 1871 this.filePopup_.contentWindow.unload) { | |
| 1872 this.filePopup_.contentWindow.unload(); | |
| 1873 } | |
| 1874 | |
| 1875 if (this.filePopupCloseCallback_) { | |
| 1876 this.filePopupCloseCallback_(); | |
| 1877 this.filePopupCloseCallback_ = null; | |
| 1878 } | |
| 1879 | |
| 1880 // These operations have to be in the end, otherwise v8 crashes on an | |
| 1881 // assert. See: crbug.com/224174. | |
| 1882 this.dialogDom_.removeChild(this.filePopup_); | |
| 1883 this.filePopup_ = null; | |
| 1884 } | |
| 1885 }; | |
| 1886 | |
| 1887 /** | |
| 1888 * Updates visibility of the draggable app region in the modal-like file | |
| 1889 * viewer/editor. | |
| 1890 * | |
| 1891 * @param {boolean} visible True for visible, false otherwise. | |
| 1892 */ | |
| 1893 FileManager.prototype.onFilePopupAppRegionChanged = function(visible) { | |
| 1894 if (!this.filePopup_) | |
| 1895 return; | |
| 1896 | |
| 1897 this.document_.querySelector('#iframe-drag-area').hidden = !visible; | |
| 1898 }; | |
| 1899 | |
| 1900 /** | |
| 1901 * @return {Array.<Entry>} List of all entries in the current directory. | |
| 1902 */ | |
| 1903 FileManager.prototype.getAllEntriesInCurrentDirectory = function() { | |
| 1904 return this.directoryModel_.getFileList().slice(); | |
| 1905 }; | |
| 1906 | |
| 1907 FileManager.prototype.isRenamingInProgress = function() { | |
| 1908 return !!this.renameInput_.currentEntry; | |
| 1909 }; | |
| 1910 | |
| 1911 /** | |
| 1912 * @private | |
| 1913 */ | |
| 1914 FileManager.prototype.focusCurrentList_ = function() { | |
| 1915 if (this.listType_ == FileManager.ListType.DETAIL) | |
| 1916 this.table_.focus(); | |
| 1917 else // this.listType_ == FileManager.ListType.THUMBNAIL) | |
| 1918 this.grid_.focus(); | |
| 1919 }; | |
| 1920 | |
| 1921 /** | |
| 1922 * Return DirectoryEntry of the current directory or null. | |
| 1923 * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns | |
| 1924 * null if the directory model is not ready or the current directory is | |
| 1925 * not set. | |
| 1926 */ | |
| 1927 FileManager.prototype.getCurrentDirectoryEntry = function() { | |
| 1928 return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry(); | |
| 1929 }; | |
| 1930 | |
| 1931 /** | |
| 1932 * Deletes the selected file and directories recursively. | |
| 1933 */ | |
| 1934 FileManager.prototype.deleteSelection = function() { | |
| 1935 // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364 | |
| 1936 var entries = this.getSelection().entries; | |
| 1937 var message = entries.length == 1 ? | |
| 1938 strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) : | |
| 1939 strf('GALLERY_CONFIRM_DELETE_SOME', entries.length); | |
| 1940 this.confirm.show(message, function() { | |
| 1941 this.fileOperationManager_.deleteEntries(entries); | |
| 1942 }.bind(this)); | |
| 1943 }; | |
| 1944 | |
| 1945 /** | |
| 1946 * Shows the share dialog for the selected file or directory. | |
| 1947 */ | |
| 1948 FileManager.prototype.shareSelection = function() { | |
| 1949 var entries = this.getSelection().entries; | |
| 1950 if (entries.length != 1) { | |
| 1951 console.warn('Unable to share multiple items at once.'); | |
| 1952 return; | |
| 1953 } | |
| 1954 // Add the overlapped class to prevent the applicaiton window from | |
| 1955 // captureing mouse events. | |
| 1956 this.shareDialog_.show(entries[0], function(result) { | |
| 1957 if (result == ShareDialog.Result.NETWORK_ERROR) | |
| 1958 this.error.show(str('SHARE_ERROR')); | |
| 1959 }.bind(this)); | |
| 1960 }; | |
| 1961 | |
| 1962 /** | |
| 1963 * Creates a folder shortcut. | |
| 1964 * @param {Entry} entry A shortcut which refers to |entry| to be created. | |
| 1965 */ | |
| 1966 FileManager.prototype.createFolderShortcut = function(entry) { | |
| 1967 // Duplicate entry. | |
| 1968 if (this.folderShortcutExists(entry)) | |
| 1969 return; | |
| 1970 | |
| 1971 this.folderShortcutsModel_.add(entry); | |
| 1972 }; | |
| 1973 | |
| 1974 /** | |
| 1975 * Checkes if the shortcut which refers to the given folder exists or not. | |
| 1976 * @param {Entry} entry Entry of the folder to be checked. | |
| 1977 */ | |
| 1978 FileManager.prototype.folderShortcutExists = function(entry) { | |
| 1979 return this.folderShortcutsModel_.exists(entry); | |
| 1980 }; | |
| 1981 | |
| 1982 /** | |
| 1983 * Removes the folder shortcut. | |
| 1984 * @param {Entry} entry The shortcut which refers to |entry| is to be removed. | |
| 1985 */ | |
| 1986 FileManager.prototype.removeFolderShortcut = function(entry) { | |
| 1987 this.folderShortcutsModel_.remove(entry); | |
| 1988 }; | |
| 1989 | |
| 1990 /** | |
| 1991 * Blinks the selection. Used to give feedback when copying or cutting the | |
| 1992 * selection. | |
| 1993 */ | |
| 1994 FileManager.prototype.blinkSelection = function() { | |
| 1995 var selection = this.getSelection(); | |
| 1996 if (!selection || selection.totalCount == 0) | |
| 1997 return; | |
| 1998 | |
| 1999 for (var i = 0; i < selection.entries.length; i++) { | |
| 2000 var selectedIndex = selection.indexes[i]; | |
| 2001 var listItem = this.currentList_.getListItemByIndex(selectedIndex); | |
| 2002 if (listItem) | |
| 2003 this.blinkListItem_(listItem); | |
| 2004 } | |
| 2005 }; | |
| 2006 | |
| 2007 /** | |
| 2008 * @param {Element} listItem List item element. | |
| 2009 * @private | |
| 2010 */ | |
| 2011 FileManager.prototype.blinkListItem_ = function(listItem) { | |
| 2012 listItem.classList.add('blink'); | |
| 2013 setTimeout(function() { | |
| 2014 listItem.classList.remove('blink'); | |
| 2015 }, 100); | |
| 2016 }; | |
| 2017 | |
| 2018 /** | |
| 2019 * @private | |
| 2020 */ | |
| 2021 FileManager.prototype.selectTargetNameInFilenameInput_ = function() { | |
| 2022 var input = this.filenameInput_; | |
| 2023 input.focus(); | |
| 2024 var selectionEnd = input.value.lastIndexOf('.'); | |
| 2025 if (selectionEnd == -1) { | |
| 2026 input.select(); | |
| 2027 } else { | |
| 2028 input.selectionStart = 0; | |
| 2029 input.selectionEnd = selectionEnd; | |
| 2030 } | |
| 2031 }; | |
| 2032 | |
| 2033 /** | |
| 2034 * Handles mouse click or tap. | |
| 2035 * | |
| 2036 * @param {Event} event The click event. | |
| 2037 * @private | |
| 2038 */ | |
| 2039 FileManager.prototype.onDetailClick_ = function(event) { | |
| 2040 if (this.isRenamingInProgress()) { | |
| 2041 // Don't pay attention to clicks during a rename. | |
| 2042 return; | |
| 2043 } | |
| 2044 | |
| 2045 var listItem = this.findListItemForEvent_(event); | |
| 2046 var selection = this.getSelection(); | |
| 2047 if (!listItem || !listItem.selected || selection.totalCount != 1) { | |
| 2048 return; | |
| 2049 } | |
| 2050 | |
| 2051 // React on double click, but only if both clicks hit the same item. | |
| 2052 // TODO(mtomasz): Simplify it, and use a double click handler if possible. | |
| 2053 var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined; | |
| 2054 this.lastClickedItem_ = listItem; | |
| 2055 | |
| 2056 if (event.detail != clickNumber) | |
| 2057 return; | |
| 2058 | |
| 2059 var entry = selection.entries[0]; | |
| 2060 if (entry.isDirectory) { | |
| 2061 this.onDirectoryAction_(entry); | |
| 2062 } else { | |
| 2063 this.dispatchSelectionAction_(); | |
| 2064 } | |
| 2065 }; | |
| 2066 | |
| 2067 /** | |
| 2068 * @private | |
| 2069 */ | |
| 2070 FileManager.prototype.dispatchSelectionAction_ = function() { | |
| 2071 if (this.dialogType == DialogType.FULL_PAGE) { | |
| 2072 var selection = this.getSelection(); | |
| 2073 var tasks = selection.tasks; | |
| 2074 var urls = selection.urls; | |
| 2075 var mimeTypes = selection.mimeTypes; | |
| 2076 if (tasks) | |
| 2077 tasks.executeDefault(); | |
| 2078 return true; | |
| 2079 } | |
| 2080 if (!this.okButton_.disabled) { | |
| 2081 this.onOk_(); | |
| 2082 return true; | |
| 2083 } | |
| 2084 return false; | |
| 2085 }; | |
| 2086 | |
| 2087 /** | |
| 2088 * Opens the suggest file dialog. | |
| 2089 * | |
| 2090 * @param {Entry} entry Entry of the file. | |
| 2091 * @param {function()} onSuccess Success callback. | |
| 2092 * @param {function()} onCancelled User-cancelled callback. | |
| 2093 * @param {function()} onFailure Failure callback. | |
| 2094 * @private | |
| 2095 */ | |
| 2096 FileManager.prototype.openSuggestAppsDialog = | |
| 2097 function(entry, onSuccess, onCancelled, onFailure) { | |
| 2098 if (!url) { | |
| 2099 onFailure(); | |
| 2100 return; | |
| 2101 } | |
| 2102 | |
| 2103 this.metadataCache_.get([entry], 'drive', function(props) { | |
| 2104 if (!props || !props[0] || !props[0].contentMimeType) { | |
| 2105 onFailure(); | |
| 2106 return; | |
| 2107 } | |
| 2108 | |
| 2109 var basename = entry.name; | |
| 2110 var splitted = PathUtil.splitExtension(basename); | |
| 2111 var filename = splitted[0]; | |
| 2112 var extension = splitted[1]; | |
| 2113 var mime = props[0].contentMimeType; | |
| 2114 | |
| 2115 // Returns with failure if the file has neither extension nor mime. | |
| 2116 if (!extension || !mime) { | |
| 2117 onFailure(); | |
| 2118 return; | |
| 2119 } | |
| 2120 | |
| 2121 var onDialogClosed = function(result) { | |
| 2122 switch (result) { | |
| 2123 case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL: | |
| 2124 onSuccess(); | |
| 2125 break; | |
| 2126 case SuggestAppsDialog.Result.FAILED: | |
| 2127 onFailure(); | |
| 2128 break; | |
| 2129 default: | |
| 2130 onCancelled(); | |
| 2131 } | |
| 2132 }; | |
| 2133 | |
| 2134 if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) { | |
| 2135 this.suggestAppsDialog.showByFilename(filename, onDialogClosed); | |
| 2136 } else { | |
| 2137 this.suggestAppsDialog.showByExtensionAndMime( | |
| 2138 extension, mime, onDialogClosed); | |
| 2139 } | |
| 2140 }.bind(this)); | |
| 2141 }; | |
| 2142 | |
| 2143 /** | |
| 2144 * Called when a dialog is shown or hidden. | |
| 2145 * @param {boolean} flag True if a dialog is shown, false if hidden. | |
| 2146 */ | |
| 2147 FileManager.prototype.onDialogShownOrHidden = function(show) { | |
| 2148 if (show) { | |
| 2149 // If a dialog is shown, activate the window. | |
| 2150 var appWindow = chrome.app.window.current(); | |
| 2151 if (appWindow) | |
| 2152 appWindow.focus(); | |
| 2153 } | |
| 2154 | |
| 2155 // Set/unset a flag to disable dragging on the title area. | |
| 2156 this.dialogContainer_.classList.toggle('disable-header-drag', show); | |
| 2157 }; | |
| 2158 | |
| 2159 /** | |
| 2160 * Executes directory action (i.e. changes directory). | |
| 2161 * | |
| 2162 * @param {DirectoryEntry} entry Directory entry to which directory should be | |
| 2163 * changed. | |
| 2164 * @private | |
| 2165 */ | |
| 2166 FileManager.prototype.onDirectoryAction_ = function(entry) { | |
| 2167 return this.directoryModel_.changeDirectoryEntry(entry); | |
| 2168 }; | |
| 2169 | |
| 2170 /** | |
| 2171 * Update the window title. | |
| 2172 * @private | |
| 2173 */ | |
| 2174 FileManager.prototype.updateTitle_ = function() { | |
| 2175 if (this.dialogType != DialogType.FULL_PAGE) | |
| 2176 return; | |
| 2177 | |
| 2178 if (!this.currentVolumeInfo_) | |
| 2179 return; | |
| 2180 | |
| 2181 this.document_.title = this.currentVolumeInfo_.label; | |
| 2182 }; | |
| 2183 | |
| 2184 /** | |
| 2185 * Update the gear menu. | |
| 2186 * @private | |
| 2187 */ | |
| 2188 FileManager.prototype.updateGearMenu_ = function() { | |
| 2189 var hideItemsForDrive = !this.isOnDrive(); | |
| 2190 this.syncButton.hidden = hideItemsForDrive; | |
| 2191 this.hostedButton.hidden = hideItemsForDrive; | |
| 2192 this.document_.getElementById('drive-separator').hidden = | |
| 2193 hideItemsForDrive; | |
| 2194 this.refreshRemainingSpace_(true); // Show loading caption. | |
| 2195 }; | |
| 2196 | |
| 2197 /** | |
| 2198 * Update menus that move the window to the other profile's desktop. | |
| 2199 * TODO(hirono): Add the GearMenu class and make it a member of the class. | |
| 2200 * TODO(hirono): Handle the case where a profile is added while the menu is | |
| 2201 * opened. | |
| 2202 * @private | |
| 2203 */ | |
| 2204 FileManager.prototype.updateVisitDesktopMenus_ = function() { | |
| 2205 var gearMenu = this.document_.querySelector('#gear-menu'); | |
| 2206 var separator = | |
| 2207 this.document_.querySelector('#multi-profile-separator'); | |
| 2208 | |
| 2209 // Remove existing menu items. | |
| 2210 var oldItems = | |
| 2211 this.document_.querySelectorAll('#gear-menu .visit-desktop'); | |
| 2212 for (var i = 0; i < oldItems.length; i++) { | |
| 2213 gearMenu.removeChild(oldItems[i]); | |
| 2214 } | |
| 2215 separator.hidden = true; | |
| 2216 | |
| 2217 if (this.dialogType !== DialogType.FULL_PAGE) | |
| 2218 return; | |
| 2219 | |
| 2220 // Obtain the profile information. | |
| 2221 chrome.fileBrowserPrivate.getProfiles(function(profiles, | |
| 2222 currentId, | |
| 2223 displayedId) { | |
| 2224 // Check if the menus are needed or not. | |
| 2225 var insertingPosition = separator.nextSibling; | |
| 2226 if (profiles.length === 1 && profiles[0].profileId === displayedId) | |
| 2227 return; | |
| 2228 | |
| 2229 separator.hidden = false; | |
| 2230 for (var i = 0; i < profiles.length; i++) { | |
| 2231 var profile = profiles[i]; | |
| 2232 if (profile.profileId === displayedId) | |
| 2233 continue; | |
| 2234 var item = this.document_.createElement('menuitem'); | |
| 2235 cr.ui.MenuItem.decorate(item); | |
| 2236 gearMenu.insertBefore(item, insertingPosition); | |
| 2237 item.className = 'visit-desktop'; | |
| 2238 item.label = strf('VISIT_DESKTOP_OF_USER', | |
| 2239 profile.displayName, | |
| 2240 profile.profileId); | |
| 2241 item.addEventListener('activate', function(inProfile, event) { | |
| 2242 // Stop propagate and hide the menu manually, in order to prevent the | |
| 2243 // focus from being back to the button. (cf. http://crbug.com/248479) | |
| 2244 event.stopPropagation(); | |
| 2245 this.gearButton_.hideMenu(); | |
| 2246 this.gearButton_.blur(); | |
| 2247 chrome.fileBrowserPrivate.visitDesktop(inProfile.profileId); | |
| 2248 }.bind(this, profile)); | |
| 2249 } | |
| 2250 }.bind(this)); | |
| 2251 }; | |
| 2252 | |
| 2253 /** | |
| 2254 * Refreshes space info of the current volume. | |
| 2255 * @param {boolean} showLoadingCaption Whether show loading caption or not. | |
| 2256 * @private | |
| 2257 */ | |
| 2258 FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) { | |
| 2259 if (!this.currentVolumeInfo_) | |
| 2260 return; | |
| 2261 | |
| 2262 var volumeSpaceInfoLabel = | |
| 2263 this.dialogDom_.querySelector('#volume-space-info-label'); | |
| 2264 var volumeSpaceInnerBar = | |
| 2265 this.dialogDom_.querySelector('#volume-space-info-bar'); | |
| 2266 var volumeSpaceOuterBar = | |
| 2267 this.dialogDom_.querySelector('#volume-space-info-bar').parentNode; | |
| 2268 | |
| 2269 volumeSpaceInnerBar.setAttribute('pending', ''); | |
| 2270 | |
| 2271 if (showLoadingCaption) { | |
| 2272 volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO'); | |
| 2273 volumeSpaceInnerBar.style.width = '100%'; | |
| 2274 } | |
| 2275 | |
| 2276 var currentVolumeInfo = this.currentVolumeInfo_; | |
| 2277 chrome.fileBrowserPrivate.getSizeStats( | |
| 2278 currentVolumeInfo.volumeId, function(result) { | |
| 2279 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
| 2280 this.directoryModel_.getCurrentDirEntry()); | |
| 2281 if (currentVolumeInfo !== this.currentVolumeInfo_) | |
| 2282 return; | |
| 2283 updateSpaceInfo(result, | |
| 2284 volumeSpaceInnerBar, | |
| 2285 volumeSpaceInfoLabel, | |
| 2286 volumeSpaceOuterBar); | |
| 2287 }.bind(this)); | |
| 2288 }; | |
| 2289 | |
| 2290 /** | |
| 2291 * Update the UI when the current directory changes. | |
| 2292 * | |
| 2293 * @param {Event} event The directory-changed event. | |
| 2294 * @private | |
| 2295 */ | |
| 2296 FileManager.prototype.onDirectoryChanged_ = function(event) { | |
| 2297 var newCurrentVolumeInfo = this.volumeManager_.getVolumeInfo( | |
| 2298 event.newDirEntry); | |
| 2299 | |
| 2300 // If volume has changed, then update the gear menu. | |
| 2301 if (this.currentVolumeInfo_ !== newCurrentVolumeInfo) { | |
| 2302 this.updateGearMenu_(); | |
| 2303 // If the volume has changed, and it was previously set, then do not | |
| 2304 // close on unmount anymore. | |
| 2305 if (this.currentVolumeInfo_) | |
| 2306 this.closeOnUnmount_ = false; | |
| 2307 } | |
| 2308 | |
| 2309 // Remember the current volume info. | |
| 2310 this.currentVolumeInfo_ = newCurrentVolumeInfo; | |
| 2311 | |
| 2312 this.selectionHandler_.onFileSelectionChanged(); | |
| 2313 this.ui_.searchBox.clear(); | |
| 2314 // TODO(mtomasz): Consider remembering the selection. | |
| 2315 util.updateAppState( | |
| 2316 this.getCurrentDirectoryEntry() ? | |
| 2317 this.getCurrentDirectoryEntry().toURL() : '', | |
| 2318 '' /* selectionURL */, | |
| 2319 '' /* opt_param */); | |
| 2320 | |
| 2321 if (this.commandHandler) | |
| 2322 this.commandHandler.updateAvailability(); | |
| 2323 | |
| 2324 this.updateUnformattedVolumeStatus_(); | |
| 2325 this.updateTitle_(); | |
| 2326 | |
| 2327 var currentEntry = this.getCurrentDirectoryEntry(); | |
| 2328 this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ? | |
| 2329 null : currentEntry; | |
| 2330 }; | |
| 2331 | |
| 2332 FileManager.prototype.updateUnformattedVolumeStatus_ = function() { | |
| 2333 var volumeInfo = this.volumeManager_.getVolumeInfo( | |
| 2334 this.directoryModel_.getCurrentDirEntry()); | |
| 2335 | |
| 2336 if (volumeInfo && volumeInfo.error) { | |
| 2337 this.dialogDom_.setAttribute('unformatted', ''); | |
| 2338 | |
| 2339 var errorNode = this.dialogDom_.querySelector('#format-panel > .error'); | |
| 2340 if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) { | |
| 2341 errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING'); | |
| 2342 } else { | |
| 2343 errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING'); | |
| 2344 } | |
| 2345 | |
| 2346 // Update 'canExecute' for format command so the format button's disabled | |
| 2347 // property is properly set. | |
| 2348 if (this.commandHandler) | |
| 2349 this.commandHandler.updateAvailability(); | |
| 2350 } else { | |
| 2351 this.dialogDom_.removeAttribute('unformatted'); | |
| 2352 } | |
| 2353 }; | |
| 2354 | |
| 2355 FileManager.prototype.findListItemForEvent_ = function(event) { | |
| 2356 return this.findListItemForNode_(event.touchedElement || event.srcElement); | |
| 2357 }; | |
| 2358 | |
| 2359 FileManager.prototype.findListItemForNode_ = function(node) { | |
| 2360 var item = this.currentList_.getListItemAncestor(node); | |
| 2361 // TODO(serya): list should check that. | |
| 2362 return item && this.currentList_.isItem(item) ? item : null; | |
| 2363 }; | |
| 2364 | |
| 2365 /** | |
| 2366 * Unload handler for the page. | |
| 2367 * @private | |
| 2368 */ | |
| 2369 FileManager.prototype.onUnload_ = function() { | |
| 2370 if (this.directoryModel_) | |
| 2371 this.directoryModel_.dispose(); | |
| 2372 if (this.volumeManager_) | |
| 2373 this.volumeManager_.dispose(); | |
| 2374 if (this.filePopup_ && | |
| 2375 this.filePopup_.contentWindow && | |
| 2376 this.filePopup_.contentWindow.unload) | |
| 2377 this.filePopup_.contentWindow.unload(true /* exiting */); | |
| 2378 if (this.progressCenterPanel_) | |
| 2379 this.backgroundPage_.background.progressCenter.removePanel( | |
| 2380 this.progressCenterPanel_); | |
| 2381 if (this.fileOperationManager_) { | |
| 2382 if (this.onCopyProgressBound_) { | |
| 2383 this.fileOperationManager_.removeEventListener( | |
| 2384 'copy-progress', this.onCopyProgressBound_); | |
| 2385 } | |
| 2386 if (this.onEntryChangedBound_) { | |
| 2387 this.fileOperationManager_.removeEventListener( | |
| 2388 'entry-changed', this.onEntryChangedBound_); | |
| 2389 } | |
| 2390 } | |
| 2391 window.closing = true; | |
| 2392 if (this.backgroundPage_) | |
| 2393 this.backgroundPage_.background.tryClose(); | |
| 2394 }; | |
| 2395 | |
| 2396 FileManager.prototype.initiateRename = function() { | |
| 2397 var item = this.currentList_.ensureLeadItemExists(); | |
| 2398 if (!item) | |
| 2399 return; | |
| 2400 var label = item.querySelector('.filename-label'); | |
| 2401 var input = this.renameInput_; | |
| 2402 | |
| 2403 input.value = label.textContent; | |
| 2404 item.setAttribute('renaming', ''); | |
| 2405 label.parentNode.appendChild(input); | |
| 2406 input.focus(); | |
| 2407 var selectionEnd = input.value.lastIndexOf('.'); | |
| 2408 if (selectionEnd == -1) { | |
| 2409 input.select(); | |
| 2410 } else { | |
| 2411 input.selectionStart = 0; | |
| 2412 input.selectionEnd = selectionEnd; | |
| 2413 } | |
| 2414 | |
| 2415 // This has to be set late in the process so we don't handle spurious | |
| 2416 // blur events. | |
| 2417 input.currentEntry = this.currentList_.dataModel.item(item.listIndex); | |
| 2418 this.table_.startBatchUpdates(); | |
| 2419 this.grid_.startBatchUpdates(); | |
| 2420 }; | |
| 2421 | |
| 2422 /** | |
| 2423 * @type {Event} Key event. | |
| 2424 * @private | |
| 2425 */ | |
| 2426 FileManager.prototype.onRenameInputKeyDown_ = function(event) { | |
| 2427 if (!this.isRenamingInProgress()) | |
| 2428 return; | |
| 2429 | |
| 2430 // Do not move selection or lead item in list during rename. | |
| 2431 if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') { | |
| 2432 event.stopPropagation(); | |
| 2433 } | |
| 2434 | |
| 2435 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
| 2436 case 'U+001B': // Escape | |
| 2437 this.cancelRename_(); | |
| 2438 event.preventDefault(); | |
| 2439 break; | |
| 2440 | |
| 2441 case 'Enter': | |
| 2442 this.commitRename_(); | |
| 2443 event.preventDefault(); | |
| 2444 break; | |
| 2445 } | |
| 2446 }; | |
| 2447 | |
| 2448 /** | |
| 2449 * @type {Event} Blur event. | |
| 2450 * @private | |
| 2451 */ | |
| 2452 FileManager.prototype.onRenameInputBlur_ = function(event) { | |
| 2453 if (this.isRenamingInProgress() && !this.renameInput_.validation_) | |
| 2454 this.commitRename_(); | |
| 2455 }; | |
| 2456 | |
| 2457 /** | |
| 2458 * @private | |
| 2459 */ | |
| 2460 FileManager.prototype.commitRename_ = function() { | |
| 2461 var input = this.renameInput_; | |
| 2462 var entry = input.currentEntry; | |
| 2463 var newName = input.value; | |
| 2464 | |
| 2465 if (newName == entry.name) { | |
| 2466 this.cancelRename_(); | |
| 2467 return; | |
| 2468 } | |
| 2469 | |
| 2470 var renamedItemElement = this.findListItemForNode_(this.renameInput_); | |
| 2471 var nameNode = renamedItemElement.querySelector('.filename-label'); | |
| 2472 | |
| 2473 input.validation_ = true; | |
| 2474 var validationDone = function(valid) { | |
| 2475 input.validation_ = false; | |
| 2476 | |
| 2477 if (!valid) { | |
| 2478 // Cancel rename if it fails to restore focus from alert dialog. | |
| 2479 // Otherwise, just cancel the commitment and continue to rename. | |
| 2480 if (this.document_.activeElement != input) | |
| 2481 this.cancelRename_(); | |
| 2482 return; | |
| 2483 } | |
| 2484 | |
| 2485 // Validation succeeded. Do renaming. | |
| 2486 this.renameInput_.currentEntry = null; | |
| 2487 if (this.renameInput_.parentNode) | |
| 2488 this.renameInput_.parentNode.removeChild(this.renameInput_); | |
| 2489 renamedItemElement.setAttribute('renaming', 'provisional'); | |
| 2490 | |
| 2491 // Optimistically apply new name immediately to avoid flickering in | |
| 2492 // case of success. | |
| 2493 nameNode.textContent = newName; | |
| 2494 | |
| 2495 util.rename( | |
| 2496 entry, newName, | |
| 2497 function(newEntry) { | |
| 2498 this.directoryModel_.onRenameEntry(entry, newEntry); | |
| 2499 renamedItemElement.removeAttribute('renaming'); | |
| 2500 this.table_.endBatchUpdates(); | |
| 2501 this.grid_.endBatchUpdates(); | |
| 2502 }.bind(this), | |
| 2503 function(error) { | |
| 2504 // Write back to the old name. | |
| 2505 nameNode.textContent = entry.name; | |
| 2506 renamedItemElement.removeAttribute('renaming'); | |
| 2507 this.table_.endBatchUpdates(); | |
| 2508 this.grid_.endBatchUpdates(); | |
| 2509 | |
| 2510 // Show error dialog. | |
| 2511 var message; | |
| 2512 if (error.name == util.FileError.PATH_EXISTS_ERR || | |
| 2513 error.name == util.FileError.TYPE_MISMATCH_ERR) { | |
| 2514 // Check the existing entry is file or not. | |
| 2515 // 1) If the entry is a file: | |
| 2516 // a) If we get PATH_EXISTS_ERR, a file exists. | |
| 2517 // b) If we get TYPE_MISMATCH_ERR, a directory exists. | |
| 2518 // 2) If the entry is a directory: | |
| 2519 // a) If we get PATH_EXISTS_ERR, a directory exists. | |
| 2520 // b) If we get TYPE_MISMATCH_ERR, a file exists. | |
| 2521 message = strf( | |
| 2522 (entry.isFile && error.name == | |
| 2523 util.FileError.PATH_EXISTS_ERR) || | |
| 2524 (!entry.isFile && error.name == | |
| 2525 util.FileError.TYPE_MISMATCH_ERR) ? | |
| 2526 'FILE_ALREADY_EXISTS' : | |
| 2527 'DIRECTORY_ALREADY_EXISTS', | |
| 2528 newName); | |
| 2529 } else { | |
| 2530 message = strf('ERROR_RENAMING', entry.name, | |
| 2531 util.getFileErrorString(error.name)); | |
| 2532 } | |
| 2533 | |
| 2534 this.alert.show(message); | |
| 2535 }.bind(this)); | |
| 2536 }; | |
| 2537 | |
| 2538 // TODO(haruki): this.getCurrentDirectoryEntry() might not return the actual | |
| 2539 // parent if the directory content is a search result. Fix it to do proper | |
| 2540 // validation. | |
| 2541 this.validateFileName_(this.getCurrentDirectoryEntry(), | |
| 2542 newName, | |
| 2543 validationDone.bind(this)); | |
| 2544 }; | |
| 2545 | |
| 2546 /** | |
| 2547 * @private | |
| 2548 */ | |
| 2549 FileManager.prototype.cancelRename_ = function() { | |
| 2550 this.renameInput_.currentEntry = null; | |
| 2551 | |
| 2552 var item = this.findListItemForNode_(this.renameInput_); | |
| 2553 if (item) | |
| 2554 item.removeAttribute('renaming'); | |
| 2555 | |
| 2556 var parent = this.renameInput_.parentNode; | |
| 2557 if (parent) | |
| 2558 parent.removeChild(this.renameInput_); | |
| 2559 | |
| 2560 this.table_.endBatchUpdates(); | |
| 2561 this.grid_.endBatchUpdates(); | |
| 2562 }; | |
| 2563 | |
| 2564 /** | |
| 2565 * @param {Event} Key event. | |
| 2566 * @private | |
| 2567 */ | |
| 2568 FileManager.prototype.onFilenameInputInput_ = function() { | |
| 2569 this.selectionHandler_.updateOkButton(); | |
| 2570 }; | |
| 2571 | |
| 2572 /** | |
| 2573 * @param {Event} Key event. | |
| 2574 * @private | |
| 2575 */ | |
| 2576 FileManager.prototype.onFilenameInputKeyDown_ = function(event) { | |
| 2577 if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */) | |
| 2578 this.okButton_.click(); | |
| 2579 }; | |
| 2580 | |
| 2581 /** | |
| 2582 * @param {Event} Focus event. | |
| 2583 * @private | |
| 2584 */ | |
| 2585 FileManager.prototype.onFilenameInputFocus_ = function(event) { | |
| 2586 var input = this.filenameInput_; | |
| 2587 | |
| 2588 // On focus we want to select everything but the extension, but | |
| 2589 // Chrome will select-all after the focus event completes. We | |
| 2590 // schedule a timeout to alter the focus after that happens. | |
| 2591 setTimeout(function() { | |
| 2592 var selectionEnd = input.value.lastIndexOf('.'); | |
| 2593 if (selectionEnd == -1) { | |
| 2594 input.select(); | |
| 2595 } else { | |
| 2596 input.selectionStart = 0; | |
| 2597 input.selectionEnd = selectionEnd; | |
| 2598 } | |
| 2599 }, 0); | |
| 2600 }; | |
| 2601 | |
| 2602 /** | |
| 2603 * @private | |
| 2604 */ | |
| 2605 FileManager.prototype.onScanStarted_ = function() { | |
| 2606 if (this.scanInProgress_) { | |
| 2607 this.table_.list.endBatchUpdates(); | |
| 2608 this.grid_.endBatchUpdates(); | |
| 2609 } | |
| 2610 | |
| 2611 if (this.commandHandler) | |
| 2612 this.commandHandler.updateAvailability(); | |
| 2613 this.table_.list.startBatchUpdates(); | |
| 2614 this.grid_.startBatchUpdates(); | |
| 2615 this.scanInProgress_ = true; | |
| 2616 | |
| 2617 this.scanUpdatedAtLeastOnceOrCompleted_ = false; | |
| 2618 if (this.scanCompletedTimer_) { | |
| 2619 clearTimeout(this.scanCompletedTimer_); | |
| 2620 this.scanCompletedTimer_ = null; | |
| 2621 } | |
| 2622 | |
| 2623 if (this.scanUpdatedTimer_) { | |
| 2624 clearTimeout(this.scanUpdatedTimer_); | |
| 2625 this.scanUpdatedTimer_ = null; | |
| 2626 } | |
| 2627 | |
| 2628 if (this.spinner_.hidden) { | |
| 2629 this.cancelSpinnerTimeout_(); | |
| 2630 this.showSpinnerTimeout_ = | |
| 2631 setTimeout(this.showSpinner_.bind(this, true), 500); | |
| 2632 } | |
| 2633 }; | |
| 2634 | |
| 2635 /** | |
| 2636 * @private | |
| 2637 */ | |
| 2638 FileManager.prototype.onScanCompleted_ = function() { | |
| 2639 if (!this.scanInProgress_) { | |
| 2640 console.error('Scan-completed event recieved. But scan is not started.'); | |
| 2641 return; | |
| 2642 } | |
| 2643 | |
| 2644 if (this.commandHandler) | |
| 2645 this.commandHandler.updateAvailability(); | |
| 2646 this.hideSpinnerLater_(); | |
| 2647 | |
| 2648 if (this.scanUpdatedTimer_) { | |
| 2649 clearTimeout(this.scanUpdatedTimer_); | |
| 2650 this.scanUpdatedTimer_ = null; | |
| 2651 } | |
| 2652 | |
| 2653 // To avoid flickering postpone updating the ui by a small amount of time. | |
| 2654 // There is a high chance, that metadata will be received within 50 ms. | |
| 2655 this.scanCompletedTimer_ = setTimeout(function() { | |
| 2656 // Check if batch updates are already finished by onScanUpdated_(). | |
| 2657 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
| 2658 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
| 2659 this.updateMiddleBarVisibility_(); | |
| 2660 } | |
| 2661 | |
| 2662 this.scanInProgress_ = false; | |
| 2663 this.table_.list.endBatchUpdates(); | |
| 2664 this.grid_.endBatchUpdates(); | |
| 2665 this.scanCompletedTimer_ = null; | |
| 2666 }.bind(this), 50); | |
| 2667 }; | |
| 2668 | |
| 2669 /** | |
| 2670 * @private | |
| 2671 */ | |
| 2672 FileManager.prototype.onScanUpdated_ = function() { | |
| 2673 if (!this.scanInProgress_) { | |
| 2674 console.error('Scan-updated event recieved. But scan is not started.'); | |
| 2675 return; | |
| 2676 } | |
| 2677 | |
| 2678 if (this.scanUpdatedTimer_ || this.scanCompletedTimer_) | |
| 2679 return; | |
| 2680 | |
| 2681 // Show contents incrementally by finishing batch updated, but only after | |
| 2682 // 200ms elapsed, to avoid flickering when it is not necessary. | |
| 2683 this.scanUpdatedTimer_ = setTimeout(function() { | |
| 2684 // We need to hide the spinner only once. | |
| 2685 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
| 2686 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
| 2687 this.hideSpinnerLater_(); | |
| 2688 this.updateMiddleBarVisibility_(); | |
| 2689 } | |
| 2690 | |
| 2691 // Update the UI. | |
| 2692 if (this.scanInProgress_) { | |
| 2693 this.table_.list.endBatchUpdates(); | |
| 2694 this.grid_.endBatchUpdates(); | |
| 2695 this.table_.list.startBatchUpdates(); | |
| 2696 this.grid_.startBatchUpdates(); | |
| 2697 } | |
| 2698 this.scanUpdatedTimer_ = null; | |
| 2699 }.bind(this), 200); | |
| 2700 }; | |
| 2701 | |
| 2702 /** | |
| 2703 * @private | |
| 2704 */ | |
| 2705 FileManager.prototype.onScanCancelled_ = function() { | |
| 2706 if (!this.scanInProgress_) { | |
| 2707 console.error('Scan-cancelled event recieved. But scan is not started.'); | |
| 2708 return; | |
| 2709 } | |
| 2710 | |
| 2711 if (this.commandHandler) | |
| 2712 this.commandHandler.updateAvailability(); | |
| 2713 this.hideSpinnerLater_(); | |
| 2714 if (this.scanCompletedTimer_) { | |
| 2715 clearTimeout(this.scanCompletedTimer_); | |
| 2716 this.scanCompletedTimer_ = null; | |
| 2717 } | |
| 2718 if (this.scanUpdatedTimer_) { | |
| 2719 clearTimeout(this.scanUpdatedTimer_); | |
| 2720 this.scanUpdatedTimer_ = null; | |
| 2721 } | |
| 2722 // Finish unfinished batch updates. | |
| 2723 if (!this.scanUpdatedAtLeastOnceOrCompleted_) { | |
| 2724 this.scanUpdatedAtLeastOnceOrCompleted_ = true; | |
| 2725 this.updateMiddleBarVisibility_(); | |
| 2726 } | |
| 2727 | |
| 2728 this.scanInProgress_ = false; | |
| 2729 this.table_.list.endBatchUpdates(); | |
| 2730 this.grid_.endBatchUpdates(); | |
| 2731 }; | |
| 2732 | |
| 2733 /** | |
| 2734 * Handle the 'rescan-completed' from the DirectoryModel. | |
| 2735 * @private | |
| 2736 */ | |
| 2737 FileManager.prototype.onRescanCompleted_ = function() { | |
| 2738 this.selectionHandler_.onFileSelectionChanged(); | |
| 2739 }; | |
| 2740 | |
| 2741 /** | |
| 2742 * @private | |
| 2743 */ | |
| 2744 FileManager.prototype.cancelSpinnerTimeout_ = function() { | |
| 2745 if (this.showSpinnerTimeout_) { | |
| 2746 clearTimeout(this.showSpinnerTimeout_); | |
| 2747 this.showSpinnerTimeout_ = null; | |
| 2748 } | |
| 2749 }; | |
| 2750 | |
| 2751 /** | |
| 2752 * @private | |
| 2753 */ | |
| 2754 FileManager.prototype.hideSpinnerLater_ = function() { | |
| 2755 this.cancelSpinnerTimeout_(); | |
| 2756 this.showSpinner_(false); | |
| 2757 }; | |
| 2758 | |
| 2759 /** | |
| 2760 * @param {boolean} on True to show, false to hide. | |
| 2761 * @private | |
| 2762 */ | |
| 2763 FileManager.prototype.showSpinner_ = function(on) { | |
| 2764 if (on && this.directoryModel_ && this.directoryModel_.isScanning()) | |
| 2765 this.spinner_.hidden = false; | |
| 2766 | |
| 2767 if (!on && (!this.directoryModel_ || | |
| 2768 !this.directoryModel_.isScanning() || | |
| 2769 this.directoryModel_.getFileList().length != 0)) { | |
| 2770 this.spinner_.hidden = true; | |
| 2771 } | |
| 2772 }; | |
| 2773 | |
| 2774 FileManager.prototype.createNewFolder = function() { | |
| 2775 var defaultName = str('DEFAULT_NEW_FOLDER_NAME'); | |
| 2776 | |
| 2777 // Find a name that doesn't exist in the data model. | |
| 2778 var files = this.directoryModel_.getFileList(); | |
| 2779 var hash = {}; | |
| 2780 for (var i = 0; i < files.length; i++) { | |
| 2781 var name = files.item(i).name; | |
| 2782 // Filtering names prevents from conflicts with prototype's names | |
| 2783 // and '__proto__'. | |
| 2784 if (name.substring(0, defaultName.length) == defaultName) | |
| 2785 hash[name] = 1; | |
| 2786 } | |
| 2787 | |
| 2788 var baseName = defaultName; | |
| 2789 var separator = ''; | |
| 2790 var suffix = ''; | |
| 2791 var index = ''; | |
| 2792 | |
| 2793 var advance = function() { | |
| 2794 separator = ' ('; | |
| 2795 suffix = ')'; | |
| 2796 index++; | |
| 2797 }; | |
| 2798 | |
| 2799 var current = function() { | |
| 2800 return baseName + separator + index + suffix; | |
| 2801 }; | |
| 2802 | |
| 2803 // Accessing hasOwnProperty is safe since hash properties filtered. | |
| 2804 while (hash.hasOwnProperty(current())) { | |
| 2805 advance(); | |
| 2806 } | |
| 2807 | |
| 2808 var self = this; | |
| 2809 var list = self.currentList_; | |
| 2810 var tryCreate = function() { | |
| 2811 self.directoryModel_.createDirectory(current(), | |
| 2812 onSuccess, onError); | |
| 2813 }; | |
| 2814 | |
| 2815 var onSuccess = function(entry) { | |
| 2816 metrics.recordUserAction('CreateNewFolder'); | |
| 2817 list.selectedItem = entry; | |
| 2818 self.initiateRename(); | |
| 2819 }; | |
| 2820 | |
| 2821 var onError = function(error) { | |
| 2822 self.alert.show(strf('ERROR_CREATING_FOLDER', current(), | |
| 2823 util.getFileErrorString(error.name))); | |
| 2824 }; | |
| 2825 | |
| 2826 tryCreate(); | |
| 2827 }; | |
| 2828 | |
| 2829 /** | |
| 2830 * @param {Event} event Click event. | |
| 2831 * @private | |
| 2832 */ | |
| 2833 FileManager.prototype.onDetailViewButtonClick_ = function(event) { | |
| 2834 // Stop propagate and hide the menu manually, in order to prevent the focus | |
| 2835 // from being back to the button. (cf. http://crbug.com/248479) | |
| 2836 event.stopPropagation(); | |
| 2837 this.gearButton_.hideMenu(); | |
| 2838 this.gearButton_.blur(); | |
| 2839 this.setListType(FileManager.ListType.DETAIL); | |
| 2840 }; | |
| 2841 | |
| 2842 /** | |
| 2843 * @param {Event} event Click event. | |
| 2844 * @private | |
| 2845 */ | |
| 2846 FileManager.prototype.onThumbnailViewButtonClick_ = function(event) { | |
| 2847 // Stop propagate and hide the menu manually, in order to prevent the focus | |
| 2848 // from being back to the button. (cf. http://crbug.com/248479) | |
| 2849 event.stopPropagation(); | |
| 2850 this.gearButton_.hideMenu(); | |
| 2851 this.gearButton_.blur(); | |
| 2852 this.setListType(FileManager.ListType.THUMBNAIL); | |
| 2853 }; | |
| 2854 | |
| 2855 /** | |
| 2856 * KeyDown event handler for the document. | |
| 2857 * @param {Event} event Key event. | |
| 2858 * @private | |
| 2859 */ | |
| 2860 FileManager.prototype.onKeyDown_ = function(event) { | |
| 2861 if (event.keyCode === 9) // Tab | |
| 2862 this.pressingTab_ = true; | |
| 2863 | |
| 2864 if (event.srcElement === this.renameInput_) { | |
| 2865 // Ignore keydown handler in the rename input box. | |
| 2866 return; | |
| 2867 } | |
| 2868 | |
| 2869 switch (util.getKeyModifiers(event) + event.keyCode) { | |
| 2870 case 'Ctrl-190': // Ctrl-. => Toggle filter files. | |
| 2871 this.fileFilter_.setFilterHidden( | |
| 2872 !this.fileFilter_.isFilterHiddenOn()); | |
| 2873 event.preventDefault(); | |
| 2874 return; | |
| 2875 | |
| 2876 case '27': // Escape => Cancel dialog. | |
| 2877 if (this.dialogType != DialogType.FULL_PAGE) { | |
| 2878 // If there is nothing else for ESC to do, then cancel the dialog. | |
| 2879 event.preventDefault(); | |
| 2880 this.cancelButton_.click(); | |
| 2881 } | |
| 2882 break; | |
| 2883 } | |
| 2884 }; | |
| 2885 | |
| 2886 /** | |
| 2887 * KeyUp event handler for the document. | |
| 2888 * @param {Event} event Key event. | |
| 2889 * @private | |
| 2890 */ | |
| 2891 FileManager.prototype.onKeyUp_ = function(event) { | |
| 2892 if (event.keyCode === 9) // Tab | |
| 2893 this.pressingTab_ = false; | |
| 2894 }; | |
| 2895 | |
| 2896 /** | |
| 2897 * KeyDown event handler for the div#list-container element. | |
| 2898 * @param {Event} event Key event. | |
| 2899 * @private | |
| 2900 */ | |
| 2901 FileManager.prototype.onListKeyDown_ = function(event) { | |
| 2902 if (event.srcElement.tagName == 'INPUT') { | |
| 2903 // Ignore keydown handler in the rename input box. | |
| 2904 return; | |
| 2905 } | |
| 2906 | |
| 2907 switch (util.getKeyModifiers(event) + event.keyCode) { | |
| 2908 case '8': // Backspace => Up one directory. | |
| 2909 event.preventDefault(); | |
| 2910 // TODO(mtomasz): Use Entry.getParent() instead. | |
| 2911 if (!this.getCurrentDirectoryEntry()) | |
| 2912 break; | |
| 2913 var currentEntry = this.getCurrentDirectoryEntry(); | |
| 2914 var locationInfo = this.volumeManager_.getLocationInfo(currentEntry); | |
| 2915 // TODO(mtomasz): There may be a tiny race in here. | |
| 2916 if (locationInfo && !locationInfo.isRootEntry && | |
| 2917 !locationInfo.isSpecialSearchRoot) { | |
| 2918 currentEntry.getParent(function(parentEntry) { | |
| 2919 this.directoryModel_.changeDirectoryEntry(parentEntry); | |
| 2920 }.bind(this), function() { /* Ignore errors. */}); | |
| 2921 } | |
| 2922 break; | |
| 2923 | |
| 2924 case '13': // Enter => Change directory or perform default action. | |
| 2925 // TODO(dgozman): move directory action to dispatchSelectionAction. | |
| 2926 var selection = this.getSelection(); | |
| 2927 if (selection.totalCount == 1 && | |
| 2928 selection.entries[0].isDirectory && | |
| 2929 !DialogType.isFolderDialog(this.dialogType)) { | |
| 2930 event.preventDefault(); | |
| 2931 this.onDirectoryAction_(selection.entries[0]); | |
| 2932 } else if (this.dispatchSelectionAction_()) { | |
| 2933 event.preventDefault(); | |
| 2934 } | |
| 2935 break; | |
| 2936 } | |
| 2937 | |
| 2938 switch (event.keyIdentifier) { | |
| 2939 case 'Home': | |
| 2940 case 'End': | |
| 2941 case 'Up': | |
| 2942 case 'Down': | |
| 2943 case 'Left': | |
| 2944 case 'Right': | |
| 2945 // When navigating with keyboard we hide the distracting mouse hover | |
| 2946 // highlighting until the user moves the mouse again. | |
| 2947 this.setNoHover_(true); | |
| 2948 break; | |
| 2949 } | |
| 2950 }; | |
| 2951 | |
| 2952 /** | |
| 2953 * Suppress/restore hover highlighting in the list container. | |
| 2954 * @param {boolean} on True to temporarity hide hover state. | |
| 2955 * @private | |
| 2956 */ | |
| 2957 FileManager.prototype.setNoHover_ = function(on) { | |
| 2958 if (on) { | |
| 2959 this.listContainer_.classList.add('nohover'); | |
| 2960 } else { | |
| 2961 this.listContainer_.classList.remove('nohover'); | |
| 2962 } | |
| 2963 }; | |
| 2964 | |
| 2965 /** | |
| 2966 * KeyPress event handler for the div#list-container element. | |
| 2967 * @param {Event} event Key event. | |
| 2968 * @private | |
| 2969 */ | |
| 2970 FileManager.prototype.onListKeyPress_ = function(event) { | |
| 2971 if (event.srcElement.tagName == 'INPUT') { | |
| 2972 // Ignore keypress handler in the rename input box. | |
| 2973 return; | |
| 2974 } | |
| 2975 | |
| 2976 if (event.ctrlKey || event.metaKey || event.altKey) | |
| 2977 return; | |
| 2978 | |
| 2979 var now = new Date(); | |
| 2980 var char = String.fromCharCode(event.charCode).toLowerCase(); | |
| 2981 var text = now - this.textSearchState_.date > 1000 ? '' : | |
| 2982 this.textSearchState_.text; | |
| 2983 this.textSearchState_ = {text: text + char, date: now}; | |
| 2984 | |
| 2985 this.doTextSearch_(); | |
| 2986 }; | |
| 2987 | |
| 2988 /** | |
| 2989 * Mousemove event handler for the div#list-container element. | |
| 2990 * @param {Event} event Mouse event. | |
| 2991 * @private | |
| 2992 */ | |
| 2993 FileManager.prototype.onListMouseMove_ = function(event) { | |
| 2994 // The user grabbed the mouse, restore the hover highlighting. | |
| 2995 this.setNoHover_(false); | |
| 2996 }; | |
| 2997 | |
| 2998 /** | |
| 2999 * Performs a 'text search' - selects a first list entry with name | |
| 3000 * starting with entered text (case-insensitive). | |
| 3001 * @private | |
| 3002 */ | |
| 3003 FileManager.prototype.doTextSearch_ = function() { | |
| 3004 var text = this.textSearchState_.text; | |
| 3005 if (!text) | |
| 3006 return; | |
| 3007 | |
| 3008 var dm = this.directoryModel_.getFileList(); | |
| 3009 for (var index = 0; index < dm.length; ++index) { | |
| 3010 var name = dm.item(index).name; | |
| 3011 if (name.substring(0, text.length).toLowerCase() == text) { | |
| 3012 this.currentList_.selectionModel.selectedIndexes = [index]; | |
| 3013 return; | |
| 3014 } | |
| 3015 } | |
| 3016 | |
| 3017 this.textSearchState_.text = ''; | |
| 3018 }; | |
| 3019 | |
| 3020 /** | |
| 3021 * Handle a click of the cancel button. Closes the window. | |
| 3022 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
| 3023 * | |
| 3024 * @param {Event} event The click event. | |
| 3025 * @private | |
| 3026 */ | |
| 3027 FileManager.prototype.onCancel_ = function(event) { | |
| 3028 chrome.fileBrowserPrivate.cancelDialog(); | |
| 3029 window.close(); | |
| 3030 }; | |
| 3031 | |
| 3032 /** | |
| 3033 * Resolves selected file urls returned from an Open dialog. | |
| 3034 * | |
| 3035 * For drive files this involves some special treatment. | |
| 3036 * Starts getting drive files if needed. | |
| 3037 * | |
| 3038 * @param {Array.<string>} fileUrls Drive URLs. | |
| 3039 * @param {function(Array.<string>)} callback To be called with fixed URLs. | |
| 3040 * @private | |
| 3041 */ | |
| 3042 FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) { | |
| 3043 if (this.isOnDrive()) { | |
| 3044 chrome.fileBrowserPrivate.getDriveFiles( | |
| 3045 fileUrls, | |
| 3046 function(localPaths) { | |
| 3047 callback(fileUrls); | |
| 3048 }); | |
| 3049 } else { | |
| 3050 callback(fileUrls); | |
| 3051 } | |
| 3052 }; | |
| 3053 | |
| 3054 /** | |
| 3055 * Closes this modal dialog with some files selected. | |
| 3056 * TODO(jamescook): Make unload handler work automatically, crbug.com/104811 | |
| 3057 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
| 3058 * @private | |
| 3059 */ | |
| 3060 FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) { | |
| 3061 var self = this; | |
| 3062 function callback() { | |
| 3063 window.close(); | |
| 3064 } | |
| 3065 if (selection.multiple) { | |
| 3066 chrome.fileBrowserPrivate.selectFiles( | |
| 3067 selection.urls, this.params_.shouldReturnLocalPath, callback); | |
| 3068 } else { | |
| 3069 var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE); | |
| 3070 chrome.fileBrowserPrivate.selectFile( | |
| 3071 selection.urls[0], selection.filterIndex, forOpening, | |
| 3072 this.params_.shouldReturnLocalPath, callback); | |
| 3073 } | |
| 3074 }; | |
| 3075 | |
| 3076 /** | |
| 3077 * Tries to close this modal dialog with some files selected. | |
| 3078 * Performs preprocessing if needed (e.g. for Drive). | |
| 3079 * @param {Object} selection Contains urls, filterIndex and multiple fields. | |
| 3080 * @private | |
| 3081 */ | |
| 3082 FileManager.prototype.selectFilesAndClose_ = function(selection) { | |
| 3083 if (!this.isOnDrive() || | |
| 3084 this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
| 3085 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
| 3086 return; | |
| 3087 } | |
| 3088 | |
| 3089 var shade = this.document_.createElement('div'); | |
| 3090 shade.className = 'shade'; | |
| 3091 var footer = this.dialogDom_.querySelector('.button-panel'); | |
| 3092 var progress = footer.querySelector('.progress-track'); | |
| 3093 progress.style.width = '0%'; | |
| 3094 var cancelled = false; | |
| 3095 | |
| 3096 var progressMap = {}; | |
| 3097 var filesStarted = 0; | |
| 3098 var filesTotal = selection.urls.length; | |
| 3099 for (var index = 0; index < selection.urls.length; index++) { | |
| 3100 progressMap[selection.urls[index]] = -1; | |
| 3101 } | |
| 3102 var lastPercent = 0; | |
| 3103 var bytesTotal = 0; | |
| 3104 var bytesDone = 0; | |
| 3105 | |
| 3106 var onFileTransfersUpdated = function(statusList) { | |
| 3107 for (var index = 0; index < statusList.length; index++) { | |
| 3108 var status = statusList[index]; | |
| 3109 var escaped = encodeURI(status.fileUrl); | |
| 3110 if (!(escaped in progressMap)) continue; | |
| 3111 if (status.total == -1) continue; | |
| 3112 | |
| 3113 var old = progressMap[escaped]; | |
| 3114 if (old == -1) { | |
| 3115 // -1 means we don't know file size yet. | |
| 3116 bytesTotal += status.total; | |
| 3117 filesStarted++; | |
| 3118 old = 0; | |
| 3119 } | |
| 3120 bytesDone += status.processed - old; | |
| 3121 progressMap[escaped] = status.processed; | |
| 3122 } | |
| 3123 | |
| 3124 var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal; | |
| 3125 // For files we don't have information about, assume the progress is zero. | |
| 3126 percent = percent * filesStarted / filesTotal * 100; | |
| 3127 // Do not decrease the progress. This may happen, if first downloaded | |
| 3128 // file is small, and the second one is large. | |
| 3129 lastPercent = Math.max(lastPercent, percent); | |
| 3130 progress.style.width = lastPercent + '%'; | |
| 3131 }.bind(this); | |
| 3132 | |
| 3133 var setup = function() { | |
| 3134 this.document_.querySelector('.dialog-container').appendChild(shade); | |
| 3135 setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100); | |
| 3136 footer.setAttribute('progress', 'progress'); | |
| 3137 this.cancelButton_.removeEventListener('click', this.onCancelBound_); | |
| 3138 this.cancelButton_.addEventListener('click', onCancel); | |
| 3139 chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener( | |
| 3140 onFileTransfersUpdated); | |
| 3141 }.bind(this); | |
| 3142 | |
| 3143 var cleanup = function() { | |
| 3144 shade.parentNode.removeChild(shade); | |
| 3145 footer.removeAttribute('progress'); | |
| 3146 this.cancelButton_.removeEventListener('click', onCancel); | |
| 3147 this.cancelButton_.addEventListener('click', this.onCancelBound_); | |
| 3148 chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener( | |
| 3149 onFileTransfersUpdated); | |
| 3150 }.bind(this); | |
| 3151 | |
| 3152 var onCancel = function() { | |
| 3153 cancelled = true; | |
| 3154 // According to API cancel may fail, but there is no proper UI to reflect | |
| 3155 // this. So, we just silently assume that everything is cancelled. | |
| 3156 chrome.fileBrowserPrivate.cancelFileTransfers( | |
| 3157 selection.urls, function(response) {}); | |
| 3158 cleanup(); | |
| 3159 }.bind(this); | |
| 3160 | |
| 3161 var onResolved = function(resolvedUrls) { | |
| 3162 if (cancelled) return; | |
| 3163 cleanup(); | |
| 3164 selection.urls = resolvedUrls; | |
| 3165 // Call next method on a timeout, as it's unsafe to | |
| 3166 // close a window from a callback. | |
| 3167 setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0); | |
| 3168 }.bind(this); | |
| 3169 | |
| 3170 var onProperties = function(properties) { | |
| 3171 for (var i = 0; i < properties.length; i++) { | |
| 3172 if (!properties[i] || properties[i].present) { | |
| 3173 // For files already in GCache, we don't get any transfer updates. | |
| 3174 filesTotal--; | |
| 3175 } | |
| 3176 } | |
| 3177 this.resolveSelectResults_(selection.urls, onResolved); | |
| 3178 }.bind(this); | |
| 3179 | |
| 3180 setup(); | |
| 3181 | |
| 3182 // TODO(mtomasz): Use Entry instead of URLs, if possible. | |
| 3183 util.URLsToEntries(selection.urls, function(entries) { | |
| 3184 this.metadataCache_.get(entries, 'drive', onProperties); | |
| 3185 }.bind(this)); | |
| 3186 }; | |
| 3187 | |
| 3188 /** | |
| 3189 * Handle a click of the ok button. | |
| 3190 * | |
| 3191 * The ok button has different UI labels depending on the type of dialog, but | |
| 3192 * in code it's always referred to as 'ok'. | |
| 3193 * | |
| 3194 * @param {Event} event The click event. | |
| 3195 * @private | |
| 3196 */ | |
| 3197 FileManager.prototype.onOk_ = function(event) { | |
| 3198 if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) { | |
| 3199 // Save-as doesn't require a valid selection from the list, since | |
| 3200 // we're going to take the filename from the text input. | |
| 3201 var filename = this.filenameInput_.value; | |
| 3202 if (!filename) | |
| 3203 throw new Error('Missing filename!'); | |
| 3204 | |
| 3205 var directory = this.getCurrentDirectoryEntry(); | |
| 3206 this.validateFileName_(directory, filename, function(isValid) { | |
| 3207 if (!isValid) | |
| 3208 return; | |
| 3209 | |
| 3210 if (util.isFakeEntry(directory)) { | |
| 3211 // Can't save a file into a fake directory. | |
| 3212 return; | |
| 3213 } | |
| 3214 | |
| 3215 var selectFileAndClose = function() { | |
| 3216 // TODO(mtomasz): Clean this up by avoiding constructing a URL | |
| 3217 // via string concatenation. | |
| 3218 var currentDirUrl = directory.toURL(); | |
| 3219 if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/') | |
| 3220 currentDirUrl += '/'; | |
| 3221 this.selectFilesAndClose_({ | |
| 3222 urls: [currentDirUrl + encodeURIComponent(filename)], | |
| 3223 multiple: false, | |
| 3224 filterIndex: this.getSelectedFilterIndex_(filename) | |
| 3225 }); | |
| 3226 }.bind(this); | |
| 3227 | |
| 3228 directory.getFile( | |
| 3229 filename, {create: false}, | |
| 3230 function(entry) { | |
| 3231 // An existing file is found. Show confirmation dialog to | |
| 3232 // overwrite it. If the user select "OK" on the dialog, save it. | |
| 3233 this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename), | |
| 3234 selectFileAndClose); | |
| 3235 }.bind(this), | |
| 3236 function(error) { | |
| 3237 if (error.name == util.FileError.NOT_FOUND_ERR) { | |
| 3238 // The file does not exist, so it should be ok to create a | |
| 3239 // new file. | |
| 3240 selectFileAndClose(); | |
| 3241 return; | |
| 3242 } | |
| 3243 if (error.name == util.FileError.TYPE_MISMATCH_ERR) { | |
| 3244 // An directory is found. | |
| 3245 // Do not allow to overwrite directory. | |
| 3246 this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename)); | |
| 3247 return; | |
| 3248 } | |
| 3249 | |
| 3250 // Unexpected error. | |
| 3251 console.error('File save failed: ' + error.code); | |
| 3252 }.bind(this)); | |
| 3253 }.bind(this)); | |
| 3254 return; | |
| 3255 } | |
| 3256 | |
| 3257 var files = []; | |
| 3258 var selectedIndexes = this.currentList_.selectionModel.selectedIndexes; | |
| 3259 | |
| 3260 if (DialogType.isFolderDialog(this.dialogType) && | |
| 3261 selectedIndexes.length == 0) { | |
| 3262 var url = this.getCurrentDirectoryEntry().toURL(); | |
| 3263 var singleSelection = { | |
| 3264 urls: [url], | |
| 3265 multiple: false, | |
| 3266 filterIndex: this.getSelectedFilterIndex_() | |
| 3267 }; | |
| 3268 this.selectFilesAndClose_(singleSelection); | |
| 3269 return; | |
| 3270 } | |
| 3271 | |
| 3272 // All other dialog types require at least one selected list item. | |
| 3273 // The logic to control whether or not the ok button is enabled should | |
| 3274 // prevent us from ever getting here, but we sanity check to be sure. | |
| 3275 if (!selectedIndexes.length) | |
| 3276 throw new Error('Nothing selected!'); | |
| 3277 | |
| 3278 var dm = this.directoryModel_.getFileList(); | |
| 3279 for (var i = 0; i < selectedIndexes.length; i++) { | |
| 3280 var entry = dm.item(selectedIndexes[i]); | |
| 3281 if (!entry) { | |
| 3282 console.error('Error locating selected file at index: ' + i); | |
| 3283 continue; | |
| 3284 } | |
| 3285 | |
| 3286 files.push(entry.toURL()); | |
| 3287 } | |
| 3288 | |
| 3289 // Multi-file selection has no other restrictions. | |
| 3290 if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) { | |
| 3291 var multipleSelection = { | |
| 3292 urls: files, | |
| 3293 multiple: true | |
| 3294 }; | |
| 3295 this.selectFilesAndClose_(multipleSelection); | |
| 3296 return; | |
| 3297 } | |
| 3298 | |
| 3299 // Everything else must have exactly one. | |
| 3300 if (files.length > 1) | |
| 3301 throw new Error('Too many files selected!'); | |
| 3302 | |
| 3303 var selectedEntry = dm.item(selectedIndexes[0]); | |
| 3304 | |
| 3305 if (DialogType.isFolderDialog(this.dialogType)) { | |
| 3306 if (!selectedEntry.isDirectory) | |
| 3307 throw new Error('Selected entry is not a folder!'); | |
| 3308 } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) { | |
| 3309 if (!selectedEntry.isFile) | |
| 3310 throw new Error('Selected entry is not a file!'); | |
| 3311 } | |
| 3312 | |
| 3313 var singleSelection = { | |
| 3314 urls: [files[0]], | |
| 3315 multiple: false, | |
| 3316 filterIndex: this.getSelectedFilterIndex_() | |
| 3317 }; | |
| 3318 this.selectFilesAndClose_(singleSelection); | |
| 3319 }; | |
| 3320 | |
| 3321 /** | |
| 3322 * Verifies the user entered name for file or folder to be created or | |
| 3323 * renamed to. Name restrictions must correspond to File API restrictions | |
| 3324 * (see DOMFilePath::isValidPath). Curernt WebKit implementation is | |
| 3325 * out of date (spec is | |
| 3326 * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to | |
| 3327 * be fixed. Shows message box if the name is invalid. | |
| 3328 * | |
| 3329 * It also verifies if the name length is in the limit of the filesystem. | |
| 3330 * | |
| 3331 * @param {DirectoryEntry} parentEntry The URL of the parent directory entry. | |
| 3332 * @param {string} name New file or folder name. | |
| 3333 * @param {function} onDone Function to invoke when user closes the | |
| 3334 * warning box or immediatelly if file name is correct. If the name was | |
| 3335 * valid it is passed true, and false otherwise. | |
| 3336 * @private | |
| 3337 */ | |
| 3338 FileManager.prototype.validateFileName_ = function( | |
| 3339 parentEntry, name, onDone) { | |
| 3340 var msg; | |
| 3341 var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name); | |
| 3342 if (testResult) { | |
| 3343 msg = strf('ERROR_INVALID_CHARACTER', testResult[0]); | |
| 3344 } else if (/^\s*$/i.test(name)) { | |
| 3345 msg = str('ERROR_WHITESPACE_NAME'); | |
| 3346 } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) { | |
| 3347 msg = str('ERROR_RESERVED_NAME'); | |
| 3348 } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') { | |
| 3349 msg = str('ERROR_HIDDEN_NAME'); | |
| 3350 } | |
| 3351 | |
| 3352 if (msg) { | |
| 3353 this.alert.show(msg, function() { | |
| 3354 onDone(false); | |
| 3355 }); | |
| 3356 return; | |
| 3357 } | |
| 3358 | |
| 3359 var self = this; | |
| 3360 chrome.fileBrowserPrivate.validatePathNameLength( | |
| 3361 parentEntry.toURL(), name, function(valid) { | |
| 3362 if (!valid) { | |
| 3363 self.alert.show(str('ERROR_LONG_NAME'), | |
| 3364 function() { onDone(false); }); | |
| 3365 } else { | |
| 3366 onDone(true); | |
| 3367 } | |
| 3368 }); | |
| 3369 }; | |
| 3370 | |
| 3371 /** | |
| 3372 * Handler invoked on preference setting in drive context menu. | |
| 3373 * | |
| 3374 * @param {string} pref The preference to alter. | |
| 3375 * @param {boolean} inverted Invert the value if true. | |
| 3376 * @param {Event} event The click event. | |
| 3377 * @private | |
| 3378 */ | |
| 3379 FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) { | |
| 3380 var newValue = !event.target.hasAttribute('checked'); | |
| 3381 if (newValue) | |
| 3382 event.target.setAttribute('checked', 'checked'); | |
| 3383 else | |
| 3384 event.target.removeAttribute('checked'); | |
| 3385 | |
| 3386 var changeInfo = {}; | |
| 3387 changeInfo[pref] = inverted ? !newValue : newValue; | |
| 3388 chrome.fileBrowserPrivate.setPreferences(changeInfo); | |
| 3389 }; | |
| 3390 | |
| 3391 /** | |
| 3392 * Invoked when the search box is changed. | |
| 3393 * | |
| 3394 * @param {Event} event The changed event. | |
| 3395 * @private | |
| 3396 */ | |
| 3397 FileManager.prototype.onSearchBoxUpdate_ = function(event) { | |
| 3398 var searchString = this.searchBox_.value; | |
| 3399 | |
| 3400 if (this.isOnDrive()) { | |
| 3401 // When the search text is changed, finishes the search and showes back | |
| 3402 // the last directory by passing an empty string to | |
| 3403 // {@code DirectoryModel.search()}. | |
| 3404 if (this.directoryModel_.isSearching() && | |
| 3405 this.lastSearchQuery_ != searchString) { | |
| 3406 this.doSearch(''); | |
| 3407 } | |
| 3408 | |
| 3409 // On drive, incremental search is not invoked since we have an auto- | |
| 3410 // complete suggestion instead. | |
| 3411 return; | |
| 3412 } | |
| 3413 | |
| 3414 this.search_(searchString); | |
| 3415 }; | |
| 3416 | |
| 3417 /** | |
| 3418 * Handle the search clear button click. | |
| 3419 * @private | |
| 3420 */ | |
| 3421 FileManager.prototype.onSearchClearButtonClick_ = function() { | |
| 3422 this.ui_.searchBox.clear(); | |
| 3423 this.onSearchBoxUpdate_(); | |
| 3424 }; | |
| 3425 | |
| 3426 /** | |
| 3427 * Search files and update the list with the search result. | |
| 3428 * | |
| 3429 * @param {string} searchString String to be searched with. | |
| 3430 * @private | |
| 3431 */ | |
| 3432 FileManager.prototype.search_ = function(searchString) { | |
| 3433 var noResultsDiv = this.document_.getElementById('no-search-results'); | |
| 3434 | |
| 3435 var reportEmptySearchResults = function() { | |
| 3436 if (this.directoryModel_.getFileList().length === 0) { | |
| 3437 // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags, | |
| 3438 // hence we escapes |searchString| here. | |
| 3439 var html = strf('SEARCH_NO_MATCHING_FILES_HTML', | |
| 3440 util.htmlEscape(searchString)); | |
| 3441 noResultsDiv.innerHTML = html; | |
| 3442 noResultsDiv.setAttribute('show', 'true'); | |
| 3443 } else { | |
| 3444 noResultsDiv.removeAttribute('show'); | |
| 3445 } | |
| 3446 }; | |
| 3447 | |
| 3448 var hideNoResultsDiv = function() { | |
| 3449 noResultsDiv.removeAttribute('show'); | |
| 3450 }; | |
| 3451 | |
| 3452 this.doSearch(searchString, | |
| 3453 reportEmptySearchResults.bind(this), | |
| 3454 hideNoResultsDiv.bind(this)); | |
| 3455 }; | |
| 3456 | |
| 3457 /** | |
| 3458 * Performs search and displays results. | |
| 3459 * | |
| 3460 * @param {string} query Query that will be searched for. | |
| 3461 * @param {function()=} opt_onSearchRescan Function that will be called when | |
| 3462 * the search directory is rescanned (i.e. search results are displayed). | |
| 3463 * @param {function()=} opt_onClearSearch Function to be called when search | |
| 3464 * state gets cleared. | |
| 3465 */ | |
| 3466 FileManager.prototype.doSearch = function( | |
| 3467 searchString, opt_onSearchRescan, opt_onClearSearch) { | |
| 3468 var onSearchRescan = opt_onSearchRescan || function() {}; | |
| 3469 var onClearSearch = opt_onClearSearch || function() {}; | |
| 3470 | |
| 3471 this.lastSearchQuery_ = searchString; | |
| 3472 this.directoryModel_.search(searchString, onSearchRescan, onClearSearch); | |
| 3473 }; | |
| 3474 | |
| 3475 /** | |
| 3476 * Requests autocomplete suggestions for files on Drive. | |
| 3477 * Once the suggestions are returned, the autocomplete popup will show up. | |
| 3478 * | |
| 3479 * @param {string} query The text to autocomplete from. | |
| 3480 * @private | |
| 3481 */ | |
| 3482 FileManager.prototype.requestAutocompleteSuggestions_ = function(query) { | |
| 3483 query = query.trimLeft(); | |
| 3484 | |
| 3485 // Only Drive supports auto-compelete | |
| 3486 if (!this.isOnDrive()) | |
| 3487 return; | |
| 3488 | |
| 3489 // Remember the most recent query. If there is an other request in progress, | |
| 3490 // then it's result will be discarded and it will call a new request for | |
| 3491 // this query. | |
| 3492 this.lastAutocompleteQuery_ = query; | |
| 3493 if (this.autocompleteSuggestionsBusy_) | |
| 3494 return; | |
| 3495 | |
| 3496 // The autocomplete list should be resized and repositioned here as the | |
| 3497 // search box is resized when it's focused. | |
| 3498 this.autocompleteList_.syncWidthAndPositionToInput(); | |
| 3499 | |
| 3500 if (!query) { | |
| 3501 this.autocompleteList_.suggestions = []; | |
| 3502 return; | |
| 3503 } | |
| 3504 | |
| 3505 var headerItem = {isHeaderItem: true, searchQuery: query}; | |
| 3506 if (!this.autocompleteList_.dataModel || | |
| 3507 this.autocompleteList_.dataModel.length == 0) | |
| 3508 this.autocompleteList_.suggestions = [headerItem]; | |
| 3509 else | |
| 3510 // Updates only the head item to prevent a flickering on typing. | |
| 3511 this.autocompleteList_.dataModel.splice(0, 1, headerItem); | |
| 3512 | |
| 3513 this.autocompleteSuggestionsBusy_ = true; | |
| 3514 | |
| 3515 var searchParams = { | |
| 3516 'query': query, | |
| 3517 'types': 'ALL', | |
| 3518 'maxResults': 4 | |
| 3519 }; | |
| 3520 chrome.fileBrowserPrivate.searchDriveMetadata( | |
| 3521 searchParams, | |
| 3522 function(suggestions) { | |
| 3523 this.autocompleteSuggestionsBusy_ = false; | |
| 3524 | |
| 3525 // Discard results for previous requests and fire a new search | |
| 3526 // for the most recent query. | |
| 3527 if (query != this.lastAutocompleteQuery_) { | |
| 3528 this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_); | |
| 3529 return; | |
| 3530 } | |
| 3531 | |
| 3532 // Keeps the items in the suggestion list. | |
| 3533 this.autocompleteList_.suggestions = [headerItem].concat(suggestions); | |
| 3534 }.bind(this)); | |
| 3535 }; | |
| 3536 | |
| 3537 /** | |
| 3538 * Opens the currently selected suggestion item. | |
| 3539 * @private | |
| 3540 */ | |
| 3541 FileManager.prototype.openAutocompleteSuggestion_ = function() { | |
| 3542 var selectedItem = this.autocompleteList_.selectedItem; | |
| 3543 | |
| 3544 // If the entry is the search item or no entry is selected, just change to | |
| 3545 // the search result. | |
| 3546 if (!selectedItem || selectedItem.isHeaderItem) { | |
| 3547 var query = selectedItem ? | |
| 3548 selectedItem.searchQuery : this.searchBox_.value; | |
| 3549 this.search_(query); | |
| 3550 return; | |
| 3551 } | |
| 3552 | |
| 3553 var entry = selectedItem.entry; | |
| 3554 // If the entry is a directory, just change the directory. | |
| 3555 if (entry.isDirectory) { | |
| 3556 this.onDirectoryAction_(entry); | |
| 3557 return; | |
| 3558 } | |
| 3559 | |
| 3560 var entries = [entry]; | |
| 3561 var self = this; | |
| 3562 | |
| 3563 // To open a file, first get the mime type. | |
| 3564 this.metadataCache_.get(entries, 'drive', function(props) { | |
| 3565 var mimeType = props[0].contentMimeType || ''; | |
| 3566 var mimeTypes = [mimeType]; | |
| 3567 var openIt = function() { | |
| 3568 if (self.dialogType == DialogType.FULL_PAGE) { | |
| 3569 var tasks = new FileTasks(self); | |
| 3570 tasks.init(entries, mimeTypes); | |
| 3571 tasks.executeDefault(); | |
| 3572 } else { | |
| 3573 self.onOk_(); | |
| 3574 } | |
| 3575 }; | |
| 3576 | |
| 3577 // Change the current directory to the directory that contains the | |
| 3578 // selected file. Note that this is necessary for an image or a video, | |
| 3579 // which should be opened in the gallery mode, as the gallery mode | |
| 3580 // requires the entry to be in the current directory model. For | |
| 3581 // consistency, the current directory is always changed regardless of | |
| 3582 // the file type. | |
| 3583 entry.getParent(function(parentEntry) { | |
| 3584 var onDirectoryChanged = function(event) { | |
| 3585 self.directoryModel_.removeEventListener('scan-completed', | |
| 3586 onDirectoryChanged); | |
| 3587 self.directoryModel_.selectEntry(entry); | |
| 3588 openIt(); | |
| 3589 }; | |
| 3590 // changeDirectoryEntry() returns immediately. We should wait until the | |
| 3591 // directory scan is complete. | |
| 3592 self.directoryModel_.addEventListener('scan-completed', | |
| 3593 onDirectoryChanged); | |
| 3594 self.directoryModel_.changeDirectoryEntry( | |
| 3595 parentEntry, | |
| 3596 function() { | |
| 3597 // Remove the listner if the change directory failed. | |
| 3598 self.directoryModel_.removeEventListener('scan-completed', | |
| 3599 onDirectoryChanged); | |
| 3600 }); | |
| 3601 }); | |
| 3602 }); | |
| 3603 }; | |
| 3604 | |
| 3605 FileManager.prototype.decorateSplitter = function(splitterElement) { | |
| 3606 var self = this; | |
| 3607 | |
| 3608 var Splitter = cr.ui.Splitter; | |
| 3609 | |
| 3610 var customSplitter = cr.ui.define('div'); | |
| 3611 | |
| 3612 customSplitter.prototype = { | |
| 3613 __proto__: Splitter.prototype, | |
| 3614 | |
| 3615 handleSplitterDragStart: function(e) { | |
| 3616 Splitter.prototype.handleSplitterDragStart.apply(this, arguments); | |
| 3617 this.ownerDocument.documentElement.classList.add('col-resize'); | |
| 3618 }, | |
| 3619 | |
| 3620 handleSplitterDragMove: function(deltaX) { | |
| 3621 Splitter.prototype.handleSplitterDragMove.apply(this, arguments); | |
| 3622 self.onResize_(); | |
| 3623 }, | |
| 3624 | |
| 3625 handleSplitterDragEnd: function(e) { | |
| 3626 Splitter.prototype.handleSplitterDragEnd.apply(this, arguments); | |
| 3627 this.ownerDocument.documentElement.classList.remove('col-resize'); | |
| 3628 } | |
| 3629 }; | |
| 3630 | |
| 3631 customSplitter.decorate(splitterElement); | |
| 3632 }; | |
| 3633 | |
| 3634 /** | |
| 3635 * Updates default action menu item to match passed taskItem (icon, | |
| 3636 * label and action). | |
| 3637 * | |
| 3638 * @param {Object} defaultItem - taskItem to match. | |
| 3639 * @param {boolean} isMultiple - if multiple tasks available. | |
| 3640 */ | |
| 3641 FileManager.prototype.updateContextMenuActionItems = function(defaultItem, | |
| 3642 isMultiple) { | |
| 3643 if (defaultItem) { | |
| 3644 if (defaultItem.iconType) { | |
| 3645 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
| 3646 this.defaultActionMenuItem_.setAttribute('file-type-icon', | |
| 3647 defaultItem.iconType); | |
| 3648 } else if (defaultItem.iconUrl) { | |
| 3649 this.defaultActionMenuItem_.style.backgroundImage = | |
| 3650 'url(' + defaultItem.iconUrl + ')'; | |
| 3651 } else { | |
| 3652 this.defaultActionMenuItem_.style.backgroundImage = ''; | |
| 3653 } | |
| 3654 | |
| 3655 this.defaultActionMenuItem_.label = defaultItem.title; | |
| 3656 this.defaultActionMenuItem_.disabled = !!defaultItem.disabled; | |
| 3657 this.defaultActionMenuItem_.taskId = defaultItem.taskId; | |
| 3658 } | |
| 3659 | |
| 3660 var defaultActionSeparator = | |
| 3661 this.dialogDom_.querySelector('#default-action-separator'); | |
| 3662 | |
| 3663 this.openWithCommand_.canExecuteChange(); | |
| 3664 this.openWithCommand_.setHidden(!(defaultItem && isMultiple)); | |
| 3665 this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled; | |
| 3666 | |
| 3667 this.defaultActionMenuItem_.hidden = !defaultItem; | |
| 3668 defaultActionSeparator.hidden = !defaultItem; | |
| 3669 }; | |
| 3670 | |
| 3671 /** | |
| 3672 * @return {FileSelection} Selection object. | |
| 3673 */ | |
| 3674 FileManager.prototype.getSelection = function() { | |
| 3675 return this.selectionHandler_.selection; | |
| 3676 }; | |
| 3677 | |
| 3678 /** | |
| 3679 * @return {ArrayDataModel} File list. | |
| 3680 */ | |
| 3681 FileManager.prototype.getFileList = function() { | |
| 3682 return this.directoryModel_.getFileList(); | |
| 3683 }; | |
| 3684 | |
| 3685 /** | |
| 3686 * @return {cr.ui.List} Current list object. | |
| 3687 */ | |
| 3688 FileManager.prototype.getCurrentList = function() { | |
| 3689 return this.currentList_; | |
| 3690 }; | |
| 3691 | |
| 3692 /** | |
| 3693 * Retrieve the preferences of the files.app. This method caches the result | |
| 3694 * and returns it unless opt_update is true. | |
| 3695 * @param {function(Object.<string, *>)} callback Callback to get the | |
| 3696 * preference. | |
| 3697 * @param {boolean=} opt_update If is's true, don't use the cache and | |
| 3698 * retrieve latest preference. Default is false. | |
| 3699 * @private | |
| 3700 */ | |
| 3701 FileManager.prototype.getPreferences_ = function(callback, opt_update) { | |
| 3702 if (!opt_update && this.preferences_ !== undefined) { | |
| 3703 callback(this.preferences_); | |
| 3704 return; | |
| 3705 } | |
| 3706 | |
| 3707 chrome.fileBrowserPrivate.getPreferences(function(prefs) { | |
| 3708 this.preferences_ = prefs; | |
| 3709 callback(prefs); | |
| 3710 }.bind(this)); | |
| 3711 }; | |
| 3712 })(); | |
| OLD | NEW |