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 */ 11 function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting); } 12 13 /** 14 * Overrided metadata worker's path. 15 * @type {string} 16 * @const 17 */ 18 ContentProvider.WORKER_SCRIPT = '/js/metadata_worker.js'; 19 20 /** 21 * Data model for gallery. 22 * 23 * @param {MetadataCache} metadataCache Metadata cache. 24 * @constructor 25 * @extends {cr.ui.ArrayDataModel} 26 */ 27 function GalleryDataModel(metadataCache) { 28 cr.ui.ArrayDataModel.call(this, []); 29 30 /** 31 * Metadata cache. 32 * @type {MetadataCache} 33 * @private 34 */ 35 this.metadataCache_ = metadataCache; 36 37 /** 38 * Directory where the image is saved if the image is located in a read-only 39 * volume. 40 * @type {DirectoryEntry} 41 */ 42 this.fallbackSaveDirectory = null; 43 } 44 45 /** 46 * Maximum number of full size image cache. 47 * @type {number} 48 * @const 49 * @private 50 */ 51 GalleryDataModel.MAX_FULL_IMAGE_CACHE_ = 3; 52 53 /** 54 * Maximum number of screen size image cache. 55 * @type {number} 56 * @const 57 * @private 58 */ 59 GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_ = 5; 60 61 GalleryDataModel.prototype = { 62 __proto__: cr.ui.ArrayDataModel.prototype 63 }; 64 65 /** 66 * Saves new image. 67 * 68 * @param {VolumeManager} volumeManager Volume manager instance. 69 * @param {Gallery.Item} item Original gallery item. 70 * @param {Canvas} canvas Canvas containing new image. 71 * @param {boolean} overwrite Whether to overwrite the image to the item or not. 72 * @return {Promise} Promise to be fulfilled with when the operation completes. 73 */ 74 GalleryDataModel.prototype.saveItem = function( 75 volumeManager, item, canvas, overwrite) { 76 var oldEntry = item.getEntry(); 77 var oldMetadata = item.getMetadata(); 78 var oldLocationInfo = item.getLocationInfo(); 79 var metadataEncoder = ImageEncoder.encodeMetadata( 80 item.getMetadata(), canvas, 1 /* quality */); 81 var newMetadata = ContentProvider.ConvertContentMetadata( 82 metadataEncoder.getMetadata(), 83 MetadataCache.cloneMetadata(item.getMetadata())); 84 if (newMetadata.filesystem) 85 newMetadata.filesystem.modificationTime = new Date(); 86 if (newMetadata.external) 87 newMetadata.external.present = true; 88 89 return new Promise(function(fulfill, reject) { 90 item.saveToFile( 91 volumeManager, 92 this.fallbackSaveDirectory, 93 overwrite, 94 canvas, 95 metadataEncoder, 96 function(success) { 97 if (!success) { 98 reject('Failed to save the image.'); 99 return; 100 } 101 102 // The item's entry is updated to the latest entry. Update metadata. 103 item.setMetadata(newMetadata); 104 105 // Current entry is updated. 106 // Dispatch an event. 107 var event = new Event('content'); 108 event.item = item; 109 event.oldEntry = oldEntry; 110 event.metadata = newMetadata; 111 this.dispatchEvent(event); 112 113 if (util.isSameEntry(oldEntry, item.getEntry())) { 114 // Need an update of metdataCache. 115 this.metadataCache_.set( 116 item.getEntry(), 117 Gallery.METADATA_TYPE, 118 newMetadata); 119 } else { 120 // New entry is added and the item now tracks it. 121 // Add another item for the old entry. 122 var anotherItem = new Gallery.Item( 123 oldEntry, 124 oldLocationInfo, 125 oldMetadata, 126 this.metadataCache_, 127 item.isOriginal()); 128 // The item must be added behind the existing item so that it does 129 // not change the index of the existing item. 130 // TODO(hirono): Update the item index of the selection model 131 // correctly. 132 this.splice(this.indexOf(item) + 1, 0, anotherItem); 133 } 134 135 fulfill(); 136 }.bind(this)); 137 }.bind(this)); 138 }; 139 140 /** 141 * Evicts image caches in the items. 142 * @param {Gallery.Item} currentSelectedItem Current selected item. 143 */ 144 GalleryDataModel.prototype.evictCache = function(currentSelectedItem) { 145 // Sort the item by the last accessed date. 146 var sorted = this.slice().sort(function(a, b) { 147 return b.getLastAccessedDate() - a.getLastAccessedDate(); 148 }); 149 150 // Evict caches. 151 var contentCacheCount = 0; 152 var screenCacheCount = 0; 153 for (var i = 0; i < sorted.length; i++) { 154 if (sorted[i].contentImage) { 155 if (++contentCacheCount > GalleryDataModel.MAX_FULL_IMAGE_CACHE_) { 156 if (sorted[i].contentImage.parentNode) { 157 console.error('The content image has a parent node.'); 158 } else { 159 // Force to free the buffer of the canvas by assigning zero size. 160 sorted[i].contentImage.width = 0; 161 sorted[i].contentImage.height = 0; 162 sorted[i].contentImage = null; 163 } 164 } 165 } 166 if (sorted[i].screenImage) { 167 if (++screenCacheCount > GalleryDataModel.MAX_SCREEN_IMAGE_CACHE_) { 168 if (sorted[i].screenImage.parentNode) { 169 console.error('The screen image has a parent node.'); 170 } else { 171 // Force to free the buffer of the canvas by assigning zero size. 172 sorted[i].screenImage.width = 0; 173 sorted[i].screenImage.height = 0; 174 sorted[i].screenImage = null; 175 } 176 } 177 } 178 } 179 }; 180 181 /** 182 * Gallery for viewing and editing image files. 183 * 184 * @param {!VolumeManager} volumeManager The VolumeManager instance of the 185 * system. 186 * @constructor 187 */ 188 function Gallery(volumeManager) { 189 this.context_ = { 190 appWindow: chrome.app.window.current(), 191 onClose: function() { close(); }, 192 onMaximize: function() { 193 var appWindow = chrome.app.window.current(); 194 if (appWindow.isMaximized()) 195 appWindow.restore(); 196 else 197 appWindow.maximize(); 198 }, 199 onMinimize: function() { chrome.app.window.current().minimize(); }, 200 onAppRegionChanged: function() {}, 201 metadataCache: MetadataCache.createFull(volumeManager), 202 readonlyDirName: '', 203 displayStringFunction: function() { return ''; }, 204 loadTimeData: {} 205 }; 206 this.container_ = document.querySelector('.gallery'); 207 this.document_ = document; 208 this.metadataCache_ = this.context_.metadataCache; 209 this.volumeManager_ = volumeManager; 210 this.selectedEntry_ = null; 211 this.metadataCacheObserverId_ = null; 212 this.onExternallyUnmountedBound_ = this.onExternallyUnmounted_.bind(this); 213 214 this.dataModel_ = new GalleryDataModel( 215 this.context_.metadataCache); 216 var downloadVolumeInfo = this.volumeManager_.getCurrentProfileVolumeInfo( 217 VolumeManagerCommon.VolumeType.DOWNLOADS); 218 downloadVolumeInfo.resolveDisplayRoot().then(function(entry) { 219 this.dataModel_.fallbackSaveDirectory = entry; 220 }.bind(this)).catch(function(error) { 221 console.error( 222 'Failed to obtain the fallback directory: ' + (error.stack || error)); 223 }); 224 this.selectionModel_ = new cr.ui.ListSelectionModel(); 225 226 this.initDom_(); 227 this.initListeners_(); 228 } 229 230 /** 231 * Gallery extends cr.EventTarget. 232 */ 233 Gallery.prototype.__proto__ = cr.EventTarget.prototype; 234 235 /** 236 * Tools fade-out timeout in milliseconds. 237 * @const 238 * @type {number} 239 */ 240 Gallery.FADE_TIMEOUT = 3000; 241 242 /** 243 * First time tools fade-out timeout in milliseconds. 244 * @const 245 * @type {number} 246 */ 247 Gallery.FIRST_FADE_TIMEOUT = 1000; 248 249 /** 250 * Time until mosaic is initialized in the background. Used to make gallery 251 * in the slide mode load faster. In milliseconds. 252 * @const 253 * @type {number} 254 */ 255 Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000; 256 257 /** 258 * Types of metadata Gallery uses (to query the metadata cache). 259 * @const 260 * @type {string} 261 */ 262 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|external'; 263 264 /** 265 * Initializes listeners. 266 * @private 267 */ 268 Gallery.prototype.initListeners_ = function() { 269 this.keyDownBound_ = this.onKeyDown_.bind(this); 270 this.document_.body.addEventListener('keydown', this.keyDownBound_); 271 272 this.inactivityWatcher_ = new MouseInactivityWatcher( 273 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this)); 274 275 // Search results may contain files from different subdirectories so 276 // the observer is not going to work. 277 if (!this.context_.searchResults && this.context_.curDirEntry) { 278 this.metadataCacheObserverId_ = this.metadataCache_.addObserver( 279 this.context_.curDirEntry, 280 MetadataCache.CHILDREN, 281 'thumbnail', 282 this.updateThumbnails_.bind(this)); 283 } 284 this.volumeManager_.addEventListener( 285 'externally-unmounted', this.onExternallyUnmountedBound_); 286 }; 287 288 /** 289 * Closes gallery when a volume containing the selected item is unmounted. 290 * @param {!Event} event The unmount event. 291 * @private 292 */ 293 Gallery.prototype.onExternallyUnmounted_ = function(event) { 294 if (!this.selectedEntry_) 295 return; 296 297 if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) === 298 event.volumeInfo) { 299 close(); 300 } 301 }; 302 303 /** 304 * Unloads the Gallery. 305 * @param {boolean} exiting True if the app is exiting. 306 */ 307 Gallery.prototype.onUnload = function(exiting) { 308 if (this.metadataCacheObserverId_ !== null) 309 this.metadataCache_.removeObserver(this.metadataCacheObserverId_); 310 this.volumeManager_.removeEventListener( 311 'externally-unmounted', this.onExternallyUnmountedBound_); 312 this.slideMode_.onUnload(exiting); 313 }; 314 315 /** 316 * Initializes DOM UI 317 * @private 318 */ 319 Gallery.prototype.initDom_ = function() { 320 // Initialize the dialog label. 321 cr.ui.dialogs.BaseDialog.OK_LABEL = str('GALLERY_OK_LABEL'); 322 cr.ui.dialogs.BaseDialog.CANCEL_LABEL = str('GALLERY_CANCEL_LABEL'); 323 324 var content = document.querySelector('#content'); 325 content.addEventListener('click', this.onContentClick_.bind(this)); 326 327 this.header_ = document.querySelector('#header'); 328 this.toolbar_ = document.querySelector('#toolbar'); 329 330 var preventDefault = function(event) { event.preventDefault(); }; 331 332 var minimizeButton = util.createChild(this.header_, 333 'minimize-button tool dimmable', 334 'button'); 335 minimizeButton.tabIndex = -1; 336 minimizeButton.addEventListener('click', this.onMinimize_.bind(this)); 337 minimizeButton.addEventListener('mousedown', preventDefault); 338 339 var maximizeButton = util.createChild(this.header_, 340 'maximize-button tool dimmable', 341 'button'); 342 maximizeButton.tabIndex = -1; 343 maximizeButton.addEventListener('click', this.onMaximize_.bind(this)); 344 maximizeButton.addEventListener('mousedown', preventDefault); 345 346 var closeButton = util.createChild(this.header_, 347 'close-button tool dimmable', 348 'button'); 349 closeButton.tabIndex = -1; 350 closeButton.addEventListener('click', this.onClose_.bind(this)); 351 closeButton.addEventListener('mousedown', preventDefault); 352 353 this.filenameSpacer_ = this.toolbar_.querySelector('.filename-spacer'); 354 this.filenameEdit_ = util.createChild(this.filenameSpacer_, 355 'namebox', 'input'); 356 357 this.filenameEdit_.setAttribute('type', 'text'); 358 this.filenameEdit_.addEventListener('blur', 359 this.onFilenameEditBlur_.bind(this)); 360 361 this.filenameEdit_.addEventListener('focus', 362 this.onFilenameFocus_.bind(this)); 363 364 this.filenameEdit_.addEventListener('keydown', 365 this.onFilenameEditKeydown_.bind(this)); 366 367 var middleSpacer = this.filenameSpacer_ = 368 this.toolbar_.querySelector('.middle-spacer'); 369 var buttonSpacer = this.toolbar_.querySelector('button-spacer'); 370 371 this.prompt_ = new ImageEditor.Prompt(this.container_, strf); 372 373 this.modeButton_ = this.toolbar_.querySelector('button.mode'); 374 this.modeButton_.addEventListener('click', this.toggleMode_.bind(this, null)); 375 376 this.mosaicMode_ = new MosaicMode(content, 377 this.dataModel_, 378 this.selectionModel_, 379 this.volumeManager_, 380 this.toggleMode_.bind(this, null)); 381 382 this.slideMode_ = new SlideMode(this.container_, 383 content, 384 this.toolbar_, 385 this.prompt_, 386 this.dataModel_, 387 this.selectionModel_, 388 this.context_, 389 this.volumeManager_, 390 this.toggleMode_.bind(this), 391 str); 392 393 this.slideMode_.addEventListener('image-displayed', function() { 394 cr.dispatchSimpleEvent(this, 'image-displayed'); 395 }.bind(this)); 396 this.slideMode_.addEventListener('image-saved', function() { 397 cr.dispatchSimpleEvent(this, 'image-saved'); 398 }.bind(this)); 399 400 var deleteButton = this.initToolbarButton_('delete', 'GALLERY_DELETE'); 401 deleteButton.addEventListener('click', this.delete_.bind(this)); 402 403 this.shareButton_ = this.initToolbarButton_('share', 'GALLERY_SHARE'); 404 this.shareButton_.addEventListener( 405 'click', this.onShareButtonClick_.bind(this)); 406 407 this.dataModel_.addEventListener('splice', this.onSplice_.bind(this)); 408 this.dataModel_.addEventListener('content', this.onContentChange_.bind(this)); 409 410 this.selectionModel_.addEventListener('change', this.onSelection_.bind(this)); 411 this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this)); 412 413 this.shareDialog_ = new ShareDialog(this.container_); 414 }; 415 416 /** 417 * Initializes a toolbar button. 418 * 419 * @param {string} className Class to add. 420 * @param {string} title Button title. 421 * @return {!HTMLElement} Newly created button. 422 * @private 423 */ 424 Gallery.prototype.initToolbarButton_ = function(className, title) { 425 var button = this.toolbar_.querySelector('button.' + className); 426 button.title = str(title); 427 return button; 428 }; 429 430 /** 431 * Loads the content. 432 * 433 * @param {!Array.<Entry>} entries Array of entries. 434 * @param {!Array.<Entry>} selectedEntries Array of selected entries. 435 */ 436 Gallery.prototype.load = function(entries, selectedEntries) { 437 // Obtains max chank size. 438 var maxChunkSize = 20; 439 var volumeInfo = this.volumeManager_.getVolumeInfo(entries[0]); 440 if (volumeInfo && 441 volumeInfo.volumeType === VolumeManagerCommon.VolumeType.MTP) { 442 maxChunkSize = 1; 443 } 444 if (volumeInfo.isReadOnly) 445 this.context_.readonlyDirName = volumeInfo.label; 446 447 // Make loading list. 448 var entrySet = {}; 449 for (var i = 0; i < entries.length; i++) { 450 var entry = entries[i]; 451 entrySet[entry.toURL()] = { 452 entry: entry, 453 selected: false, 454 index: i 455 }; 456 } 457 for (var i = 0; i < selectedEntries.length; i++) { 458 var entry = selectedEntries[i]; 459 entrySet[entry.toURL()] = { 460 entry: entry, 461 selected: true, 462 index: i 463 }; 464 } 465 var loadingList = []; 466 for (var url in entrySet) { 467 loadingList.push(entrySet[url]); 468 } 469 loadingList = loadingList.sort(function(a, b) { 470 if (a.selected && !b.selected) 471 return -1; 472 else if (!a.selected && b.selected) 473 return 1; 474 else 475 return a.index - b.index; 476 }); 477 478 // Load entries. 479 // Use the self variable capture-by-closure because it is faster than bind. 480 var self = this; 481 var loadChunk = function(firstChunk) { 482 // Extract chunk. 483 var chunk = loadingList.splice(0, maxChunkSize); 484 if (!chunk.length) 485 return; 486 487 return new Promise(function(fulfill) { 488 // Obtains metadata for chunk. 489 var entries = chunk.map(function(chunkItem) { 490 return chunkItem.entry; 491 }); 492 self.metadataCache_.get(entries, Gallery.METADATA_TYPE, fulfill); 493 }).then(function(metadataList) { 494 if (chunk.length !== metadataList.length) 495 return Promise.reject('Failed to load metadata.'); 496 497 // Add items to the model. 498 var items = []; 499 chunk.forEach(function(chunkItem, index) { 500 var locationInfo = self.volumeManager_.getLocationInfo(chunkItem.entry); 501 if (!locationInfo) // Skip the item, since gone. 502 return; 503 var clonedMetadata = MetadataCache.cloneMetadata(metadataList[index]); 504 items.push(new Gallery.Item( 505 chunkItem.entry, 506 locationInfo, 507 clonedMetadata, 508 self.metadataCache_, 509 /* original */ true)); 510 }); 511 self.dataModel_.push.apply(self.dataModel_, items); 512 513 // Apply the selection. 514 var selectionUpdated = false; 515 for (var i = 0; i < chunk.length; i++) { 516 if (!chunk[i].selected) 517 continue; 518 var index = self.dataModel_.indexOf(items[i]); 519 if (index < 0) 520 continue; 521 self.selectionModel_.setIndexSelected(index, true); 522 selectionUpdated = true; 523 } 524 if (selectionUpdated) 525 self.onSelection_(); 526 527 // Init modes after the first chunk is loaded. 528 if (firstChunk) { 529 // Determine the initial mode. 530 var shouldShowMosaic = selectedEntries.length > 1 || 531 (self.context_.pageState && 532 self.context_.pageState.gallery === 'mosaic'); 533 self.setCurrentMode_( 534 shouldShowMosaic ? self.mosaicMode_ : self.slideMode_); 535 536 // Init mosaic mode. 537 var mosaic = self.mosaicMode_.getMosaic(); 538 mosaic.init(); 539 540 // Do the initialization for each mode. 541 if (shouldShowMosaic) { 542 mosaic.show(); 543 self.inactivityWatcher_.check(); // Show the toolbar. 544 cr.dispatchSimpleEvent(self, 'loaded'); 545 } else { 546 self.slideMode_.enter( 547 null, 548 function() { 549 // Flash the toolbar briefly to show it is there. 550 self.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT); 551 }, 552 function() { 553 cr.dispatchSimpleEvent(self, 'loaded'); 554 }); 555 } 556 } 557 558 // Continue to load chunks. 559 return loadChunk(/* firstChunk */ false); 560 }); 561 }; 562 loadChunk(/* firstChunk */ true).catch(function(error) { 563 console.error(error.stack || error); 564 }); 565 }; 566 567 /** 568 * Handles user's 'Close' action. 569 * @private 570 */ 571 Gallery.prototype.onClose_ = function() { 572 this.executeWhenReady(this.context_.onClose); 573 }; 574 575 /** 576 * Handles user's 'Maximize' action (Escape or a click on the X icon). 577 * @private 578 */ 579 Gallery.prototype.onMaximize_ = function() { 580 this.executeWhenReady(this.context_.onMaximize); 581 }; 582 583 /** 584 * Handles user's 'Maximize' action (Escape or a click on the X icon). 585 * @private 586 */ 587 Gallery.prototype.onMinimize_ = function() { 588 this.executeWhenReady(this.context_.onMinimize); 589 }; 590 591 /** 592 * Executes a function when the editor is done with the modifications. 593 * @param {function} callback Function to execute. 594 */ 595 Gallery.prototype.executeWhenReady = function(callback) { 596 this.currentMode_.executeWhenReady(callback); 597 }; 598 599 /** 600 * @return {Object} File manager private API. 601 */ 602 Gallery.getFileManagerPrivate = function() { 603 return chrome.fileManagerPrivate || window.top.chrome.fileManagerPrivate; 604 }; 605 606 /** 607 * @return {boolean} True if some tool is currently active. 608 */ 609 Gallery.prototype.hasActiveTool = function() { 610 return (this.currentMode_ && this.currentMode_.hasActiveTool()) || 611 this.isRenaming_(); 612 }; 613 614 /** 615 * External user action event handler. 616 * @private 617 */ 618 Gallery.prototype.onUserAction_ = function() { 619 // Show the toolbar and hide it after the default timeout. 620 this.inactivityWatcher_.kick(); 621 }; 622 623 /** 624 * Sets the current mode, update the UI. 625 * @param {Object} mode Current mode. 626 * @private 627 */ 628 Gallery.prototype.setCurrentMode_ = function(mode) { 629 if (mode !== this.slideMode_ && mode !== this.mosaicMode_) 630 console.error('Invalid Gallery mode'); 631 632 this.currentMode_ = mode; 633 this.container_.setAttribute('mode', this.currentMode_.getName()); 634 this.updateSelectionAndState_(); 635 this.updateButtons_(); 636 }; 637 638 /** 639 * Mode toggle event handler. 640 * @param {function=} opt_callback Callback. 641 * @param {Event=} opt_event Event that caused this call. 642 * @private 643 */ 644 Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) { 645 if (!this.modeButton_) 646 return; 647 648 if (this.changingMode_) // Do not re-enter while changing the mode. 649 return; 650 651 if (opt_event) 652 this.onUserAction_(); 653 654 this.changingMode_ = true; 655 656 var onModeChanged = function() { 657 this.changingMode_ = false; 658 if (opt_callback) opt_callback(); 659 }.bind(this); 660 661 var tileIndex = Math.max(0, this.selectionModel_.selectedIndex); 662 663 var mosaic = this.mosaicMode_.getMosaic(); 664 var tileRect = mosaic.getTileRect(tileIndex); 665 666 if (this.currentMode_ === this.slideMode_) { 667 this.setCurrentMode_(this.mosaicMode_); 668 mosaic.transform( 669 tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */); 670 this.slideMode_.leave( 671 tileRect, 672 function() { 673 // Animate back to normal position. 674 mosaic.transform(); 675 mosaic.show(); 676 onModeChanged(); 677 }.bind(this)); 678 } else { 679 this.setCurrentMode_(this.slideMode_); 680 this.slideMode_.enter( 681 tileRect, 682 function() { 683 // Animate to zoomed position. 684 mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect()); 685 mosaic.hide(); 686 }.bind(this), 687 onModeChanged); 688 } 689 }; 690 691 /** 692 * Deletes the selected items. 693 * @private 694 */ 695 Gallery.prototype.delete_ = function() { 696 this.onUserAction_(); 697 698 // Clone the sorted selected indexes array. 699 var indexesToRemove = this.selectionModel_.selectedIndexes.slice(); 700 if (!indexesToRemove.length) 701 return; 702 703 /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */ 704 705 var itemsToRemove = this.getSelectedItems(); 706 var plural = itemsToRemove.length > 1; 707 var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName(); 708 709 function deleteNext() { 710 if (!itemsToRemove.length) 711 return; // All deleted. 712 713 var entry = itemsToRemove.pop().getEntry(); 714 entry.remove(deleteNext, function() { 715 util.flog('Error deleting: ' + entry.name, deleteNext); 716 }); 717 } 718 719 // Prevent the Gallery from handling Esc and Enter. 720 this.document_.body.removeEventListener('keydown', this.keyDownBound_); 721 var restoreListener = function() { 722 this.document_.body.addEventListener('keydown', this.keyDownBound_); 723 }.bind(this); 724 725 726 var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_); 727 confirm.setOkLabel(str('DELETE_BUTTON_LABEL')); 728 confirm.show(strf(plural ? 729 'GALLERY_CONFIRM_DELETE_SOME' : 'GALLERY_CONFIRM_DELETE_ONE', param), 730 function() { 731 restoreListener(); 732 this.selectionModel_.unselectAll(); 733 this.selectionModel_.leadIndex = -1; 734 // Remove items from the data model, starting from the highest index. 735 while (indexesToRemove.length) 736 this.dataModel_.splice(indexesToRemove.pop(), 1); 737 // Delete actual files. 738 deleteNext(); 739 }.bind(this), 740 function() { 741 // Restore the listener after a timeout so that ESC is processed. 742 setTimeout(restoreListener, 0); 743 }); 744 }; 745 746 /** 747 * @return {Array.<Gallery.Item>} Current selection. 748 */ 749 Gallery.prototype.getSelectedItems = function() { 750 return this.selectionModel_.selectedIndexes.map( 751 this.dataModel_.item.bind(this.dataModel_)); 752 }; 753 754 /** 755 * @return {Array.<Entry>} Array of currently selected entries. 756 */ 757 Gallery.prototype.getSelectedEntries = function() { 758 return this.selectionModel_.selectedIndexes.map(function(index) { 759 return this.dataModel_.item(index).getEntry(); 760 }.bind(this)); 761 }; 762 763 /** 764 * @return {?Gallery.Item} Current single selection. 765 */ 766 Gallery.prototype.getSingleSelectedItem = function() { 767 var items = this.getSelectedItems(); 768 if (items.length > 1) { 769 console.error('Unexpected multiple selection'); 770 return null; 771 } 772 return items[0]; 773 }; 774 775 /** 776 * Selection change event handler. 777 * @private 778 */ 779 Gallery.prototype.onSelection_ = function() { 780 this.updateSelectionAndState_(); 781 }; 782 783 /** 784 * Data model splice event handler. 785 * @private 786 */ 787 Gallery.prototype.onSplice_ = function() { 788 this.selectionModel_.adjustLength(this.dataModel_.length); 789 }; 790 791 /** 792 * Content change event handler. 793 * @param {Event} event Event. 794 * @private 795 */ 796 Gallery.prototype.onContentChange_ = function(event) { 797 var index = this.dataModel_.indexOf(event.item); 798 if (index !== this.selectionModel_.selectedIndex) 799 console.error('Content changed for unselected item'); 800 this.updateSelectionAndState_(); 801 }; 802 803 /** 804 * Keydown handler. 805 * 806 * @param {Event} event Event. 807 * @private 808 */ 809 Gallery.prototype.onKeyDown_ = function(event) { 810 if (this.currentMode_.onKeyDown(event)) 811 return; 812 813 switch (util.getKeyModifiers(event) + event.keyIdentifier) { 814 case 'U+0008': // Backspace. 815 // The default handler would call history.back and close the Gallery. 816 event.preventDefault(); 817 break; 818 819 case 'U+004D': // 'm' switches between Slide and Mosaic mode. 820 this.toggleMode_(null, event); 821 break; 822 823 case 'U+0056': // 'v' 824 case 'MediaPlayPause': 825 this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event); 826 break; 827 828 case 'U+007F': // Delete 829 case 'Shift-U+0033': // Shift+'3' (Delete key might be missing). 830 case 'U+0044': // 'd' 831 this.delete_(); 832 break; 833 } 834 }; 835 836 // Name box and rename support. 837 838 /** 839 * Updates the UI related to the selected item and the persistent state. 840 * 841 * @private 842 */ 843 Gallery.prototype.updateSelectionAndState_ = function() { 844 var numSelectedItems = this.selectionModel_.selectedIndexes.length; 845 var selectedEntryURL = null; 846 847 // If it's selecting something, update the variable values. 848 if (numSelectedItems) { 849 // Obtains selected item. 850 var selectedItem = 851 this.dataModel_.item(this.selectionModel_.selectedIndex); 852 this.selectedEntry_ = selectedItem.getEntry(); 853 selectedEntryURL = this.selectedEntry_.toURL(); 854 855 // Update cache. 856 selectedItem.touch(); 857 this.dataModel_.evictCache(); 858 859 // Update the title and the display name. 860 if (numSelectedItems === 1) { 861 document.title = this.selectedEntry_.name; 862 this.filenameEdit_.disabled = selectedItem.getLocationInfo().isReadOnly; 863 this.filenameEdit_.value = 864 ImageUtil.getDisplayNameFromName(this.selectedEntry_.name); 865 this.shareButton_.hidden = !selectedItem.getLocationInfo().isDriveBased; 866 } else { 867 if (this.context_.curDirEntry) { 868 // If the Gallery was opened on search results the search query will not 869 // be recorded in the app state and the relaunch will just open the 870 // gallery in the curDirEntry directory. 871 document.title = this.context_.curDirEntry.name; 872 } else { 873 document.title = ''; 874 } 875 this.filenameEdit_.disabled = true; 876 this.filenameEdit_.value = 877 strf('GALLERY_ITEMS_SELECTED', numSelectedItems); 878 this.shareButton_.hidden = true; 879 } 880 } else { 881 document.title = ''; 882 this.filenameEdit_.disabled = true; 883 this.filenameEdit_.value = ''; 884 this.shareButton_.hidden = true; 885 } 886 887 util.updateAppState( 888 null, // Keep the current directory. 889 selectedEntryURL, // Update the selection. 890 {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')}); 891 }; 892 893 /** 894 * Click event handler on filename edit box 895 * @private 896 */ 897 Gallery.prototype.onFilenameFocus_ = function() { 898 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); 899 this.filenameEdit_.originalValue = this.filenameEdit_.value; 900 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); 901 this.onUserAction_(); 902 }; 903 904 /** 905 * Blur event handler on filename edit box. 906 * 907 * @param {Event} event Blur event. 908 * @return {Promise} Promise fulfilled on renaming completed. 909 * @private 910 */ 911 Gallery.prototype.onFilenameEditBlur_ = function(event) { 912 var item = this.getSingleSelectedItem(); 913 if (item) { 914 var oldEntry = item.getEntry(); 915 916 item.rename(this.filenameEdit_.value).then(function() { 917 var event = new Event('content'); 918 event.item = item; 919 event.oldEntry = oldEntry; 920 event.metadata = null; // Metadata unchanged. 921 this.dataModel_.dispatchEvent(event); 922 }.bind(this), function(error) { 923 if (error === 'NOT_CHANGED') 924 return Promise.resolve(); 925 this.filenameEdit_.value = 926 ImageUtil.getDisplayNameFromName(item.getEntry().name); 927 this.filenameEdit_.focus(); 928 if (typeof error === 'string') 929 this.prompt_.showStringAt('center', error, 5000); 930 else 931 return Promise.reject(error); 932 }.bind(this)).catch(function(error) { 933 console.error(error.stack || error); 934 }); 935 } 936 937 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); 938 this.onUserAction_(); 939 return Promise.resolve(); 940 }; 941 942 /** 943 * Keydown event handler on filename edit box 944 * @private 945 */ 946 Gallery.prototype.onFilenameEditKeydown_ = function() { 947 switch (event.keyCode) { 948 case 27: // Escape 949 this.filenameEdit_.value = this.filenameEdit_.originalValue; 950 this.filenameEdit_.blur(); 951 break; 952 953 case 13: // Enter 954 this.filenameEdit_.blur(); 955 break; 956 } 957 event.stopPropagation(); 958 }; 959 960 /** 961 * @return {boolean} True if file renaming is currently in progress. 962 * @private 963 */ 964 Gallery.prototype.isRenaming_ = function() { 965 return this.filenameSpacer_.hasAttribute('renaming'); 966 }; 967 968 /** 969 * Content area click handler. 970 * @private 971 */ 972 Gallery.prototype.onContentClick_ = function() { 973 this.filenameEdit_.blur(); 974 }; 975 976 /** 977 * Share button handler. 978 * @private 979 */ 980 Gallery.prototype.onShareButtonClick_ = function() { 981 var item = this.getSingleSelectedItem(); 982 if (!item) 983 return; 984 this.shareDialog_.show(item.getEntry(), function() {}); 985 }; 986 987 /** 988 * Updates thumbnails. 989 * @private 990 */ 991 Gallery.prototype.updateThumbnails_ = function() { 992 if (this.currentMode_ === this.slideMode_) 993 this.slideMode_.updateThumbnails(); 994 995 if (this.mosaicMode_) { 996 var mosaic = this.mosaicMode_.getMosaic(); 997 if (mosaic.isInitialized()) 998 mosaic.reload(); 999 } 1000 }; 1001 1002 /** 1003 * Updates buttons. 1004 * @private 1005 */ 1006 Gallery.prototype.updateButtons_ = function() { 1007 if (this.modeButton_) { 1008 var oppositeMode = 1009 this.currentMode_ === this.slideMode_ ? this.mosaicMode_ : 1010 this.slideMode_; 1011 this.modeButton_.title = str(oppositeMode.getTitle()); 1012 } 1013 }; 1014 1015 /** 1016 * Singleton gallery. 1017 * @type {Gallery} 1018 */ 1019 var gallery = null; 1020 1021 /** 1022 * Initialize the window. 1023 * @param {Object} backgroundComponents Background components. 1024 */ 1025 window.initialize = function(backgroundComponents) { 1026 window.loadTimeData.data = backgroundComponents.stringData; 1027 gallery = new Gallery(backgroundComponents.volumeManager); 1028 }; 1029 1030 /** 1031 * Loads entries. 1032 * @param {!Array.<Entry>} entries Array of entries. 1033 * @param {!Array.<Entry>} selectedEntries Array of selected entries. 1034 */ 1035 window.loadEntries = function(entries, selectedEntries) { 1036 gallery.load(entries, selectedEntries); 1037 }; 1038