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