• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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