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 7var CommandUtil = {}; 8 9/** 10 * Extracts path on which command event was dispatched. 11 * 12 * @param {DirectoryTree|DirectoryItem|NavigationList|HTMLLIElement|cr.ui.List} 13 * element Directory to extract a path from. 14 * @return {?string} Path of the found node. 15 */ 16CommandUtil.getCommandPath = function(element) { 17 if (element instanceof NavigationList) { 18 // element is a NavigationList. 19 return element.selectedItem; 20 } else if (element instanceof NavigationListItem) { 21 // element is a subitem of NavigationList. 22 var navigationList = element.parentElement; 23 var index = navigationList.getIndexOfListItem(element); 24 return (index != -1) ? navigationList.dataModel.item(index) : null; 25 } else if (element instanceof DirectoryTree) { 26 // element is a DirectoryTree. 27 var item = element.selectedItem; 28 return item && item.fullPath; 29 } else if (element instanceof DirectoryItem) { 30 // element is a sub item in DirectoryTree. 31 32 // DirectoryItem.fullPath is set on initialization, but entry is lazily. 33 // We may use fullPath just in case that the entry has not been set yet. 34 return element.entry && element.entry.fullPath || 35 element.fullPath; 36 } else if (cr.ui.List) { 37 // element is a normal List (eg. the file list on the right panel). 38 var entry = element.selectedItem; 39 return entry && entry.fullPath; 40 } else { 41 console.warn('Unsupported element'); 42 return null; 43 } 44}; 45 46/** 47 * @param {NavigationList} navigationList navigation list to extract root node. 48 * @return {?RootType} Type of the found root. 49 */ 50CommandUtil.getCommandRootType = function(navigationList) { 51 var root = CommandUtil.getCommandPath(navigationList); 52 return root && PathUtil.isRootPath(root) && PathUtil.getRootType(root); 53}; 54 55/** 56 * Checks if command can be executed on drive. 57 * @param {Event} event Command event to mark. 58 * @param {FileManager} fileManager FileManager to use. 59 */ 60CommandUtil.canExecuteEnabledOnDriveOnly = function(event, fileManager) { 61 event.canExecute = fileManager.isOnDrive(); 62}; 63 64/** 65 * Checks if command should be visible on drive. 66 * @param {Event} event Command event to mark. 67 * @param {FileManager} fileManager FileManager to use. 68 */ 69CommandUtil.canExecuteVisibleOnDriveOnly = function(event, fileManager) { 70 event.canExecute = fileManager.isOnDrive(); 71 event.command.setHidden(!fileManager.isOnDrive()); 72}; 73 74/** 75 * Checks if command should be visible on drive with pressing ctrl key. 76 * @param {Event} event Command event to mark. 77 * @param {FileManager} fileManager FileManager to use. 78 */ 79CommandUtil.canExecuteVisibleOnDriveWithCtrlKeyOnly = 80 function(event, fileManager) { 81 event.canExecute = fileManager.isOnDrive() && fileManager.isCtrlKeyPressed(); 82 event.command.setHidden(!event.canExecute); 83}; 84 85/** 86 * Sets as the command as always enabled. 87 * @param {Event} event Command event to mark. 88 */ 89CommandUtil.canExecuteAlways = function(event) { 90 event.canExecute = true; 91}; 92 93/** 94 * Returns a single selected/passed entry or null. 95 * @param {Event} event Command event. 96 * @param {FileManager} fileManager FileManager to use. 97 * @return {FileEntry} The entry or null. 98 */ 99CommandUtil.getSingleEntry = function(event, fileManager) { 100 if (event.target.entry) { 101 return event.target.entry; 102 } 103 var selection = fileManager.getSelection(); 104 if (selection.totalCount == 1) { 105 return selection.entries[0]; 106 } 107 return null; 108}; 109 110/** 111 * Registers handler on specific command on specific node. 112 * @param {Node} node Node to register command handler on. 113 * @param {string} commandId Command id to respond to. 114 * @param {{execute:function, canExecute:function}} handler Handler to use. 115 * @param {...Object} var_args Additional arguments to pass to handler. 116 */ 117CommandUtil.registerCommand = function(node, commandId, handler, var_args) { 118 var args = Array.prototype.slice.call(arguments, 3); 119 120 node.addEventListener('command', function(event) { 121 if (event.command.id == commandId) { 122 handler.execute.apply(handler, [event].concat(args)); 123 event.cancelBubble = true; 124 } 125 }); 126 127 node.addEventListener('canExecute', function(event) { 128 if (event.command.id == commandId) 129 handler.canExecute.apply(handler, [event].concat(args)); 130 }); 131}; 132 133/** 134 * Sets Commands.defaultCommand for the commandId and prevents handling 135 * the keydown events for this command. Not doing that breaks relationship 136 * of original keyboard event and the command. WebKit would handle it 137 * differently in some cases. 138 * @param {Node} node to register command handler on. 139 * @param {string} commandId Command id to respond to. 140 */ 141CommandUtil.forceDefaultHandler = function(node, commandId) { 142 var doc = node.ownerDocument; 143 var command = doc.querySelector('command[id="' + commandId + '"]'); 144 node.addEventListener('keydown', function(e) { 145 if (command.matchesEvent(e)) { 146 // Prevent cr.ui.CommandManager of handling it and leave it 147 // for the default handler. 148 e.stopPropagation(); 149 } 150 }); 151 CommandUtil.registerCommand(node, commandId, Commands.defaultCommand, doc); 152}; 153 154var Commands = {}; 155 156/** 157 * Forwards all command events to standard document handlers. 158 */ 159Commands.defaultCommand = { 160 execute: function(event, document) { 161 document.execCommand(event.command.id); 162 }, 163 canExecute: function(event, document) { 164 event.canExecute = document.queryCommandEnabled(event.command.id); 165 } 166}; 167 168/** 169 * Unmounts external drive. 170 */ 171Commands.unmountCommand = { 172 /** 173 * @param {Event} event Command event. 174 * @param {FileManager} fileManager The file manager instance. 175 */ 176 execute: function(event, fileManager) { 177 var root = CommandUtil.getCommandPath(event.target); 178 if (root) 179 fileManager.unmountVolume(PathUtil.getRootPath(root)); 180 }, 181 /** 182 * @param {Event} event Command event. 183 */ 184 canExecute: function(event) { 185 var rootType = CommandUtil.getCommandRootType(event.target); 186 187 event.canExecute = (rootType == RootType.ARCHIVE || 188 rootType == RootType.REMOVABLE); 189 event.command.setHidden(!event.canExecute); 190 event.command.label = rootType == RootType.ARCHIVE ? 191 str('CLOSE_ARCHIVE_BUTTON_LABEL') : 192 str('UNMOUNT_DEVICE_BUTTON_LABEL'); 193 } 194}; 195 196/** 197 * Formats external drive. 198 */ 199Commands.formatCommand = { 200 /** 201 * @param {Event} event Command event. 202 * @param {FileManager} fileManager The file manager instance. 203 */ 204 execute: function(event, fileManager) { 205 var root = CommandUtil.getCommandPath(event.target); 206 207 if (root) { 208 var url = util.makeFilesystemUrl(PathUtil.getRootPath(root)); 209 fileManager.confirm.show( 210 loadTimeData.getString('FORMATTING_WARNING'), 211 chrome.fileBrowserPrivate.formatDevice.bind(null, url)); 212 } 213 }, 214 /** 215 * @param {Event} event Command event. 216 * @param {FileManager} fileManager The file manager instance. 217 * @param {DirectoryModel} directoryModel The directory model instance. 218 */ 219 canExecute: function(event, fileManager, directoryModel) { 220 var root = CommandUtil.getCommandPath(event.target); 221 var removable = root && 222 PathUtil.getRootType(root) == RootType.REMOVABLE; 223 var isReadOnly = root && directoryModel.isPathReadOnly(root); 224 event.canExecute = removable && !isReadOnly; 225 event.command.setHidden(!removable); 226 } 227}; 228 229/** 230 * Imports photos from external drive 231 */ 232Commands.importCommand = { 233 /** 234 * @param {Event} event Command event. 235 * @param {NavigationList} navigationList Target navigation list. 236 */ 237 execute: function(event, navigationList) { 238 var root = CommandUtil.getCommandPath(navigationList); 239 if (!root) 240 return; 241 242 // TODO(mtomasz): Implement launching Photo Importer. 243 }, 244 /** 245 * @param {Event} event Command event. 246 * @param {NavigationList} navigationList Target navigation list. 247 */ 248 canExecute: function(event, navigationList) { 249 var rootType = CommandUtil.getCommandRootType(navigationList); 250 event.canExecute = (rootType != RootType.DRIVE); 251 } 252}; 253 254/** 255 * Initiates new folder creation. 256 */ 257Commands.newFolderCommand = { 258 execute: function(event, fileManager) { 259 fileManager.createNewFolder(); 260 }, 261 canExecute: function(event, fileManager, directoryModel) { 262 event.canExecute = !fileManager.isOnReadonlyDirectory() && 263 !fileManager.isRenamingInProgress() && 264 !directoryModel.isSearching() && 265 !directoryModel.isScanning(); 266 } 267}; 268 269/** 270 * Initiates new window creation. 271 */ 272Commands.newWindowCommand = { 273 execute: function(event, fileManager, directoryModel) { 274 chrome.runtime.getBackgroundPage(function(background) { 275 var appState = { 276 defaultPath: directoryModel.getCurrentDirPath() 277 }; 278 background.launchFileManager(appState); 279 }); 280 }, 281 canExecute: function(event, fileManager) { 282 event.canExecute = (fileManager.dialogType == DialogType.FULL_PAGE); 283 } 284}; 285 286/** 287 * Changed the default app handling inserted media. 288 */ 289Commands.changeDefaultAppCommand = { 290 execute: function(event, fileManager) { 291 fileManager.showChangeDefaultAppPicker(); 292 }, 293 canExecute: CommandUtil.canExecuteAlways 294}; 295 296/** 297 * Deletes selected files. 298 */ 299Commands.deleteFileCommand = { 300 execute: function(event, fileManager) { 301 fileManager.deleteSelection(); 302 }, 303 canExecute: function(event, fileManager) { 304 var selection = fileManager.getSelection(); 305 event.canExecute = !fileManager.isOnReadonlyDirectory() && 306 selection && 307 selection.totalCount > 0; 308 } 309}; 310 311/** 312 * Pastes files from clipboard. 313 */ 314Commands.pasteFileCommand = { 315 execute: Commands.defaultCommand.execute, 316 canExecute: function(event, document, fileTransferController) { 317 event.canExecute = (fileTransferController && 318 fileTransferController.queryPasteCommandEnabled()); 319 } 320}; 321 322/** 323 * Initiates file renaming. 324 */ 325Commands.renameFileCommand = { 326 execute: function(event, fileManager) { 327 fileManager.initiateRename(); 328 }, 329 canExecute: function(event, fileManager) { 330 var selection = fileManager.getSelection(); 331 event.canExecute = 332 !fileManager.isRenamingInProgress() && 333 !fileManager.isOnReadonlyDirectory() && 334 selection && 335 selection.totalCount == 1; 336 } 337}; 338 339/** 340 * Opens drive help. 341 */ 342Commands.volumeHelpCommand = { 343 execute: function() { 344 if (fileManager.isOnDrive()) 345 chrome.windows.create({url: FileManager.GOOGLE_DRIVE_HELP}); 346 else 347 chrome.windows.create({url: FileManager.FILES_APP_HELP}); 348 }, 349 canExecute: CommandUtil.canExecuteAlways 350}; 351 352/** 353 * Opens drive buy-more-space url. 354 */ 355Commands.driveBuySpaceCommand = { 356 execute: function() { 357 chrome.windows.create({url: FileManager.GOOGLE_DRIVE_BUY_STORAGE}); 358 }, 359 canExecute: CommandUtil.canExecuteVisibleOnDriveOnly 360}; 361 362/** 363 * Clears drive cache. 364 */ 365Commands.driveClearCacheCommand = { 366 execute: function() { 367 chrome.fileBrowserPrivate.clearDriveCache(); 368 }, 369 canExecute: CommandUtil.canExecuteVisibleOnDriveWithCtrlKeyOnly 370}; 371 372/** 373 * Opens drive.google.com. 374 */ 375Commands.driveGoToDriveCommand = { 376 execute: function() { 377 chrome.windows.create({url: FileManager.GOOGLE_DRIVE_ROOT}); 378 }, 379 canExecute: CommandUtil.canExecuteVisibleOnDriveOnly 380}; 381 382/** 383 * Displays open with dialog for current selection. 384 */ 385Commands.openWithCommand = { 386 execute: function(event, fileManager) { 387 var tasks = fileManager.getSelection().tasks; 388 if (tasks) { 389 tasks.showTaskPicker(fileManager.defaultTaskPicker, 390 str('OPEN_WITH_BUTTON_LABEL'), 391 null, 392 function(task) { 393 tasks.execute(task.taskId); 394 }); 395 } 396 }, 397 canExecute: function(event, fileManager) { 398 var tasks = fileManager.getSelection().tasks; 399 event.canExecute = tasks && tasks.size() > 1; 400 } 401}; 402 403/** 404 * Focuses search input box. 405 */ 406Commands.searchCommand = { 407 execute: function(event, fileManager, element) { 408 element.focus(); 409 element.select(); 410 }, 411 canExecute: function(event, fileManager) { 412 event.canExecute = !fileManager.isRenamingInProgress(); 413 } 414}; 415 416/** 417 * Activates the n-th volume. 418 */ 419Commands.volumeSwitchCommand = { 420 execute: function(event, navigationList, index) { 421 navigationList.selectByIndex(index - 1); 422 }, 423 canExecute: function(event, navigationList, index) { 424 event.canExecute = index > 0 && index <= navigationList.dataModel.length; 425 } 426}; 427 428/** 429 * Flips 'available offline' flag on the file. 430 */ 431Commands.togglePinnedCommand = { 432 execute: function(event, fileManager) { 433 var pin = !event.command.checked; 434 event.command.checked = pin; 435 var entries = Commands.togglePinnedCommand.getTargetEntries_(); 436 var currentEntry; 437 var error = false; 438 var steps = { 439 // Pick an entry and pin it. 440 start: function() { 441 // Check if all the entries are pinned or not. 442 if (entries.length == 0) 443 return; 444 currentEntry = entries.shift(); 445 chrome.fileBrowserPrivate.pinDriveFile( 446 currentEntry.toURL(), 447 pin, 448 steps.entryPinned); 449 }, 450 451 // Check the result of pinning 452 entryPinned: function() { 453 // Convert to boolean. 454 error = !!chrome.runtime.lastError; 455 if (error && pin) { 456 fileManager.metadataCache_.get( 457 currentEntry, 'filesystem', steps.showError); 458 } 459 fileManager.metadataCache_.clear(currentEntry, 'drive'); 460 fileManager.metadataCache_.get( 461 currentEntry, 'drive', steps.updateUI.bind(this)); 462 }, 463 464 // Update the user interface accoding to the cache state. 465 updateUI: function(drive) { 466 fileManager.updateMetadataInUI_( 467 'drive', [currentEntry.toURL()], [drive]); 468 if (!error) 469 steps.start(); 470 }, 471 472 // Show the error 473 showError: function(filesystem) { 474 fileManager.alert.showHtml(str('DRIVE_OUT_OF_SPACE_HEADER'), 475 strf('DRIVE_OUT_OF_SPACE_MESSAGE', 476 unescape(currentEntry.name), 477 util.bytesToString(filesystem.size))); 478 } 479 }; 480 steps.start(); 481 }, 482 483 canExecute: function(event, fileManager) { 484 var entries = Commands.togglePinnedCommand.getTargetEntries_(); 485 var checked = true; 486 for (var i = 0; i < entries.length; i++) { 487 checked = checked && entries[i].pinned; 488 } 489 if (entries.length > 0) { 490 event.canExecute = true; 491 event.command.setHidden(false); 492 event.command.checked = checked; 493 } else { 494 event.canExecute = false; 495 event.command.setHidden(true); 496 } 497 }, 498 499 /** 500 * Obtains target entries from the selection. 501 * If directories are included in the selection, it just returns an empty 502 * array to avoid confusing because pinning directory is not supported 503 * currently. 504 * 505 * @return {Array.<Entry>} Target entries. 506 * @private 507 */ 508 getTargetEntries_: function() { 509 var hasDirectory = false; 510 var results = fileManager.getSelection().entries.filter(function(entry) { 511 hasDirectory = hasDirectory || entry.isDirectory; 512 if (!entry || hasDirectory) 513 return false; 514 var metadata = fileManager.metadataCache_.getCached(entry, 'drive'); 515 if (!metadata || metadata.hosted) 516 return false; 517 entry.pinned = metadata.pinned; 518 return true; 519 }); 520 return hasDirectory ? [] : results; 521 } 522}; 523 524/** 525 * Creates zip file for current selection. 526 */ 527Commands.zipSelectionCommand = { 528 execute: function(event, fileManager, directoryModel) { 529 var dirEntry = directoryModel.getCurrentDirEntry(); 530 var selectionEntries = fileManager.getSelection().entries; 531 fileManager.copyManager_.zipSelection(dirEntry, selectionEntries); 532 }, 533 canExecute: function(event, fileManager) { 534 var selection = fileManager.getSelection(); 535 event.canExecute = !fileManager.isOnReadonlyDirectory() && 536 !fileManager.isOnDrive() && 537 selection && selection.totalCount > 0; 538 } 539}; 540 541/** 542 * Shows the share dialog for the current selection (single only). 543 */ 544Commands.shareCommand = { 545 execute: function(event, fileManager) { 546 fileManager.shareSelection(); 547 }, 548 canExecute: function(event, fileManager) { 549 var selection = fileManager.getSelection(); 550 event.canExecute = fileManager.isOnDrive() && 551 !fileManager.isDriveOffline() && 552 selection && selection.totalCount == 1; 553 event.command.setHidden(!fileManager.isOnDrive()); 554 } 555}; 556 557/** 558 * Creates a shortcut of the selected folder (single only). 559 */ 560Commands.createFolderShortcutCommand = { 561 /** 562 * @param {Event} event Command event. 563 * @param {FileManager} fileManager The file manager instance. 564 */ 565 execute: function(event, fileManager) { 566 var path = CommandUtil.getCommandPath(event.target); 567 if (path) 568 fileManager.createFolderShortcut(path); 569 }, 570 571 /** 572 * @param {Event} event Command event. 573 * @param {FileManager} fileManager The file manager instance. 574 */ 575 canExecute: function(event, fileManager) { 576 var target = event.target; 577 // TODO(yoshiki): remove this after launching folder shortcuts feature. 578 if (!fileManager.isFolderShortcutsEnabled() || 579 (!target instanceof NavigationListItem && 580 !target instanceof DirectoryItem)) { 581 event.command.setHidden(true); 582 return; 583 } 584 585 var path = CommandUtil.getCommandPath(event.target); 586 var folderShortcutExists = path && fileManager.folderShortcutExists(path); 587 588 var onlyOneFolderSelected = true; 589 // Only on list, user can select multiple files. The command is enabled only 590 // when a single file is selected. 591 if (event.target instanceof cr.ui.List) { 592 var items = event.target.selectedItems; 593 onlyOneFolderSelected = (items.length == 1 && items[0].isDirectory); 594 } 595 596 var eligible = path && PathUtil.isEligibleForFolderShortcut(path); 597 event.canExecute = 598 eligible && onlyOneFolderSelected && !folderShortcutExists; 599 event.command.setHidden(!eligible || !onlyOneFolderSelected); 600 } 601}; 602 603/** 604 * Removes the folder shortcut. 605 */ 606Commands.removeFolderShortcutCommand = { 607 /** 608 * @param {Event} event Command event. 609 * @param {FileManager} fileManager The file manager instance. 610 */ 611 execute: function(event, fileManager) { 612 var path = CommandUtil.getCommandPath(event.target); 613 if (path) 614 fileManager.removeFolderShortcut(path); 615 }, 616 617 /** 618 * @param {Event} event Command event. 619 * @param {FileManager} fileManager The file manager instance. 620 */ 621 canExecute: function(event, fileManager) { 622 var target = event.target; 623 // TODO(yoshiki): remove this after launching folder shortcut feature. 624 if (!fileManager.isFolderShortcutsEnabled() || 625 (!target instanceof NavigationListItem && 626 !target instanceof DirectoryItem)) { 627 event.command.setHidden(true); 628 return; 629 } 630 631 var path = CommandUtil.getCommandPath(target); 632 var eligible = path && PathUtil.isEligibleForFolderShortcut(path); 633 var isShortcut = path && fileManager.folderShortcutExists(path); 634 event.canExecute = isShortcut && eligible; 635 event.command.setHidden(!event.canExecute); 636 } 637}; 638 639/** 640 * Zoom in to the Files.app. 641 */ 642Commands.zoomInCommand = { 643 execute: function(event) { 644 chrome.fileBrowserPrivate.zoom('in'); 645 }, 646 canExecute: CommandUtil.canExecuteAlways 647}; 648 649/** 650 * Zoom out from the Files.app. 651 */ 652Commands.zoomOutCommand = { 653 execute: function(event) { 654 chrome.fileBrowserPrivate.zoom('out'); 655 }, 656 canExecute: CommandUtil.canExecuteAlways 657}; 658 659/** 660 * Reset the zoom factor. 661 */ 662Commands.zoomResetCommand = { 663 execute: function(event) { 664 chrome.fileBrowserPrivate.zoom('reset'); 665 }, 666 canExecute: CommandUtil.canExecuteAlways 667}; 668