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