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 * This object encapsulates everything related to tasks execution. 9 * 10 * TODO(hirono): Pass each component instead of the entire FileManager. 11 * @param {FileManager} fileManager FileManager instance. 12 * @param {Object=} opt_params File manager load parameters. 13 * @constructor 14 */ 15function FileTasks(fileManager, opt_params) { 16 this.fileManager_ = fileManager; 17 this.params_ = opt_params; 18 this.tasks_ = null; 19 this.defaultTask_ = null; 20 this.entries_ = null; 21 22 /** 23 * List of invocations to be called once tasks are available. 24 * 25 * @private 26 * @type {Array.<Object>} 27 */ 28 this.pendingInvocations_ = []; 29} 30 31/** 32 * Location of the Chrome Web Store. 33 * 34 * @const 35 * @type {string} 36 */ 37FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore'; 38 39/** 40 * Base URL of apps list in the Chrome Web Store. This constant is used in 41 * FileTasks.createWebStoreLink(). 42 * 43 * @const 44 * @type {string} 45 */ 46FileTasks.WEB_STORE_HANDLER_BASE_URL = 47 'https://chrome.google.com/webstore/category/collection/file_handlers'; 48 49 50/** 51 * The app ID of the video player app. 52 * @const 53 * @type {string} 54 */ 55FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko'; 56 57/** 58 * Returns URL of the Chrome Web Store which show apps supporting the given 59 * file-extension and mime-type. 60 * 61 * @param {string} extension Extension of the file (with the first dot). 62 * @param {string} mimeType Mime type of the file. 63 * @return {string} URL 64 */ 65FileTasks.createWebStoreLink = function(extension, mimeType) { 66 if (!extension) 67 return FileTasks.CHROME_WEB_STORE_URL; 68 69 if (extension[0] === '.') 70 extension = extension.substr(1); 71 else 72 console.warn('Please pass an extension with a dot to createWebStoreLink.'); 73 74 var url = FileTasks.WEB_STORE_HANDLER_BASE_URL; 75 url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, ''); 76 77 // If a mime is given, add it into the URL. 78 if (mimeType) 79 url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, ''); 80 return url; 81}; 82 83/** 84 * Complete the initialization. 85 * 86 * @param {Array.<Entry>} entries List of file entries. 87 * @param {Array.<string>=} opt_mimeTypes List of MIME types for each 88 * of the files. 89 */ 90FileTasks.prototype.init = function(entries, opt_mimeTypes) { 91 this.entries_ = entries; 92 this.mimeTypes_ = opt_mimeTypes || []; 93 94 // TODO(mtomasz): Move conversion from entry to url to custom bindings. 95 var urls = util.entriesToURLs(entries); 96 if (urls.length > 0) { 97 chrome.fileBrowserPrivate.getFileTasks(urls, this.mimeTypes_, 98 this.onTasks_.bind(this)); 99 } 100}; 101 102/** 103 * Returns amount of tasks. 104 * 105 * @return {number} amount of tasks. 106 */ 107FileTasks.prototype.size = function() { 108 return (this.tasks_ && this.tasks_.length) || 0; 109}; 110 111/** 112 * Callback when tasks found. 113 * 114 * @param {Array.<Object>} tasks The tasks. 115 * @private 116 */ 117FileTasks.prototype.onTasks_ = function(tasks) { 118 this.processTasks_(tasks); 119 for (var index = 0; index < this.pendingInvocations_.length; index++) { 120 var name = this.pendingInvocations_[index][0]; 121 var args = this.pendingInvocations_[index][1]; 122 this[name].apply(this, args); 123 } 124 this.pendingInvocations_ = []; 125}; 126 127/** 128 * The list of known extensions to record UMA. 129 * Note: Because the data is recorded by the index, so new item shouldn't be 130 * inserted. 131 * 132 * @const 133 * @type {Array.<string>} 134 * @private 135 */ 136FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_ = Object.freeze([ 137 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv', 138 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8', 139 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf', 140 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt', 141 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp', 142 '.wma', '.wmv', '.xls', '.xlsx', 143]); 144 145/** 146 * The list of executable file extensions. 147 * 148 * @const 149 * @type {Array.<string>} 150 */ 151FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([ 152 '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi', 153]); 154 155/** 156 * The list of extensions to skip the suggest app dialog. 157 * @const 158 * @type {Array.<string>} 159 * @private 160 */ 161FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([ 162 '.crdownload', '.dsc', '.inf', '.crx', 163]); 164 165/** 166 * Records trial of opening file grouped by extensions. 167 * 168 * @param {Array.<Entry>} entries The entries to be opened. 169 * @private 170 */ 171FileTasks.recordViewingFileTypeUMA_ = function(entries) { 172 for (var i = 0; i < entries.length; i++) { 173 var entry = entries[i]; 174 var extension = FileType.getExtension(entry).toLowerCase(); 175 if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_.indexOf(extension) < 0) { 176 extension = 'other'; 177 } 178 metrics.recordEnum( 179 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_); 180 } 181}; 182 183/** 184 * Returns true if the taskId is for an internal task. 185 * 186 * @param {string} taskId Task identifier. 187 * @return {boolean} True if the task ID is for an internal task. 188 * @private 189 */ 190FileTasks.isInternalTask_ = function(taskId) { 191 var taskParts = taskId.split('|'); 192 var appId = taskParts[0]; 193 var taskType = taskParts[1]; 194 var actionId = taskParts[2]; 195 // The action IDs here should match ones used in executeInternalTask_(). 196 return (appId === chrome.runtime.id && 197 taskType === 'file' && 198 (actionId === 'play' || 199 actionId === 'mount-archive' || 200 actionId === 'gallery' || 201 actionId === 'gallery-video')); 202}; 203 204/** 205 * Processes internal tasks. 206 * 207 * @param {Array.<Object>} tasks The tasks. 208 * @private 209 */ 210FileTasks.prototype.processTasks_ = function(tasks) { 211 this.tasks_ = []; 212 var id = chrome.runtime.id; 213 var isOnDrive = false; 214 var fm = this.fileManager_; 215 for (var index = 0; index < this.entries_.length; ++index) { 216 var locationInfo = fm.volumeManager.getLocationInfo(this.entries_[index]); 217 if (locationInfo && locationInfo.isDriveBased) { 218 isOnDrive = true; 219 break; 220 } 221 } 222 223 for (var i = 0; i < tasks.length; i++) { 224 var task = tasks[i]; 225 var taskParts = task.taskId.split('|'); 226 227 // Skip internal Files.app's handlers. 228 if (taskParts[0] === id && (taskParts[2] === 'auto-open' || 229 taskParts[2] === 'select' || taskParts[2] === 'open')) { 230 continue; 231 } 232 233 // Tweak images, titles of internal tasks. 234 if (taskParts[0] === id && taskParts[1] === 'file') { 235 if (taskParts[2] === 'play') { 236 // TODO(serya): This hack needed until task.iconUrl is working 237 // (see GetFileTasksFileBrowserFunction::RunImpl). 238 task.iconType = 'audio'; 239 task.title = loadTimeData.getString('ACTION_LISTEN'); 240 } else if (taskParts[2] === 'mount-archive') { 241 task.iconType = 'archive'; 242 task.title = loadTimeData.getString('MOUNT_ARCHIVE'); 243 } else if (taskParts[2] === 'gallery' || 244 taskParts[2] === 'gallery-video') { 245 task.iconType = 'image'; 246 task.title = loadTimeData.getString('ACTION_OPEN'); 247 } else if (taskParts[2] === 'open-hosted-generic') { 248 if (this.entries_.length > 1) 249 task.iconType = 'generic'; 250 else // Use specific icon. 251 task.iconType = FileType.getIcon(this.entries_[0]); 252 task.title = loadTimeData.getString('ACTION_OPEN'); 253 } else if (taskParts[2] === 'open-hosted-gdoc') { 254 task.iconType = 'gdoc'; 255 task.title = loadTimeData.getString('ACTION_OPEN_GDOC'); 256 } else if (taskParts[2] === 'open-hosted-gsheet') { 257 task.iconType = 'gsheet'; 258 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET'); 259 } else if (taskParts[2] === 'open-hosted-gslides') { 260 task.iconType = 'gslides'; 261 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES'); 262 } else if (taskParts[2] === 'view-swf') { 263 // Do not render this task if disabled. 264 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED')) 265 continue; 266 task.iconType = 'generic'; 267 task.title = loadTimeData.getString('ACTION_VIEW'); 268 } else if (taskParts[2] === 'view-pdf') { 269 // Do not render this task if disabled. 270 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED')) 271 continue; 272 task.iconType = 'pdf'; 273 task.title = loadTimeData.getString('ACTION_VIEW'); 274 } else if (taskParts[2] === 'view-in-browser') { 275 task.iconType = 'generic'; 276 task.title = loadTimeData.getString('ACTION_VIEW'); 277 } 278 } 279 280 if (!task.iconType && taskParts[1] === 'web-intent') { 281 task.iconType = 'generic'; 282 } 283 284 this.tasks_.push(task); 285 if (this.defaultTask_ === null && task.isDefault) { 286 this.defaultTask_ = task; 287 } 288 } 289 if (!this.defaultTask_ && this.tasks_.length > 0) { 290 // If we haven't picked a default task yet, then just pick the first one. 291 // This is not the preferred way we want to pick this, but better this than 292 // no default at all if the C++ code didn't set one. 293 this.defaultTask_ = this.tasks_[0]; 294 } 295}; 296 297/** 298 * Executes default task. 299 * 300 * @param {function(boolean, Array.<string>)=} opt_callback Called when the 301 * default task is executed, or the error is occurred. 302 * @private 303 */ 304FileTasks.prototype.executeDefault_ = function(opt_callback) { 305 FileTasks.recordViewingFileTypeUMA_(this.entries_); 306 this.executeDefaultInternal_(this.entries_, opt_callback); 307}; 308 309/** 310 * Executes default task. 311 * 312 * @param {Array.<Entry>} entries Entries to execute. 313 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the 314 * default task is executed, or the error is occurred. 315 * @private 316 */ 317FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) { 318 var callback = opt_callback || function(arg1, arg2) {}; 319 320 if (this.defaultTask_ !== null) { 321 this.executeInternal_(this.defaultTask_.taskId, entries); 322 callback(true, entries); 323 return; 324 } 325 326 // We don't have tasks, so try to show a file in a browser tab. 327 // We only do that for single selection to avoid confusion. 328 if (entries.length !== 1 || !entries[0]) 329 return; 330 331 var filename = entries[0].name; 332 var extension = util.splitExtension(filename)[1]; 333 var mimeType = this.mimeTypes_[0]; 334 335 var showAlert = function() { 336 var textMessageId; 337 var titleMessageId; 338 switch (extension) { 339 case '.exe': 340 textMessageId = 'NO_ACTION_FOR_EXECUTABLE'; 341 break; 342 case '.crx': 343 textMessageId = 'NO_ACTION_FOR_CRX'; 344 titleMessageId = 'NO_ACTION_FOR_CRX_TITLE'; 345 break; 346 default: 347 textMessageId = 'NO_ACTION_FOR_FILE'; 348 } 349 350 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType); 351 var text = strf(textMessageId, webStoreUrl, str('NO_ACTION_FOR_FILE_URL')); 352 var title = titleMessageId ? str(titleMessageId) : filename; 353 this.fileManager_.alert.showHtml(title, text, function() {}); 354 callback(false, entries); 355 }.bind(this); 356 357 var onViewFilesFailure = function() { 358 var fm = this.fileManager_; 359 if (!fm.isOnDrive() || 360 !entries[0] || 361 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) { 362 showAlert(); 363 return; 364 } 365 366 fm.openSuggestAppsDialog( 367 entries[0], 368 function() { 369 var newTasks = new FileTasks(fm); 370 newTasks.init(entries, this.mimeTypes_); 371 newTasks.executeDefault(); 372 callback(true, entries); 373 }.bind(this), 374 // Cancelled callback. 375 function() { 376 callback(false, entries); 377 }, 378 showAlert); 379 }.bind(this); 380 381 var onViewFiles = function(result) { 382 switch (result) { 383 case 'opened': 384 callback(success, entries); 385 break; 386 case 'message_sent': 387 util.isTeleported(window).then(function(teleported) { 388 if (teleported) { 389 util.showOpenInOtherDesktopAlert( 390 this.fileManager_.ui.alertDialog, entries); 391 } 392 }.bind(this)); 393 callback(success, entries); 394 break; 395 case 'empty': 396 callback(success, entries); 397 break; 398 case 'failed': 399 onViewFilesFailure(); 400 break; 401 } 402 }.bind(this); 403 404 this.checkAvailability_(function() { 405 // TODO(mtomasz): Pass entries instead. 406 var urls = util.entriesToURLs(entries); 407 var taskId = chrome.runtime.id + '|file|view-in-browser'; 408 chrome.fileBrowserPrivate.executeTask(taskId, urls, onViewFiles); 409 }.bind(this)); 410}; 411 412/** 413 * Executes a single task. 414 * 415 * @param {string} taskId Task identifier. 416 * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of 417 * this.entries_|. 418 * @private 419 */ 420FileTasks.prototype.execute_ = function(taskId, opt_entries) { 421 var entries = opt_entries || this.entries_; 422 FileTasks.recordViewingFileTypeUMA_(entries); 423 this.executeInternal_(taskId, entries); 424}; 425 426/** 427 * The core implementation to execute a single task. 428 * 429 * @param {string} taskId Task identifier. 430 * @param {Array.<Entry>} entries Entries to execute. 431 * @private 432 */ 433FileTasks.prototype.executeInternal_ = function(taskId, entries) { 434 this.checkAvailability_(function() { 435 if (FileTasks.isInternalTask_(taskId)) { 436 var taskParts = taskId.split('|'); 437 this.executeInternalTask_(taskParts[2], entries); 438 } else { 439 // TODO(mtomasz): Pass entries instead. 440 var urls = util.entriesToURLs(entries); 441 chrome.fileBrowserPrivate.executeTask(taskId, urls, function(result) { 442 if (result !== 'message_sent') 443 return; 444 util.isTeleported(window).then(function(teleported) { 445 if (teleported) { 446 util.showOpenInOtherDesktopAlert( 447 this.fileManager_.ui.alertDialog, entries); 448 } 449 }.bind(this)); 450 }.bind(this)); 451 } 452 }.bind(this)); 453}; 454 455/** 456 * Checks whether the remote files are available right now. 457 * 458 * @param {function} callback The callback. 459 * @private 460 */ 461FileTasks.prototype.checkAvailability_ = function(callback) { 462 var areAll = function(props, name) { 463 var isOne = function(e) { 464 // If got no properties, we safely assume that item is unavailable. 465 return e && e[name]; 466 }; 467 return props.filter(isOne).length === props.length; 468 }; 469 470 var fm = this.fileManager_; 471 var entries = this.entries_; 472 473 var isDriveOffline = fm.volumeManager.getDriveConnectionState().type === 474 VolumeManagerCommon.DriveConnectionType.OFFLINE; 475 476 if (fm.isOnDrive() && isDriveOffline) { 477 fm.metadataCache_.get(entries, 'drive', function(props) { 478 if (areAll(props, 'availableOffline')) { 479 callback(); 480 return; 481 } 482 483 fm.alert.showHtml( 484 loadTimeData.getString('OFFLINE_HEADER'), 485 props[0].hosted ? 486 loadTimeData.getStringF( 487 entries.length === 1 ? 488 'HOSTED_OFFLINE_MESSAGE' : 489 'HOSTED_OFFLINE_MESSAGE_PLURAL') : 490 loadTimeData.getStringF( 491 entries.length === 1 ? 492 'OFFLINE_MESSAGE' : 493 'OFFLINE_MESSAGE_PLURAL', 494 loadTimeData.getString('OFFLINE_COLUMN_LABEL'))); 495 }); 496 return; 497 } 498 499 var isOnMetered = fm.volumeManager.getDriveConnectionState().type === 500 VolumeManagerCommon.DriveConnectionType.METERED; 501 502 if (fm.isOnDrive() && isOnMetered) { 503 fm.metadataCache_.get(entries, 'drive', function(driveProps) { 504 if (areAll(driveProps, 'availableWhenMetered')) { 505 callback(); 506 return; 507 } 508 509 fm.metadataCache_.get(entries, 'filesystem', function(fileProps) { 510 var sizeToDownload = 0; 511 for (var i = 0; i !== entries.length; i++) { 512 if (!driveProps[i].availableWhenMetered) 513 sizeToDownload += fileProps[i].size; 514 } 515 fm.confirm.show( 516 loadTimeData.getStringF( 517 entries.length === 1 ? 518 'CONFIRM_MOBILE_DATA_USE' : 519 'CONFIRM_MOBILE_DATA_USE_PLURAL', 520 util.bytesToString(sizeToDownload)), 521 callback); 522 }); 523 }); 524 return; 525 } 526 527 callback(); 528}; 529 530/** 531 * Executes an internal task. 532 * 533 * @param {string} id The short task id. 534 * @param {Array.<Entry>} entries The entries to execute on. 535 * @private 536 */ 537FileTasks.prototype.executeInternalTask_ = function(id, entries) { 538 var fm = this.fileManager_; 539 540 if (id === 'play') { 541 var selectedEntry = entries[0]; 542 if (entries.length === 1) { 543 // If just a single audio file is selected pass along every audio file 544 // in the directory. 545 entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio); 546 } 547 // TODO(mtomasz): Pass entries instead. 548 var urls = util.entriesToURLs(entries); 549 var position = urls.indexOf(selectedEntry.toURL()); 550 chrome.fileBrowserPrivate.getProfiles(function(profiles, 551 currentId, 552 displayedId) { 553 fm.backgroundPage.launchAudioPlayer({items: urls, position: position}, 554 displayedId); 555 }); 556 return; 557 } 558 559 if (id === 'mount-archive') { 560 this.mountArchivesInternal_(entries); 561 return; 562 } 563 564 if (id === 'gallery' || id === 'gallery-video') { 565 this.openGalleryInternal_(entries); 566 return; 567 } 568 569 console.error('Unexpected action ID: ' + id); 570}; 571 572/** 573 * Mounts archives. 574 * 575 * @param {Array.<Entry>} entries Mount file entries list. 576 */ 577FileTasks.prototype.mountArchives = function(entries) { 578 FileTasks.recordViewingFileTypeUMA_(entries); 579 this.mountArchivesInternal_(entries); 580}; 581 582/** 583 * The core implementation of mounts archives. 584 * 585 * @param {Array.<Entry>} entries Mount file entries list. 586 * @private 587 */ 588FileTasks.prototype.mountArchivesInternal_ = function(entries) { 589 var fm = this.fileManager_; 590 591 var tracker = fm.directoryModel.createDirectoryChangeTracker(); 592 tracker.start(); 593 594 // TODO(mtomasz): Pass Entries instead of URLs. 595 var urls = util.entriesToURLs(entries); 596 fm.resolveSelectResults_(urls, function(resolvedURLs) { 597 for (var index = 0; index < resolvedURLs.length; ++index) { 598 // TODO(mtomasz): Pass Entry instead of URL. 599 fm.volumeManager.mountArchive(resolvedURLs[index], 600 function(volumeInfo) { 601 if (tracker.hasChanged) { 602 tracker.stop(); 603 return; 604 } 605 volumeInfo.resolveDisplayRoot(function(displayRoot) { 606 if (tracker.hasChanged) { 607 tracker.stop(); 608 return; 609 } 610 fm.directoryModel.changeDirectoryEntry(displayRoot); 611 }, function() { 612 console.warn('Failed to resolve the display root after mounting.'); 613 tracker.stop(); 614 }); 615 }, function(url, error) { 616 tracker.stop(); 617 var path = util.extractFilePath(url); 618 var namePos = path.lastIndexOf('/'); 619 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED', 620 path.substr(namePos + 1), error)); 621 }.bind(null, resolvedURLs[index])); 622 } 623 }); 624}; 625 626/** 627 * Open the Gallery. 628 * 629 * @param {Array.<Entry>} entries List of selected entries. 630 */ 631FileTasks.prototype.openGallery = function(entries) { 632 FileTasks.recordViewingFileTypeUMA_(entries); 633 this.openGalleryInternal_(entries); 634}; 635 636/** 637 * The core implementation to open the Gallery. 638 * 639 * @param {Array.<Entry>} entries List of selected entries. 640 * @private 641 */ 642FileTasks.prototype.openGalleryInternal_ = function(entries) { 643 var fm = this.fileManager_; 644 645 var allEntries = 646 fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo); 647 648 var galleryFrame = fm.document_.createElement('iframe'); 649 galleryFrame.className = 'overlay-pane'; 650 galleryFrame.scrolling = 'no'; 651 galleryFrame.setAttribute('webkitallowfullscreen', true); 652 653 if (this.params_ && this.params_.gallery) { 654 // Remove the Gallery state from the location, we do not need it any more. 655 // TODO(mtomasz): Consider keeping the selection path. 656 util.updateAppState( 657 null, /* keep current directory */ 658 '', /* remove current selection */ 659 '' /* remove search. */); 660 } 661 662 var savedAppState = JSON.parse(JSON.stringify(window.appState)); 663 var savedTitle = document.title; 664 665 // Push a temporary state which will be replaced every time the selection 666 // changes in the Gallery and popped when the Gallery is closed. 667 util.updateAppState(); 668 669 var onBack = function(selectedEntries) { 670 fm.directoryModel.selectEntries(selectedEntries); 671 fm.closeFilePopup(); // Will call Gallery.unload. 672 window.appState = savedAppState; 673 util.saveAppState(); 674 document.title = savedTitle; 675 }; 676 677 var onAppRegionChanged = function(visible) { 678 fm.onFilePopupAppRegionChanged(visible); 679 }; 680 681 galleryFrame.onload = function() { 682 galleryFrame.contentWindow.ImageUtil.metrics = metrics; 683 684 // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the 685 // root. We should check more granular permission to know whether the file 686 // is writable or not. 687 var readonly = fm.isOnReadonlyDirectory(); 688 var currentDir = fm.getCurrentDirectoryEntry(); 689 var downloadsVolume = fm.volumeManager.getCurrentProfileVolumeInfo( 690 VolumeManagerCommon.RootType.DOWNLOADS); 691 var downloadsDir = downloadsVolume && downloadsVolume.fileSystem.root; 692 693 // TODO(mtomasz): Pass Entry instead of localized name. Conversion to a 694 // display string should be done in gallery.js. 695 var readonlyDirName = null; 696 if (readonly && currentDir) 697 readonlyDirName = util.getEntryLabel(fm.volumeManager, currentDir); 698 699 var context = { 700 // We show the root label in readonly warning (e.g. archive name). 701 readonlyDirName: readonlyDirName, 702 curDirEntry: currentDir, 703 saveDirEntry: readonly ? downloadsDir : null, 704 searchResults: fm.directoryModel.isSearching(), 705 metadataCache: fm.metadataCache_, 706 pageState: this.params_, 707 appWindow: chrome.app.window.current(), 708 onBack: onBack, 709 onClose: fm.onClose.bind(fm), 710 onMaximize: fm.onMaximize.bind(fm), 711 onMinimize: fm.onMinimize.bind(fm), 712 onAppRegionChanged: onAppRegionChanged, 713 loadTimeData: fm.backgroundPage.background.stringData 714 }; 715 galleryFrame.contentWindow.Gallery.open( 716 context, fm.volumeManager, allEntries, entries); 717 }.bind(this); 718 719 galleryFrame.src = 'gallery.html'; 720 fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm)); 721}; 722 723/** 724 * Displays the list of tasks in a task picker combobutton. 725 * 726 * @param {cr.ui.ComboButton} combobutton The task picker element. 727 * @private 728 */ 729FileTasks.prototype.display_ = function(combobutton) { 730 if (this.tasks_.length === 0) { 731 combobutton.hidden = true; 732 return; 733 } 734 735 combobutton.clear(); 736 combobutton.hidden = false; 737 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_); 738 739 var items = this.createItems_(); 740 741 if (items.length > 1) { 742 var defaultIdx = 0; 743 744 for (var j = 0; j < items.length; j++) { 745 combobutton.addDropDownItem(items[j]); 746 if (items[j].task.taskId === this.defaultTask_.taskId) 747 defaultIdx = j; 748 } 749 750 combobutton.addSeparator(); 751 var changeDefaultMenuItem = combobutton.addDropDownItem({ 752 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM') 753 }); 754 changeDefaultMenuItem.classList.add('change-default'); 755 } 756}; 757 758/** 759 * Creates sorted array of available task descriptions such as title and icon. 760 * 761 * @return {Array} created array can be used to feed combobox, menus and so on. 762 * @private 763 */ 764FileTasks.prototype.createItems_ = function() { 765 var items = []; 766 var title = this.defaultTask_.title + ' ' + 767 loadTimeData.getString('DEFAULT_ACTION_LABEL'); 768 items.push(this.createCombobuttonItem_(this.defaultTask_, title, true)); 769 770 for (var index = 0; index < this.tasks_.length; index++) { 771 var task = this.tasks_[index]; 772 if (task !== this.defaultTask_) 773 items.push(this.createCombobuttonItem_(task)); 774 } 775 776 items.sort(function(a, b) { 777 return a.label.localeCompare(b.label); 778 }); 779 780 return items; 781}; 782 783/** 784 * Updates context menu with default item. 785 * @private 786 */ 787 788FileTasks.prototype.updateMenuItem_ = function() { 789 this.fileManager_.updateContextMenuActionItems(this.defaultTask_, 790 this.tasks_.length > 1); 791}; 792 793/** 794 * Creates combobutton item based on task. 795 * 796 * @param {Object} task Task to convert. 797 * @param {string=} opt_title Title. 798 * @param {boolean=} opt_bold Make a menu item bold. 799 * @return {Object} Item appendable to combobutton drop-down list. 800 * @private 801 */ 802FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title, 803 opt_bold) { 804 return { 805 label: opt_title || task.title, 806 iconUrl: task.iconUrl, 807 iconType: task.iconType, 808 task: task, 809 bold: opt_bold || false 810 }; 811}; 812 813/** 814 * Shows modal action picker dialog with currently available list of tasks. 815 * 816 * @param {DefaultActionDialog} actionDialog Action dialog to show and update. 817 * @param {string} title Title to use. 818 * @param {string} message Message to use. 819 * @param {function(Object)} onSuccess Callback to pass selected task. 820 */ 821FileTasks.prototype.showTaskPicker = function(actionDialog, title, message, 822 onSuccess) { 823 var items = this.createItems_(); 824 825 var defaultIdx = 0; 826 for (var j = 0; j < items.length; j++) { 827 if (items[j].task.taskId === this.defaultTask_.taskId) 828 defaultIdx = j; 829 } 830 831 actionDialog.show( 832 title, 833 message, 834 items, defaultIdx, 835 function(item) { 836 onSuccess(item.task); 837 }); 838}; 839 840/** 841 * Decorates a FileTasks method, so it will be actually executed after the tasks 842 * are available. 843 * This decorator expects an implementation called |method + '_'|. 844 * 845 * @param {string} method The method name. 846 */ 847FileTasks.decorate = function(method) { 848 var privateMethod = method + '_'; 849 FileTasks.prototype[method] = function() { 850 if (this.tasks_) { 851 this[privateMethod].apply(this, arguments); 852 } else { 853 this.pendingInvocations_.push([privateMethod, arguments]); 854 } 855 return this; 856 }; 857}; 858 859FileTasks.decorate('display'); 860FileTasks.decorate('updateMenuItem'); 861FileTasks.decorate('execute'); 862FileTasks.decorate('executeDefault'); 863