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