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