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.system.bluetooth', function() { 6 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel; 7 /** @const */ var DeletableItem = options.DeletableItem; 8 /** @const */ var DeletableItemList = options.DeletableItemList; 9 /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; 10 11 /** 12 * Bluetooth settings constants. 13 */ 14 function Constants() {} 15 16 /** 17 * Creates a new bluetooth list item. 18 * @param {{name: string, 19 * address: string, 20 * paired: boolean, 21 * connected: boolean, 22 * connecting: boolean, 23 * connectable: boolean, 24 * pairing: string|undefined, 25 * passkey: number|undefined, 26 * pincode: string|undefined, 27 * entered: number|undefined}} device 28 * Description of the Bluetooth device. 29 * @constructor 30 * @extends {options.DeletableItem} 31 */ 32 function BluetoothListItem(device) { 33 var el = cr.doc.createElement('div'); 34 el.__proto__ = BluetoothListItem.prototype; 35 el.data = {}; 36 for (var key in device) 37 el.data[key] = device[key]; 38 el.decorate(); 39 // Only show the close button for paired devices, but not for connecting 40 // devices. 41 el.deletable = device.paired && !device.connecting; 42 return el; 43 } 44 45 BluetoothListItem.prototype = { 46 __proto__: DeletableItem.prototype, 47 48 /** 49 * Description of the Bluetooth device. 50 * @type {{name: string, 51 * address: string, 52 * paired: boolean, 53 * connected: boolean, 54 * connecting: boolean, 55 * connectable: boolean, 56 * pairing: string|undefined, 57 * passkey: number|undefined, 58 * pincode: string|undefined, 59 * entered: number|undefined}} 60 */ 61 data: null, 62 63 /** @override */ 64 decorate: function() { 65 DeletableItem.prototype.decorate.call(this); 66 var label = this.ownerDocument.createElement('div'); 67 label.className = 'bluetooth-device-label'; 68 this.classList.add('bluetooth-device'); 69 // There are four kinds of devices we want to distinguish: 70 // * Connecting devices: in bold with a "connecting" label, 71 // * Connected devices: in bold, 72 // * Paired, not connected but connectable devices: regular and 73 // * Paired, not connected and not connectable devices: grayed out. 74 this.connected = this.data.connecting || 75 (this.data.paired && this.data.connected); 76 this.notconnectable = this.data.paired && !this.data.connecting && 77 !this.data.connected && !this.data.connectable; 78 // "paired" devices are those that are remembered but not connected. 79 this.paired = this.data.paired && !this.data.connected && 80 this.data.connectable; 81 82 var content = this.data.name; 83 // Update the device's label according to its state. A "connecting" device 84 // can be in the process of connecting and pairing, so we check connecting 85 // first. 86 if (this.data.connecting) { 87 content = loadTimeData.getStringF('bluetoothDeviceConnecting', 88 this.data.name); 89 } 90 label.textContent = content; 91 this.contentElement.appendChild(label); 92 }, 93 }; 94 95 /** 96 * Class for displaying a list of Bluetooth devices. 97 * @constructor 98 * @extends {options.DeletableItemList} 99 */ 100 var BluetoothDeviceList = cr.ui.define('list'); 101 102 BluetoothDeviceList.prototype = { 103 __proto__: DeletableItemList.prototype, 104 105 /** 106 * Height of a list entry in px. 107 * @type {number} 108 * @private 109 */ 110 itemHeight_: 32, 111 112 /** 113 * Width of a list entry in px. 114 * @type {number} 115 * @private. 116 */ 117 itemWidth_: 400, 118 119 /** @override */ 120 decorate: function() { 121 DeletableItemList.prototype.decorate.call(this); 122 // Force layout of all items even if not in the viewport to address 123 // errors in scroll positioning when the list is hidden during initial 124 // layout. The impact on performance should be minimal given that the 125 // list is not expected to grow very large. Fixed height items are also 126 // required to avoid caching incorrect sizes during layout of a hidden 127 // list. 128 this.autoExpands = true; 129 this.fixedHeight = true; 130 this.clear(); 131 this.selectionModel = new ListSingleSelectionModel(); 132 }, 133 134 /** 135 * Adds a bluetooth device to the list of available devices. A check is 136 * made to see if the device is already in the list, in which case the 137 * existing device is updated. 138 * @param {{name: string, 139 * address: string, 140 * paired: boolean, 141 * connected: boolean, 142 * connecting: boolean, 143 * connectable: boolean, 144 * pairing: string|undefined, 145 * passkey: number|undefined, 146 * pincode: string|undefined, 147 * entered: number|undefined}} device 148 * Description of the bluetooth device. 149 * @return {boolean} True if the devies was successfully added or updated. 150 */ 151 appendDevice: function(device) { 152 var selectedDevice = this.getSelectedDevice_(); 153 var index = this.find(device.address); 154 if (index == undefined) { 155 this.dataModel.push(device); 156 this.redraw(); 157 } else { 158 this.dataModel.splice(index, 1, device); 159 this.redrawItem(index); 160 } 161 this.updateListVisibility_(); 162 if (selectedDevice) 163 this.setSelectedDevice_(selectedDevice); 164 return true; 165 }, 166 167 /** 168 * Forces a revailidation of the list content. Deleting a single item from 169 * the list results in a stale cache requiring an invalidation. 170 * @param {string=} opt_selection Optional address of device to select 171 * after refreshing the list. 172 */ 173 refresh: function(opt_selection) { 174 // TODO(kevers): Investigate if the stale cache issue can be fixed in 175 // cr.ui.list. 176 var selectedDevice = opt_selection ? opt_selection : 177 this.getSelectedDevice_(); 178 this.invalidate(); 179 this.redraw(); 180 if (selectedDevice) 181 this.setSelectedDevice_(selectedDevice); 182 }, 183 184 /** 185 * Retrieves the address of the selected device, or null if no device is 186 * selected. 187 * @return {?string} Address of selected device or null. 188 * @private 189 */ 190 getSelectedDevice_: function() { 191 var selection = this.selectedItem; 192 if (selection) 193 return selection.address; 194 return null; 195 }, 196 197 /** 198 * Selects the device with the matching address. 199 * @param {string} address The unique address of the device. 200 * @private 201 */ 202 setSelectedDevice_: function(address) { 203 var index = this.find(address); 204 if (index != undefined) 205 this.selectionModel.selectRange(index, index); 206 }, 207 208 /** 209 * Perges all devices from the list. 210 */ 211 clear: function() { 212 this.dataModel = new ArrayDataModel([]); 213 this.redraw(); 214 this.updateListVisibility_(); 215 }, 216 217 /** 218 * Returns the index of the list entry with the matching address. 219 * @param {string} address Unique address of the Bluetooth device. 220 * @return {number|undefined} Index of the matching entry or 221 * undefined if no match found. 222 */ 223 find: function(address) { 224 var size = this.dataModel.length; 225 for (var i = 0; i < size; i++) { 226 var entry = this.dataModel.item(i); 227 if (entry.address == address) 228 return i; 229 } 230 }, 231 232 /** @override */ 233 createItem: function(entry) { 234 return new BluetoothListItem(entry); 235 }, 236 237 /** 238 * Overrides the default implementation, which is used to compute the 239 * size of an element in the list. The default implementation relies 240 * on adding a placeholder item to the list and fetching its size and 241 * position. This strategy does not work if an item is added to the list 242 * while it is hidden, as the computed metrics will all be zero in that 243 * case. 244 * @return {{height: number, marginTop: number, marginBottom: number, 245 * width: number, marginLeft: number, marginRight: number}} 246 * The height and width of the item, taking margins into account, 247 * and the margins themselves. 248 */ 249 measureItem: function() { 250 return { 251 height: this.itemHeight_, 252 marginTop: 0, 253 marginBotton: 0, 254 width: this.itemWidth_, 255 marginLeft: 0, 256 marginRight: 0 257 }; 258 }, 259 260 /** 261 * Override the default implementation to return a predetermined size, 262 * which in turns allows proper layout of items even if the list is hidden. 263 * @return {height: number, width: number} Dimensions of a single item in 264 * the list of bluetooth device. 265 * @private. 266 */ 267 getDefaultItemSize_: function() { 268 return { 269 height: this.itemHeight_, 270 width: this.itemWidth_ 271 }; 272 }, 273 274 /** 275 * Override base implementation of handleClick_, which unconditionally 276 * removes the item. In this case, removal of the element is deferred 277 * pending confirmation from the Bluetooth adapter. 278 * @param {Event} e The click event object. 279 * @private 280 */ 281 handleClick_: function(e) { 282 if (this.disabled) 283 return; 284 285 var target = e.target; 286 if (!target.classList.contains('row-delete-button')) 287 return; 288 289 var item = this.getListItemAncestor(target); 290 var selected = this.selectionModel.selectedIndex; 291 var index = this.getIndexOfListItem(item); 292 if (item && item.deletable) { 293 if (selected != index) 294 this.setSelectedDevice_(item.data.address); 295 // Device is busy until we hear back from the Bluetooth adapter. 296 // Prevent double removal request. 297 item.deletable = false; 298 // TODO(kevers): Provide visual feedback that the device is busy. 299 300 // Inform the bluetooth adapter that we are disconnecting or 301 // forgetting the device. 302 chrome.send('updateBluetoothDevice', 303 [item.data.address, item.connected ? 'disconnect' : 'forget']); 304 } 305 }, 306 307 /** @override */ 308 deleteItemAtIndex: function(index) { 309 var selectedDevice = this.getSelectedDevice_(); 310 this.dataModel.splice(index, 1); 311 this.refresh(selectedDevice); 312 this.updateListVisibility_(); 313 }, 314 315 /** 316 * If the list has an associated empty list placholder then update the 317 * visibility of the list and placeholder. 318 * @private 319 */ 320 updateListVisibility_: function() { 321 var empty = this.dataModel.length == 0; 322 var listPlaceHolderID = this.id + '-empty-placeholder'; 323 if ($(listPlaceHolderID)) { 324 if (this.hidden != empty) { 325 this.hidden = empty; 326 $(listPlaceHolderID).hidden = !empty; 327 this.refresh(); 328 } 329 } 330 }, 331 }; 332 333 cr.defineProperty(BluetoothListItem, 'connected', cr.PropertyKind.BOOL_ATTR); 334 335 cr.defineProperty(BluetoothListItem, 'paired', cr.PropertyKind.BOOL_ATTR); 336 337 cr.defineProperty(BluetoothListItem, 'connecting', cr.PropertyKind.BOOL_ATTR); 338 339 cr.defineProperty(BluetoothListItem, 'notconnectable', 340 cr.PropertyKind.BOOL_ATTR); 341 342 return { 343 BluetoothListItem: BluetoothListItem, 344 BluetoothDeviceList: BluetoothDeviceList, 345 Constants: Constants 346 }; 347}); 348