1// Copyright 2014 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 * Called from the main frame when unloading. 9 * @param {boolean=} opt_exiting True if the app is exiting. 10 */ 11function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting); } 12 13/** 14 * Overrided metadata worker's path. 15 * @type {string} 16 * @const 17 */ 18ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js'; 19 20/** 21 * Gallery for viewing and editing image files. 22 * 23 * @param {!VolumeManager} volumeManager The VolumeManager instance of the 24 * system. 25 * @class 26 * @constructor 27 */ 28function Gallery(volumeManager) { 29 this.context_ = { 30 appWindow: chrome.app.window.current(), 31 onClose: function() { close(); }, 32 onMaximize: function() { 33 var appWindow = chrome.app.window.current(); 34 if (appWindow.isMaximized()) 35 appWindow.restore(); 36 else 37 appWindow.maximize(); 38 }, 39 onMinimize: function() { chrome.app.window.current().minimize(); }, 40 onAppRegionChanged: function() {}, 41 metadataCache: MetadataCache.createFull(volumeManager), 42 shareActions: [], 43 readonlyDirName: '', 44 saveDirEntry: null, 45 displayStringFunction: function() { return ''; }, 46 loadTimeData: {} 47 }; 48 this.container_ = document.querySelector('.gallery'); 49 this.document_ = document; 50 this.metadataCache_ = this.context_.metadataCache; 51 this.volumeManager_ = volumeManager; 52 this.selectedEntry_ = null; 53 this.metadataCacheObserverId_ = null; 54 this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this); 55 56 this.dataModel_ = new cr.ui.ArrayDataModel([]); 57 this.selectionModel_ = new cr.ui.ListSelectionModel(); 58 59 this.initDom_(); 60 this.initListeners_(); 61} 62 63/** 64 * Gallery extends cr.EventTarget. 65 */ 66Gallery.prototype.__proto__ = cr.EventTarget.prototype; 67 68/** 69 * Tools fade-out timeout in milliseconds. 70 * @const 71 * @type {number} 72 */ 73Gallery.FADE_TIMEOUT = 3000; 74 75/** 76 * First time tools fade-out timeout in milliseconds. 77 * @const 78 * @type {number} 79 */ 80Gallery.FIRST_FADE_TIMEOUT = 1000; 81 82/** 83 * Time until mosaic is initialized in the background. Used to make gallery 84 * in the slide mode load faster. In miiliseconds. 85 * @const 86 * @type {number} 87 */ 88Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000; 89 90/** 91 * Types of metadata Gallery uses (to query the metadata cache). 92 * @const 93 * @type {string} 94 */ 95Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming|drive'; 96 97/** 98 * Initializes listeners. 99 * @private 100 */ 101Gallery.prototype.initListeners_ = function() { 102 this.keyDownBound_ = this.onKeyDown_.bind(this); 103 this.document_.body.addEventListener('keydown', this.keyDownBound_); 104 105 this.inactivityWatcher_ = new MouseInactivityWatcher( 106 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this)); 107 108 // Search results may contain files from different subdirectories so 109 // the observer is not going to work. 110 if (!this.context_.searchResults && this.context_.curDirEntry) { 111 this.metadataCacheObserverId_ = this.metadataCache_.addObserver( 112 this.context_.curDirEntry, 113 MetadataCache.CHILDREN, 114 'thumbnail', 115 this.updateThumbnails_.bind(this)); 116 } 117 this.volumeManager_.addEventListener( 118 'externally-unmounted', this.onExternallyUnmountedBound_); 119}; 120 121/** 122 * Closes gallery when a volume containing the selected item is unmounted. 123 * @param {!Event} event The unmount event. 124 * @private 125 */ 126Gallery.prototype.onExternallyUnmounted_ = function(event) { 127 if (!this.selectedEntry_) 128 return; 129 130 if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) === 131 event.volumeInfo) { 132 close(); 133 } 134}; 135 136/** 137 * Unloads the Gallery. 138 * @param {boolean} exiting True if the app is exiting. 139 */ 140Gallery.prototype.onUnload = function(exiting) { 141 if (this.metadataCacheObserverId_ !== null) 142 this.metadataCache_.removeObserver(this.metadataCacheObserverId_); 143 this.volumeManager_.removeEventListener( 144 'externally-unmounted', this.onExternallyUnmountedBound_); 145 this.slideMode_.onUnload(exiting); 146}; 147 148/** 149 * Initializes DOM UI 150 * @private 151 */ 152Gallery.prototype.initDom_ = function() { 153 // Initialize the dialog label. 154 cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL'); 155 cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL'); 156 157 var content = util.createChild(this.container_, 'content'); 158 content.addEventListener('click', this.onContentClick_.bind(this)); 159 160 this.header_ = util.createChild(this.container_, 'header tool dimmable'); 161 this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable'); 162 163 var preventDefault = function(event) { event.preventDefault(); }; 164 165 var minimizeButton = util.createChild(this.header_, 166 'minimize-button tool dimmable', 167 'button'); 168 minimizeButton.tabIndex = -1; 169 minimizeButton.addEventListener('click', this.onMinimize_.bind(this)); 170 minimizeButton.addEventListener('mousedown', preventDefault); 171 172 var maximizeButton = util.createChild(this.header_, 173 'maximize-button tool dimmable', 174 'button'); 175 maximizeButton.tabIndex = -1; 176 maximizeButton.addEventListener('click', this.onMaximize_.bind(this)); 177 maximizeButton.addEventListener('mousedown', preventDefault); 178 179 var closeButton = util.createChild(this.header_, 180 'close-button tool dimmable', 181 'button'); 182 closeButton.tabIndex = -1; 183 closeButton.addEventListener('click', this.onClose_.bind(this)); 184 closeButton.addEventListener('mousedown', preventDefault); 185 186 this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer'); 187 this.filenameEdit_ = util.createChild(this.filenameSpacer_, 188 'namebox', 'input'); 189 190 this.filenameEdit_.setAttribute('type', 'text'); 191 this.filenameEdit_.addEventListener('blur', 192 this.onFilenameEditBlur_.bind(this)); 193 194 this.filenameEdit_.addEventListener('focus', 195 this.onFilenameFocus_.bind(this)); 196 197 this.filenameEdit_.addEventListener('keydown', 198 this.onFilenameEditKeydown_.bind(this)); 199 200 util.createChild(this.toolbar_, 'button-spacer'); 201 202 this.prompt_ = new ImageEditor.Prompt(this.container_, str); 203 204 this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button'); 205 this.modeButton_.addEventListener('click', 206 this.toggleMode_.bind(this, null)); 207 208 this.mosaicMode_ = new MosaicMode(content, 209 this.dataModel_, 210 this.selectionModel_, 211 this.metadataCache_, 212 this.volumeManager_, 213 this.toggleMode_.bind(this, null)); 214 215 this.slideMode_ = new SlideMode(this.container_, 216 content, 217 this.toolbar_, 218 this.prompt_, 219 this.dataModel_, 220 this.selectionModel_, 221 this.context_, 222 this.toggleMode_.bind(this), 223 str); 224 225 this.slideMode_.addEventListener('image-displayed', function() { 226 cr.dispatchSimpleEvent(this, 'image-displayed'); 227 }.bind(this)); 228 this.slideMode_.addEventListener('image-saved', function() { 229 cr.dispatchSimpleEvent(this, 'image-saved'); 230 }.bind(this)); 231 232 var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE'); 233 deleteButton.addEventListener('click', this.delete_.bind(this)); 234 235 this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE'); 236 this.shareButton_.setAttribute('disabled', ''); 237 this.shareButton_.addEventListener('click', this.toggleShare_.bind(this)); 238 239 this.shareMenu_ = util.createChild(this.container_, 'share-menu'); 240 this.shareMenu_.hidden = true; 241 util.createChild(this.shareMenu_, 'bubble-point'); 242 243 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); 244 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); 245 246 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); 247 this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this)); 248}; 249 250/** 251 * Creates toolbar button. 252 * 253 * @param {string} className Class to add. 254 * @param {string} title Button title. 255 * @return {!HTMLElement} Newly created button. 256 * @private 257 */ 258Gallery.prototype.createToolbarButton_ = function(className, title) { 259 var button = util.createChild(this.toolbar_, className, 'button'); 260 button.title = str(title); 261 return button; 262}; 263 264/** 265 * Loads the content. 266 * 267 * @param {!Array.<Entry>} entries Array of entries. 268 * @param {!Array.<Entry>} selectedEntries Array of selected entries. 269 */ 270Gallery.prototype.load = function(entries, selectedEntries) { 271 var items = []; 272 for (var index = 0; index < entries.length; ++index) { 273 items.push(new Gallery.Item(entries[index])); 274 } 275 this.dataModel_.push.apply(this.dataModel_, items); 276 277 this.selectionModel_.adjustLength(this.dataModel_.length); 278 279 // Comparing Entries by reference is not safe. Therefore we have to use URLs. 280 var entryIndexesByURLs = {}; 281 for (var index = 0; index < entries.length; index++) { 282 entryIndexesByURLs[entries[index].toURL()] = index; 283 } 284 285 for (var i = 0; i !== selectedEntries.length; i++) { 286 var selectedIndex = entryIndexesByURLs[selectedEntries[i].toURL()]; 287 if (selectedIndex !== undefined) 288 this.selectionModel_.setIndexSelected(selectedIndex, true); 289 else 290 console.error('Cannot select ' + selectedEntries[i]); 291 } 292 293 if (this.selectionModel_.selectedIndexes.length === 0) 294 this.onSelection_(); 295 296 var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic(); 297 298 // Mosaic view should show up if most of the selected files are images. 299 var imagesCount = 0; 300 for (var i = 0; i !== selectedEntries.length; i++) { 301 if (FileType.getMediaType(selectedEntries[i]) === 'image') 302 imagesCount++; 303 } 304 var mostlyImages = imagesCount > (selectedEntries.length / 2.0); 305 306 var forcedMosaic = (this.context_.pageState && 307 this.context_.pageState.gallery === 'mosaic'); 308 309 var showMosaic = (mostlyImages && selectedEntries.length > 1) || forcedMosaic; 310 if (mosaic && showMosaic) { 311 this.setCurrentMode_(this.mosaicMode_); 312 mosaic.init(); 313 mosaic.show(); 314 this.inactivityWatcher_.check(); // Show the toolbar. 315 cr.dispatchSimpleEvent(this, 'loaded'); 316 } else { 317 this.setCurrentMode_(this.slideMode_); 318 var maybeLoadMosaic = function() { 319 if (mosaic) 320 mosaic.init(); 321 cr.dispatchSimpleEvent(this, 'loaded'); 322 }.bind(this); 323 /* TODO: consider nice blow-up animation for the first image */ 324 this.slideMode_.enter(null, function() { 325 // Flash the toolbar briefly to show it is there. 326 this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT); 327 }.bind(this), 328 maybeLoadMosaic); 329 } 330}; 331 332/** 333 * Handles user's 'Close' action. 334 * @private 335 */ 336Gallery.prototype.onClose_ = function() { 337 this.executeWhenReady(this.context_.onClose); 338}; 339 340/** 341 * Handles user's 'Maximize' action (Escape or a click on the X icon). 342 * @private 343 */ 344Gallery.prototype.onMaximize_ = function() { 345 this.executeWhenReady(this.context_.onMaximize); 346}; 347 348/** 349 * Handles user's 'Maximize' action (Escape or a click on the X icon). 350 * @private 351 */ 352Gallery.prototype.onMinimize_ = function() { 353 this.executeWhenReady(this.context_.onMinimize); 354}; 355 356/** 357 * Executes a function when the editor is done with the modifications. 358 * @param {function} callback Function to execute. 359 */ 360Gallery.prototype.executeWhenReady = function(callback) { 361 this.currentMode_.executeWhenReady(callback); 362}; 363 364/** 365 * @return {Object} File browser private API. 366 */ 367Gallery.getFileBrowserPrivate = function() { 368 return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; 369}; 370 371/** 372 * @return {boolean} True if some tool is currently active. 373 */ 374Gallery.prototype.hasActiveTool = function() { 375 return this.currentMode_.hasActiveTool() || 376 this.isSharing_() || this.isRenaming_(); 377}; 378 379/** 380* External user action event handler. 381* @private 382*/ 383Gallery.prototype.onUserAction_ = function() { 384 this.closeShareMenu_(); 385 // Show the toolbar and hide it after the default timeout. 386 this.inactivityWatcher_.kick(); 387}; 388 389/** 390 * Sets the current mode, update the UI. 391 * @param {Object} mode Current mode. 392 * @private 393 */ 394Gallery.prototype.setCurrentMode_ = function(mode) { 395 if (mode !== this.slideMode_ && mode !== this.mosaicMode_) 396 console.error('Invalid Gallery mode'); 397 398 this.currentMode_ = mode; 399 this.container_.setAttribute('mode', this.currentMode_.getName()); 400 this.updateSelectionAndState_(); 401 this.updateButtons_(); 402}; 403 404/** 405 * Mode toggle event handler. 406 * @param {function=} opt_callback Callback. 407 * @param {Event=} opt_event Event that caused this call. 408 * @private 409 */ 410Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) { 411 if (!this.modeButton_) 412 return; 413 414 if (this.changingMode_) // Do not re-enter while changing the mode. 415 return; 416 417 if (opt_event) 418 this.onUserAction_(); 419 420 this.changingMode_ = true; 421 422 var onModeChanged = function() { 423 this.changingMode_ = false; 424 if (opt_callback) opt_callback(); 425 }.bind(this); 426 427 var tileIndex = Math.max(0, this.selectionModel_.selectedIndex); 428 429 var mosaic = this.mosaicMode_.getMosaic(); 430 var tileRect = mosaic.getTileRect(tileIndex); 431 432 if (this.currentMode_ === this.slideMode_) { 433 this.setCurrentMode_(this.mosaicMode_); 434 mosaic.transform( 435 tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */); 436 this.slideMode_.leave( 437 tileRect, 438 function() { 439 // Animate back to normal position. 440 mosaic.transform(); 441 mosaic.show(); 442 onModeChanged(); 443 }.bind(this)); 444 } else { 445 this.setCurrentMode_(this.slideMode_); 446 this.slideMode_.enter( 447 tileRect, 448 function() { 449 // Animate to zoomed position. 450 mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect()); 451 mosaic.hide(); 452 }.bind(this), 453 onModeChanged); 454 } 455}; 456 457/** 458 * Deletes the selected items. 459 * @private 460 */ 461Gallery.prototype.delete_ = function() { 462 this.onUserAction_(); 463 464 // Clone the sorted selected indexes array. 465 var indexesToRemove = this.selectionModel_.selectedIndexes.slice(); 466 if (!indexesToRemove.length) 467 return; 468 469 /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */ 470 471 var itemsToRemove = this.getSelectedItems(); 472 var plural = itemsToRemove.length > 1; 473 var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName(); 474 475 function deleteNext() { 476 if (!itemsToRemove.length) 477 return; // All deleted. 478 479 var entry = itemsToRemove.pop().getEntry(); 480 entry.remove(deleteNext, function() { 481 util.flog('Error deleting: ' + entry.name, deleteNext); 482 }); 483 } 484 485 // Prevent the Gallery from handling Esc and Enter. 486 this.document_.body.removeEventListener('keydown', this.keyDownBound_); 487 var restoreListener = function() { 488 this.document_.body.addEventListener('keydown', this.keyDownBound_); 489 }.bind(this); 490 491 492 var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_); 493 confirm.setOkLabel(str('DELETE_BUTTON_LABEL')); 494 confirm.show(strf(plural ? 495 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param), 496 function() { 497 restoreListener(); 498 this.selectionModel_.unselectAll(); 499 this.selectionModel_.leadIndex = -1; 500 // Remove items from the data model, starting from the highest index. 501 while (indexesToRemove.length) 502 this.dataModel_.splice(indexesToRemove.pop(), 1); 503 // Delete actual files. 504 deleteNext(); 505 }.bind(this), 506 function() { 507 // Restore the listener after a timeout so that ESC is processed. 508 setTimeout(restoreListener, 0); 509 }); 510}; 511 512/** 513 * @return {Array.<Gallery.Item>} Current selection. 514 */ 515Gallery.prototype.getSelectedItems = function() { 516 return this.selectionModel_.selectedIndexes.map( 517 this.dataModel_.item.bind(this.dataModel_)); 518}; 519 520/** 521 * @return {Array.<Entry>} Array of currently selected entries. 522 */ 523Gallery.prototype.getSelectedEntries = function() { 524 return this.selectionModel_.selectedIndexes.map(function(index) { 525 return this.dataModel_.item(index).getEntry(); 526 }.bind(this)); 527}; 528 529/** 530 * @return {?Gallery.Item} Current single selection. 531 */ 532Gallery.prototype.getSingleSelectedItem = function() { 533 var items = this.getSelectedItems(); 534 if (items.length > 1) { 535 console.error('Unexpected multiple selection'); 536 return null; 537 } 538 return items[0]; 539}; 540 541/** 542 * Selection change event handler. 543 * @private 544 */ 545Gallery.prototype.onSelection_ = function() { 546 this.updateSelectionAndState_(); 547 this.updateShareMenu_(); 548}; 549 550/** 551 * Data model splice event handler. 552 * @private 553 */ 554Gallery.prototype.onSplice_ = function() { 555 this.selectionModel_.adjustLength(this.dataModel_.length); 556}; 557 558/** 559 * Content change event handler. 560 * @param {Event} event Event. 561 * @private 562*/ 563Gallery.prototype.onContentChange_ = function(event) { 564 var index = this.dataModel_.indexOf(event.item); 565 if (index !== this.selectionModel_.selectedIndex) 566 console.error('Content changed for unselected item'); 567 this.updateSelectionAndState_(); 568}; 569 570/** 571 * Keydown handler. 572 * 573 * @param {Event} event Event. 574 * @private 575 */ 576Gallery.prototype.onKeyDown_ = function(event) { 577 var wasSharing = this.isSharing_(); 578 this.closeShareMenu_(); 579 580 if (this.currentMode_.onKeyDown(event)) 581 return; 582 583 switch (util.getKeyModifiers(event) + event.keyIdentifier) { 584 case 'U+0008': // Backspace. 585 // The default handler would call history.back and close the Gallery. 586 event.preventDefault(); 587 break; 588 589 case 'U+004D': // 'm' switches between Slide and Mosaic mode. 590 this.toggleMode_(null, event); 591 break; 592 593 case 'U+0056': // 'v' 594 this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event); 595 break; 596 597 case 'U+007F': // Delete 598 case 'Shift-U+0033': // Shift+'3' (Delete key might be missing). 599 this.delete_(); 600 break; 601 } 602}; 603 604// Name box and rename support. 605 606/** 607 * Updates the UI related to the selected item and the persistent state. 608 * 609 * @private 610 */ 611Gallery.prototype.updateSelectionAndState_ = function() { 612 var numSelectedItems = this.selectionModel_.selectedIndexes.length; 613 var displayName = ''; 614 var selectedEntryURL = null; 615 616 // If it's selecting something, update the variable values. 617 if (numSelectedItems) { 618 var selectedItem = 619 this.dataModel_.item(this.selectionModel_.selectedIndex); 620 this.selectedEntry_ = selectedItem.getEntry(); 621 selectedEntryURL = this.selectedEntry_.toURL(); 622 623 if (numSelectedItems === 1) { 624 window.top.document.title = this.selectedEntry_.name; 625 displayName = ImageUtil.getDisplayNameFromName(this.selectedEntry_.name); 626 } else if (this.context_.curDirEntry) { 627 // If the Gallery was opened on search results the search query will not 628 // be recorded in the app state and the relaunch will just open the 629 // gallery in the curDirEntry directory. 630 window.top.document.title = this.context_.curDirEntry.name; 631 displayName = strf('GALLERY_ITEMS_SELECTED', numSelectedItems); 632 } 633 } 634 635 window.top.util.updateAppState( 636 null, // Keep the current directory. 637 selectedEntryURL, // Update the selection. 638 {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')}); 639 640 // We can't rename files in readonly directory. 641 // We can only rename a single file. 642 this.filenameEdit_.disabled = numSelectedItems !== 1 || 643 this.context_.readonlyDirName; 644 this.filenameEdit_.value = displayName; 645}; 646 647/** 648 * Click event handler on filename edit box 649 * @private 650 */ 651Gallery.prototype.onFilenameFocus_ = function() { 652 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); 653 this.filenameEdit_.originalValue = this.filenameEdit_.value; 654 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); 655 this.onUserAction_(); 656}; 657 658/** 659 * Blur event handler on filename edit box. 660 * 661 * @param {Event} event Blur event. 662 * @return {boolean} if default action should be prevented. 663 * @private 664 */ 665Gallery.prototype.onFilenameEditBlur_ = function(event) { 666 if (this.filenameEdit_.value && this.filenameEdit_.value[0] === '.') { 667 this.prompt_.show('GALLERY_FILE_HIDDEN_NAME', 5000); 668 this.filenameEdit_.focus(); 669 event.stopPropagation(); 670 event.preventDefault(); 671 return false; 672 } 673 674 var item = this.getSingleSelectedItem(); 675 if (item) { 676 var oldEntry = item.getEntry(); 677 678 var onFileExists = function() { 679 this.prompt_.show('GALLERY_FILE_EXISTS', 3000); 680 this.filenameEdit_.value = name; 681 this.filenameEdit_.focus(); 682 }.bind(this); 683 684 var onSuccess = function() { 685 var event = new Event('content'); 686 event.item = item; 687 event.oldEntry = oldEntry; 688 event.metadata = null; // Metadata unchanged. 689 this.dataModel_.dispatchEvent(event); 690 }.bind(this); 691 692 if (this.filenameEdit_.value) { 693 item.rename( 694 this.filenameEdit_.value, onSuccess, onFileExists); 695 } 696 } 697 698 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); 699 this.onUserAction_(); 700}; 701 702/** 703 * Keydown event handler on filename edit box 704 * @private 705 */ 706Gallery.prototype.onFilenameEditKeydown_ = function() { 707 switch (event.keyCode) { 708 case 27: // Escape 709 this.filenameEdit_.value = this.filenameEdit_.originalValue; 710 this.filenameEdit_.blur(); 711 break; 712 713 case 13: // Enter 714 this.filenameEdit_.blur(); 715 break; 716 } 717 event.stopPropagation(); 718}; 719 720/** 721 * @return {boolean} True if file renaming is currently in progress. 722 * @private 723 */ 724Gallery.prototype.isRenaming_ = function() { 725 return this.filenameSpacer_.hasAttribute('renaming'); 726}; 727 728/** 729 * Content area click handler. 730 * @private 731 */ 732Gallery.prototype.onContentClick_ = function() { 733 this.closeShareMenu_(); 734 this.filenameEdit_.blur(); 735}; 736 737// Share button support. 738 739/** 740 * @return {boolean} True if the Share menu is active. 741 * @private 742 */ 743Gallery.prototype.isSharing_ = function() { 744 return !this.shareMenu_.hidden; 745}; 746 747/** 748 * Close Share menu if it is open. 749 * @private 750 */ 751Gallery.prototype.closeShareMenu_ = function() { 752 if (this.isSharing_()) 753 this.toggleShare_(); 754}; 755 756/** 757 * Share button handler. 758 * @private 759 */ 760Gallery.prototype.toggleShare_ = function() { 761 if (!this.shareButton_.hasAttribute('disabled')) 762 this.shareMenu_.hidden = !this.shareMenu_.hidden; 763 this.inactivityWatcher_.check(); 764}; 765 766/** 767 * Updates available actions list based on the currently selected urls. 768 * @private. 769 */ 770Gallery.prototype.updateShareMenu_ = function() { 771 var entries = this.getSelectedEntries(); 772 773 function isShareAction(task) { 774 var taskParts = task.taskId.split('|'); 775 return taskParts[0] !== chrome.runtime.id; 776 } 777 778 var api = Gallery.getFileBrowserPrivate(); 779 var mimeTypes = []; // TODO(kaznacheev) Collect mime types properly. 780 781 var createShareMenu = function(tasks) { 782 var wasHidden = this.shareMenu_.hidden; 783 this.shareMenu_.hidden = true; 784 var items = this.shareMenu_.querySelectorAll('.item'); 785 for (var i = 0; i !== items.length; i++) { 786 items[i].parentNode.removeChild(items[i]); 787 } 788 789 for (var t = 0; t !== tasks.length; t++) { 790 var task = tasks[t]; 791 if (!isShareAction(task)) 792 continue; 793 // Filter out Files.app tasks. 794 // TODO(hirono): Remove the hack after the files.app's handlers are 795 // removed. 796 if (task.taskId.indexOf('hhaomjibdihmijegdhdafkllkbggdgoj|') === 0) 797 continue; 798 799 var item = util.createChild(this.shareMenu_, 'item'); 800 item.textContent = task.title; 801 item.style.backgroundImage = 'url(' + task.iconUrl + ')'; 802 item.addEventListener('click', function(taskId) { 803 this.toggleShare_(); // Hide the menu. 804 // TODO(hirono): Use entries instead of URLs. 805 this.executeWhenReady( 806 api.executeTask.bind( 807 api, 808 taskId, 809 util.entriesToURLs(entries), 810 function(result) { 811 var alertDialog = 812 new cr.ui.dialogs.AlertDialog(this.container_); 813 util.isTeleported(window).then(function(teleported) { 814 if (teleported) 815 util.showOpenInOtherDesktopAlert(alertDialog, entries); 816 }.bind(this)); 817 }.bind(this))); 818 }.bind(this, task.taskId)); 819 } 820 821 var empty = this.shareMenu_.querySelector('.item') === null; 822 ImageUtil.setAttribute(this.shareButton_, 'disabled', empty); 823 this.shareMenu_.hidden = wasHidden || empty; 824 }.bind(this); 825 826 // Create or update the share menu with a list of sharing tasks and show 827 // or hide the share button. 828 // TODO(mtomasz): Pass Entries directly, instead of URLs. 829 if (!entries.length) 830 createShareMenu([]); // Empty list of tasks, since there is no selection. 831 else 832 api.getFileTasks(util.entriesToURLs(entries), mimeTypes, createShareMenu); 833}; 834 835/** 836 * Updates thumbnails. 837 * @private 838 */ 839Gallery.prototype.updateThumbnails_ = function() { 840 if (this.currentMode_ === this.slideMode_) 841 this.slideMode_.updateThumbnails(); 842 843 if (this.mosaicMode_) { 844 var mosaic = this.mosaicMode_.getMosaic(); 845 if (mosaic.isInitialized()) 846 mosaic.reload(); 847 } 848}; 849 850/** 851 * Updates buttons. 852 * @private 853 */ 854Gallery.prototype.updateButtons_ = function() { 855 if (this.modeButton_) { 856 var oppositeMode = 857 this.currentMode_ === this.slideMode_ ? this.mosaicMode_ : 858 this.slideMode_; 859 this.modeButton_.title = str(oppositeMode.getTitle()); 860 } 861}; 862 863/** 864 * Singleton gallery. 865 * @type {Gallery} 866 */ 867var gallery = null; 868 869/** 870 * Initialize the window. 871 * @param {Object} backgroundComponents Background components. 872 */ 873window.initialize = function(backgroundComponents) { 874 window.loadTimeData.data = backgroundComponents.stringData; 875 gallery = new Gallery(backgroundComponents.volumeManager); 876}; 877 878/** 879 * Loads entries. 880 */ 881window.loadEntries = function(entries, selectedEntries) { 882 gallery.load(entries, selectedEntries); 883}; 884