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