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 5cr.define('options', function() { 6 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; 7 /** @const */ var Grid = cr.ui.Grid; 8 /** @const */ var GridItem = cr.ui.GridItem; 9 /** @const */ var GridSelectionController = cr.ui.GridSelectionController; 10 /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; 11 12 /** 13 * Interval between consecutive camera presence checks in msec. 14 * @const 15 */ 16 var CAMERA_CHECK_INTERVAL_MS = 3000; 17 18 /** 19 * Interval between consecutive camera liveness checks in msec. 20 * @const 21 */ 22 var CAMERA_LIVENESS_CHECK_MS = 3000; 23 24 /** 25 * Number of frames recorded by takeVideo(). 26 * @const 27 */ 28 var RECORD_FRAMES = 48; 29 30 /** 31 * FPS at which camera stream is recorded. 32 * @const 33 */ 34 var RECORD_FPS = 16; 35 36 /** 37 * Dimensions for camera capture. 38 * @const 39 */ 40 var CAPTURE_SIZE = { 41 height: 480, 42 width: 480 43 }; 44 45 /** 46 * Path for internal URLs. 47 * @const 48 */ 49 var CHROME_THEME_PATH = 'chrome://theme'; 50 51 /** 52 * Creates a new user images grid item. 53 * @param {{url: string, title: string=, decorateFn: function=, 54 * clickHandler: function=}} imageInfo User image URL, optional title, 55 * decorator callback and click handler. 56 * @constructor 57 * @extends {cr.ui.GridItem} 58 */ 59 function UserImagesGridItem(imageInfo) { 60 var el = new GridItem(imageInfo); 61 el.__proto__ = UserImagesGridItem.prototype; 62 return el; 63 } 64 65 UserImagesGridItem.prototype = { 66 __proto__: GridItem.prototype, 67 68 /** @override */ 69 decorate: function() { 70 GridItem.prototype.decorate.call(this); 71 var imageEl = cr.doc.createElement('img'); 72 // Force 1x scale for chrome://theme URLs. Grid elements are much smaller 73 // than actual images so there is no need in full scale on HDPI. 74 var url = this.dataItem.url; 75 if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH) 76 imageEl.src = this.dataItem.url + '@1x'; 77 else 78 imageEl.src = this.dataItem.url; 79 imageEl.title = this.dataItem.title || ''; 80 if (typeof this.dataItem.clickHandler == 'function') 81 imageEl.addEventListener('mousedown', this.dataItem.clickHandler); 82 // Remove any garbage added by GridItem and ListItem decorators. 83 this.textContent = ''; 84 this.appendChild(imageEl); 85 if (typeof this.dataItem.decorateFn == 'function') 86 this.dataItem.decorateFn(this); 87 this.setAttribute('role', 'option'); 88 this.oncontextmenu = function(e) { e.preventDefault(); }; 89 } 90 }; 91 92 /** 93 * Creates a selection controller that wraps selection on grid ends 94 * and translates Enter presses into 'activate' events. 95 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to 96 * interact with. 97 * @param {cr.ui.Grid} grid The grid to interact with. 98 * @constructor 99 * @extends {cr.ui.GridSelectionController} 100 */ 101 function UserImagesGridSelectionController(selectionModel, grid) { 102 GridSelectionController.call(this, selectionModel, grid); 103 } 104 105 UserImagesGridSelectionController.prototype = { 106 __proto__: GridSelectionController.prototype, 107 108 /** @override */ 109 getIndexBefore: function(index) { 110 var result = 111 GridSelectionController.prototype.getIndexBefore.call(this, index); 112 return result == -1 ? this.getLastIndex() : result; 113 }, 114 115 /** @override */ 116 getIndexAfter: function(index) { 117 var result = 118 GridSelectionController.prototype.getIndexAfter.call(this, index); 119 return result == -1 ? this.getFirstIndex() : result; 120 }, 121 122 /** @override */ 123 handleKeyDown: function(e) { 124 if (e.keyIdentifier == 'Enter') 125 cr.dispatchSimpleEvent(this.grid_, 'activate'); 126 else 127 GridSelectionController.prototype.handleKeyDown.call(this, e); 128 } 129 }; 130 131 /** 132 * Creates a new user images grid element. 133 * @param {Object=} opt_propertyBag Optional properties. 134 * @constructor 135 * @extends {cr.ui.Grid} 136 */ 137 var UserImagesGrid = cr.ui.define('grid'); 138 139 UserImagesGrid.prototype = { 140 __proto__: Grid.prototype, 141 142 /** @override */ 143 createSelectionController: function(sm) { 144 return new UserImagesGridSelectionController(sm, this); 145 }, 146 147 /** @override */ 148 decorate: function() { 149 Grid.prototype.decorate.call(this); 150 this.dataModel = new ArrayDataModel([]); 151 this.itemConstructor = UserImagesGridItem; 152 this.selectionModel = new ListSingleSelectionModel(); 153 this.inProgramSelection_ = false; 154 this.addEventListener('dblclick', this.handleDblClick_.bind(this)); 155 this.addEventListener('change', this.handleChange_.bind(this)); 156 this.setAttribute('role', 'listbox'); 157 this.autoExpands = true; 158 }, 159 160 /** 161 * Handles double click on the image grid. 162 * @param {Event} e Double click Event. 163 * @private 164 */ 165 handleDblClick_: function(e) { 166 // If a child element is double-clicked and not the grid itself, handle 167 // this as 'Enter' keypress. 168 if (e.target != this) 169 cr.dispatchSimpleEvent(this, 'activate'); 170 }, 171 172 /** 173 * Handles selection change. 174 * @param {Event} e Double click Event. 175 * @private 176 */ 177 handleChange_: function(e) { 178 if (this.selectedItem === null) 179 return; 180 181 var oldSelectionType = this.selectionType; 182 183 // Update current selection type. 184 this.selectionType = this.selectedItem.type; 185 186 // Show grey silhouette with the same border as stock images. 187 if (/^chrome:\/\/theme\//.test(this.selectedItemUrl)) 188 this.previewElement.classList.add('default-image'); 189 190 this.updatePreview_(); 191 192 var e = new Event('select'); 193 e.oldSelectionType = oldSelectionType; 194 this.dispatchEvent(e); 195 }, 196 197 /** 198 * Updates the preview image, if present. 199 * @private 200 */ 201 updatePreview_: function() { 202 var url = this.selectedItemUrl; 203 if (url && this.previewImage_) { 204 if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH) 205 this.previewImage_.src = url + '@' + window.devicePixelRatio + 'x'; 206 else 207 this.previewImage_.src = url; 208 } 209 }, 210 211 /** 212 * Start camera presence check. 213 * @private 214 */ 215 checkCameraPresence_: function() { 216 if (this.cameraPresentCheckTimer_) { 217 window.clearTimeout(this.cameraPresentCheckTimer_); 218 this.cameraPresentCheckTimer_ = null; 219 } 220 if (!this.cameraVideo_) 221 return; 222 chrome.send('checkCameraPresence'); 223 }, 224 225 /** 226 * Whether a camera is present or not. 227 * @type {boolean} 228 */ 229 get cameraPresent() { 230 return this.cameraPresent_; 231 }, 232 set cameraPresent(value) { 233 this.cameraPresent_ = value; 234 if (this.cameraLive) 235 this.cameraImage = null; 236 // Repeat the check after some time. 237 this.cameraPresentCheckTimer_ = window.setTimeout( 238 this.checkCameraPresence_.bind(this), 239 CAMERA_CHECK_INTERVAL_MS); 240 }, 241 242 /** 243 * Whether camera is actually streaming video. May be |false| even when 244 * camera is present and shown but still initializing. 245 * @type {boolean} 246 */ 247 get cameraOnline() { 248 return this.previewElement.classList.contains('online'); 249 }, 250 set cameraOnline(value) { 251 this.previewElement.classList[value ? 'add' : 'remove']('online'); 252 if (value) { 253 this.cameraLiveCheckTimer_ = window.setInterval( 254 this.checkCameraLive_.bind(this), CAMERA_LIVENESS_CHECK_MS); 255 } else if (this.cameraLiveCheckTimer_) { 256 window.clearInterval(this.cameraLiveCheckTimer_); 257 this.cameraLiveCheckTimer_ = null; 258 } 259 }, 260 261 /** 262 * Tries to starts camera stream capture. 263 * @param {function(): boolean} onAvailable Callback that is called if 264 * camera is available. If it returns |true|, capture is started 265 * immediately. 266 */ 267 startCamera: function(onAvailable, onAbsent) { 268 this.stopCamera(); 269 this.cameraStartInProgress_ = true; 270 navigator.webkitGetUserMedia( 271 {video: true}, 272 this.handleCameraAvailable_.bind(this, onAvailable), 273 this.handleCameraAbsent_.bind(this)); 274 }, 275 276 /** 277 * Stops camera capture, if it's currently active. 278 */ 279 stopCamera: function() { 280 this.cameraOnline = false; 281 if (this.cameraVideo_) 282 this.cameraVideo_.src = ''; 283 if (this.cameraStream_) 284 this.cameraStream_.stop(); 285 // Cancel any pending getUserMedia() checks. 286 this.cameraStartInProgress_ = false; 287 }, 288 289 /** 290 * Handles successful camera check. 291 * @param {function(): boolean} onAvailable Callback to call. If it returns 292 * |true|, capture is started immediately. 293 * @param {MediaStream} stream Stream object as returned by getUserMedia. 294 * @private 295 */ 296 handleCameraAvailable_: function(onAvailable, stream) { 297 if (this.cameraStartInProgress_ && onAvailable()) { 298 this.cameraVideo_.src = window.webkitURL.createObjectURL(stream); 299 this.cameraStream_ = stream; 300 } else { 301 stream.stop(); 302 } 303 this.cameraStartInProgress_ = false; 304 }, 305 306 /** 307 * Handles camera check failure. 308 * @param {NavigatorUserMediaError=} err Error object. 309 * @private 310 */ 311 handleCameraAbsent_: function(err) { 312 this.cameraPresent = false; 313 this.cameraOnline = false; 314 this.cameraStartInProgress_ = false; 315 }, 316 317 /** 318 * Handles successful camera capture start. 319 * @private 320 */ 321 handleVideoStarted_: function() { 322 this.cameraOnline = true; 323 this.handleVideoUpdate_(); 324 }, 325 326 /** 327 * Handles camera stream update. Called regularly (at rate no greater then 328 * 4/sec) while camera stream is live. 329 * @private 330 */ 331 handleVideoUpdate_: function() { 332 this.lastFrameTime_ = new Date().getTime(); 333 }, 334 335 /** 336 * Checks if camera is still live by comparing the timestamp of the last 337 * 'timeupdate' event with the current time. 338 * @private 339 */ 340 checkCameraLive_: function() { 341 if (new Date().getTime() - this.lastFrameTime_ > 342 CAMERA_LIVENESS_CHECK_MS) { 343 this.cameraPresent = false; 344 } 345 }, 346 347 /** 348 * Type of the selected image (one of 'default', 'profile', 'camera'). 349 * Setting it will update class list of |previewElement|. 350 * @type {string} 351 */ 352 get selectionType() { 353 return this.selectionType_; 354 }, 355 set selectionType(value) { 356 this.selectionType_ = value; 357 var previewClassList = this.previewElement.classList; 358 previewClassList[value == 'default' ? 'add' : 'remove']('default-image'); 359 previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image'); 360 previewClassList[value == 'camera' ? 'add' : 'remove']('camera'); 361 362 var setFocusIfLost = function() { 363 // Set focus to the grid, if focus is not on UI. 364 if (!document.activeElement || 365 document.activeElement.tagName == 'BODY') { 366 $('user-image-grid').focus(); 367 } 368 } 369 // Timeout guarantees processing AFTER style changes display attribute. 370 setTimeout(setFocusIfLost, 0); 371 }, 372 373 /** 374 * Current image captured from camera as data URL. Setting to null will 375 * return to the live camera stream. 376 * @type {string=} 377 */ 378 get cameraImage() { 379 return this.cameraImage_; 380 }, 381 set cameraImage(imageUrl) { 382 this.cameraLive = !imageUrl; 383 if (this.cameraPresent && !imageUrl) 384 imageUrl = UserImagesGrid.ButtonImages.TAKE_PHOTO; 385 if (imageUrl) { 386 this.cameraImage_ = this.cameraImage_ ? 387 this.updateItem(this.cameraImage_, imageUrl, this.cameraTitle_) : 388 this.addItem(imageUrl, this.cameraTitle_, undefined, 0); 389 this.cameraImage_.type = 'camera'; 390 } else { 391 this.removeItem(this.cameraImage_); 392 this.cameraImage_ = null; 393 } 394 }, 395 396 /** 397 * Updates the titles for the camera element. 398 * @param {string} placeholderTitle Title when showing a placeholder. 399 * @param {string} capturedImageTitle Title when showing a captured photo. 400 */ 401 setCameraTitles: function(placeholderTitle, capturedImageTitle) { 402 this.placeholderTitle_ = placeholderTitle; 403 this.capturedImageTitle_ = capturedImageTitle; 404 this.cameraTitle_ = this.placeholderTitle_; 405 }, 406 407 /** 408 * True when camera is in live mode (i.e. no still photo selected). 409 * @type {boolean} 410 */ 411 get cameraLive() { 412 return this.cameraLive_; 413 }, 414 set cameraLive(value) { 415 this.cameraLive_ = value; 416 this.previewElement.classList[value ? 'add' : 'remove']('live'); 417 }, 418 419 /** 420 * Should only be queried from the 'change' event listener, true if the 421 * change event was triggered by a programmatical selection change. 422 * @type {boolean} 423 */ 424 get inProgramSelection() { 425 return this.inProgramSelection_; 426 }, 427 428 /** 429 * URL of the image selected. 430 * @type {string?} 431 */ 432 get selectedItemUrl() { 433 var selectedItem = this.selectedItem; 434 return selectedItem ? selectedItem.url : null; 435 }, 436 set selectedItemUrl(url) { 437 for (var i = 0, el; el = this.dataModel.item(i); i++) { 438 if (el.url === url) 439 this.selectedItemIndex = i; 440 } 441 }, 442 443 /** 444 * Set index to the image selected. 445 * @type {number} index The index of selected image. 446 */ 447 set selectedItemIndex(index) { 448 this.inProgramSelection_ = true; 449 this.selectionModel.selectedIndex = index; 450 this.inProgramSelection_ = false; 451 }, 452 453 /** @override */ 454 get selectedItem() { 455 var index = this.selectionModel.selectedIndex; 456 return index != -1 ? this.dataModel.item(index) : null; 457 }, 458 set selectedItem(selectedItem) { 459 var index = this.indexOf(selectedItem); 460 this.inProgramSelection_ = true; 461 this.selectionModel.selectedIndex = index; 462 this.selectionModel.leadIndex = index; 463 this.inProgramSelection_ = false; 464 }, 465 466 /** 467 * Element containing the preview image (the first IMG element) and the 468 * camera live stream (the first VIDEO element). 469 * @type {HTMLElement} 470 */ 471 get previewElement() { 472 // TODO(ivankr): temporary hack for non-HTML5 version. 473 return this.previewElement_ || this; 474 }, 475 set previewElement(value) { 476 this.previewElement_ = value; 477 this.previewImage_ = value.querySelector('img'); 478 this.cameraVideo_ = value.querySelector('video'); 479 this.cameraVideo_.addEventListener('canplay', 480 this.handleVideoStarted_.bind(this)); 481 this.cameraVideo_.addEventListener('timeupdate', 482 this.handleVideoUpdate_.bind(this)); 483 this.updatePreview_(); 484 // Initialize camera state and check for its presence. 485 this.cameraLive = true; 486 this.cameraPresent = false; 487 }, 488 489 /** 490 * Whether the camera live stream and photo should be flipped horizontally. 491 * If setting this property results in photo update, 'photoupdated' event 492 * will be fired with 'dataURL' property containing the photo encoded as 493 * a data URL 494 * @type {boolean} 495 */ 496 get flipPhoto() { 497 return this.flipPhoto_ || false; 498 }, 499 set flipPhoto(value) { 500 if (this.flipPhoto_ == value) 501 return; 502 this.flipPhoto_ = value; 503 this.previewElement.classList.toggle('flip-x', value); 504 /* TODO(merkulova): remove when webkit crbug.com/126479 is fixed. */ 505 this.flipPhotoElement.classList.toggle('flip-trick', value); 506 if (!this.cameraLive) { 507 // Flip current still photo. 508 var e = new Event('photoupdated'); 509 e.dataURL = this.flipPhoto ? 510 this.flipFrame_(this.previewImage_) : this.previewImage_.src; 511 this.dispatchEvent(e); 512 } 513 }, 514 515 /** 516 * Performs photo capture from the live camera stream. 'phototaken' event 517 * will be fired as soon as captured photo is available, with 'dataURL' 518 * property containing the photo encoded as a data URL. 519 * @return {boolean} Whether photo capture was successful. 520 */ 521 takePhoto: function() { 522 if (!this.cameraOnline) 523 return false; 524 var canvas = document.createElement('canvas'); 525 canvas.width = CAPTURE_SIZE.width; 526 canvas.height = CAPTURE_SIZE.height; 527 this.captureFrame_( 528 this.cameraVideo_, canvas.getContext('2d'), CAPTURE_SIZE); 529 // Preload image before displaying it. 530 var previewImg = new Image(); 531 previewImg.addEventListener('load', function(e) { 532 this.cameraTitle_ = this.capturedImageTitle_; 533 this.cameraImage = previewImg.src; 534 }.bind(this)); 535 previewImg.src = canvas.toDataURL('image/png'); 536 var e = new Event('phototaken'); 537 e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src; 538 this.dispatchEvent(e); 539 return true; 540 }, 541 542 /** 543 * Performs video capture from the live camera stream. 544 * @param {function=} opt_callback Callback that receives taken video as 545 * data URL of a vertically stacked PNG sprite. 546 */ 547 takeVideo: function(opt_callback) { 548 var canvas = document.createElement('canvas'); 549 canvas.width = CAPTURE_SIZE.width; 550 canvas.height = CAPTURE_SIZE.height * RECORD_FRAMES; 551 var ctx = canvas.getContext('2d'); 552 // Force canvas initialization to prevent FPS lag on the first frame. 553 ctx.fillRect(0, 0, 1, 1); 554 var captureData = { 555 callback: opt_callback, 556 canvas: canvas, 557 ctx: ctx, 558 frameNo: 0, 559 lastTimestamp: new Date().getTime() 560 }; 561 captureData.timer = window.setInterval( 562 this.captureVideoFrame_.bind(this, captureData), 1000 / RECORD_FPS); 563 }, 564 565 /** 566 * Discard current photo and return to the live camera stream. 567 */ 568 discardPhoto: function() { 569 this.cameraTitle_ = this.placeholderTitle_; 570 this.cameraImage = null; 571 }, 572 573 /** 574 * Capture a single still frame from a <video> element, placing it at the 575 * current drawing origin of a canvas context. 576 * @param {HTMLVideoElement} video Video element to capture from. 577 * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto. 578 * @param {{width: number, height: number}} destSize Capture size. 579 * @private 580 */ 581 captureFrame_: function(video, ctx, destSize) { 582 var width = video.videoWidth; 583 var height = video.videoHeight; 584 if (width < destSize.width || height < destSize.height) { 585 console.error('Video capture size too small: ' + 586 width + 'x' + height + '!'); 587 } 588 var src = {}; 589 if (width / destSize.width > height / destSize.height) { 590 // Full height, crop left/right. 591 src.height = height; 592 src.width = height * destSize.width / destSize.height; 593 } else { 594 // Full width, crop top/bottom. 595 src.width = width; 596 src.height = width * destSize.height / destSize.width; 597 } 598 src.x = (width - src.width) / 2; 599 src.y = (height - src.height) / 2; 600 ctx.drawImage(video, src.x, src.y, src.width, src.height, 601 0, 0, destSize.width, destSize.height); 602 }, 603 604 /** 605 * Flips frame horizontally. 606 * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source 607 * Frame to flip. 608 * @return {string} Flipped frame as data URL. 609 */ 610 flipFrame_: function(source) { 611 var canvas = document.createElement('canvas'); 612 canvas.width = CAPTURE_SIZE.width; 613 canvas.height = CAPTURE_SIZE.height; 614 var ctx = canvas.getContext('2d'); 615 ctx.translate(CAPTURE_SIZE.width, 0); 616 ctx.scale(-1.0, 1.0); 617 ctx.drawImage(source, 0, 0); 618 return canvas.toDataURL('image/png'); 619 }, 620 621 /** 622 * Capture next frame of the video being recorded after a takeVideo() call. 623 * @param {Object} data Property bag with the recorder details. 624 * @private 625 */ 626 captureVideoFrame_: function(data) { 627 var lastTimestamp = new Date().getTime(); 628 var delayMs = lastTimestamp - data.lastTimestamp; 629 console.error('Delay: ' + delayMs + ' (' + (1000 / delayMs + ' FPS)')); 630 data.lastTimestamp = lastTimestamp; 631 632 this.captureFrame_(this.cameraVideo_, data.ctx, CAPTURE_SIZE); 633 data.ctx.translate(0, CAPTURE_SIZE.height); 634 635 if (++data.frameNo == RECORD_FRAMES) { 636 window.clearTimeout(data.timer); 637 if (data.callback && typeof data.callback == 'function') 638 data.callback(data.canvas.toDataURL('image/png')); 639 } 640 }, 641 642 /** 643 * Adds new image to the user image grid. 644 * @param {string} src Image URL. 645 * @param {string=} opt_title Image tooltip. 646 * @param {function=} opt_clickHandler Image click handler. 647 * @param {number=} opt_position If given, inserts new image into 648 * that position (0-based) in image list. 649 * @param {function=} opt_decorateFn Function called with the list element 650 * as argument to do any final decoration. 651 * @return {!Object} Image data inserted into the data model. 652 */ 653 // TODO(ivankr): this function needs some argument list refactoring. 654 addItem: function(url, opt_title, opt_clickHandler, opt_position, 655 opt_decorateFn) { 656 var imageInfo = { 657 url: url, 658 title: opt_title, 659 clickHandler: opt_clickHandler, 660 decorateFn: opt_decorateFn 661 }; 662 this.inProgramSelection_ = true; 663 if (opt_position !== undefined) 664 this.dataModel.splice(opt_position, 0, imageInfo); 665 else 666 this.dataModel.push(imageInfo); 667 this.inProgramSelection_ = false; 668 return imageInfo; 669 }, 670 671 /** 672 * Returns index of an image in grid. 673 * @param {Object} imageInfo Image data returned from addItem() call. 674 * @return {number} Image index (0-based) or -1 if image was not found. 675 */ 676 indexOf: function(imageInfo) { 677 return this.dataModel.indexOf(imageInfo); 678 }, 679 680 /** 681 * Replaces an image in the grid. 682 * @param {Object} imageInfo Image data returned from addItem() call. 683 * @param {string} imageUrl New image URL. 684 * @param {string=} opt_title New image tooltip (if undefined, tooltip 685 * is left unchanged). 686 * @return {!Object} Image data of the added or updated image. 687 */ 688 updateItem: function(imageInfo, imageUrl, opt_title) { 689 var imageIndex = this.indexOf(imageInfo); 690 var wasSelected = this.selectionModel.selectedIndex == imageIndex; 691 this.removeItem(imageInfo); 692 var newInfo = this.addItem( 693 imageUrl, 694 opt_title === undefined ? imageInfo.title : opt_title, 695 imageInfo.clickHandler, 696 imageIndex, 697 imageInfo.decorateFn); 698 // Update image data with the reset of the keys from the old data. 699 for (k in imageInfo) { 700 if (!(k in newInfo)) 701 newInfo[k] = imageInfo[k]; 702 } 703 if (wasSelected) 704 this.selectedItem = newInfo; 705 return newInfo; 706 }, 707 708 /** 709 * Removes previously added image from the grid. 710 * @param {Object} imageInfo Image data returned from the addItem() call. 711 */ 712 removeItem: function(imageInfo) { 713 var index = this.indexOf(imageInfo); 714 if (index != -1) { 715 var wasSelected = this.selectionModel.selectedIndex == index; 716 this.inProgramSelection_ = true; 717 this.dataModel.splice(index, 1); 718 if (wasSelected) { 719 // If item removed was selected, select the item next to it. 720 this.selectedItem = this.dataModel.item( 721 Math.min(this.dataModel.length - 1, index)); 722 } 723 this.inProgramSelection_ = false; 724 } 725 }, 726 727 /** 728 * Forces re-display, size re-calculation and focuses grid. 729 */ 730 updateAndFocus: function() { 731 // Recalculate the measured item size. 732 this.measured_ = null; 733 this.columns = 0; 734 this.redraw(); 735 this.focus(); 736 } 737 }; 738 739 /** 740 * URLs of special button images. 741 * @enum {string} 742 */ 743 UserImagesGrid.ButtonImages = { 744 TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO', 745 CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE', 746 PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING' 747 }; 748 749 return { 750 UserImagesGrid: UserImagesGrid 751 }; 752}); 753