477 lines
18 KiB
JavaScript
477 lines
18 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
const Gdk = imports.gi.Gdk;
|
|
const GLib = imports.gi.GLib;
|
|
const Gio = imports.gi.Gio;
|
|
const GnomeBluetoothApplet = imports.gi.GnomeBluetoothApplet;
|
|
const GnomeBluetooth = imports.gi.GnomeBluetooth;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const St = imports.gi.St;
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const Main = imports.ui.main;
|
|
const MessageTray = imports.ui.messageTray;
|
|
const PanelMenu = imports.ui.panelMenu;
|
|
const PopupMenu = imports.ui.popupMenu;
|
|
|
|
const ConnectionState = {
|
|
DISCONNECTED: 0,
|
|
CONNECTED: 1,
|
|
DISCONNECTING: 2,
|
|
CONNECTING: 3
|
|
}
|
|
|
|
const Indicator = new Lang.Class({
|
|
Name: 'BTIndicator',
|
|
Extends: PanelMenu.SystemStatusButton,
|
|
|
|
_init: function() {
|
|
this.parent('bluetooth-disabled', _("Bluetooth"));
|
|
|
|
this._applet = new GnomeBluetoothApplet.Applet();
|
|
|
|
this._killswitch = new PopupMenu.PopupSwitchMenuItem(_("Bluetooth"), false);
|
|
this._applet.connect('notify::killswitch-state', Lang.bind(this, this._updateKillswitch));
|
|
this._killswitch.connect('toggled', Lang.bind(this, function() {
|
|
let current_state = this._applet.killswitch_state;
|
|
if (current_state != GnomeBluetooth.KillswitchState.HARD_BLOCKED &&
|
|
current_state != GnomeBluetooth.KillswitchState.NO_ADAPTER) {
|
|
this._applet.killswitch_state = this._killswitch.state ?
|
|
GnomeBluetooth.KillswitchState.UNBLOCKED:
|
|
GnomeBluetooth.KillswitchState.SOFT_BLOCKED;
|
|
} else
|
|
this._killswitch.setToggleState(false);
|
|
}));
|
|
|
|
this._discoverable = new PopupMenu.PopupSwitchMenuItem(_("Visibility"), this._applet.discoverable);
|
|
this._applet.connect('notify::discoverable', Lang.bind(this, function() {
|
|
this._discoverable.setToggleState(this._applet.discoverable);
|
|
}));
|
|
this._discoverable.connect('toggled', Lang.bind(this, function() {
|
|
this._applet.discoverable = this._discoverable.state;
|
|
}));
|
|
|
|
this._updateKillswitch();
|
|
this.menu.addMenuItem(this._killswitch);
|
|
this.menu.addMenuItem(this._discoverable);
|
|
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
|
|
|
this._fullMenuItems = [new PopupMenu.PopupSeparatorMenuItem(),
|
|
new PopupMenu.PopupMenuItem(_("Send Files to Device...")),
|
|
new PopupMenu.PopupMenuItem(_("Set up a New Device...")),
|
|
new PopupMenu.PopupSeparatorMenuItem()];
|
|
this._hasDevices = false;
|
|
|
|
this._fullMenuItems[1].connect('activate', function() {
|
|
GLib.spawn_command_line_async('bluetooth-sendto');
|
|
});
|
|
this._fullMenuItems[2].connect('activate', function() {
|
|
GLib.spawn_command_line_async('bluetooth-wizard');
|
|
});
|
|
|
|
for (let i = 0; i < this._fullMenuItems.length; i++) {
|
|
let item = this._fullMenuItems[i];
|
|
this.menu.addMenuItem(item);
|
|
}
|
|
|
|
this._deviceItemPosition = 3;
|
|
this._deviceItems = [];
|
|
this._applet.connect('devices-changed', Lang.bind(this, this._updateDevices));
|
|
this._updateDevices();
|
|
|
|
this._applet.connect('notify::show-full-menu', Lang.bind(this, this._updateFullMenu));
|
|
this._updateFullMenu();
|
|
|
|
this.menu.addSettingsAction(_("Bluetooth Settings"), 'bluetooth-properties.desktop');
|
|
|
|
this._applet.connect('pincode-request', Lang.bind(this, this._pinRequest));
|
|
this._applet.connect('confirm-request', Lang.bind(this, this._confirmRequest));
|
|
this._applet.connect('auth-request', Lang.bind(this, this._authRequest));
|
|
this._applet.connect('cancel-request', Lang.bind(this, this._cancelRequest));
|
|
},
|
|
|
|
setLockedState: function(locked) {
|
|
this._isLocked = locked;
|
|
this._updateKillswitch();
|
|
},
|
|
|
|
_updateKillswitch: function() {
|
|
let current_state = this._applet.killswitch_state;
|
|
let on = current_state == GnomeBluetooth.KillswitchState.UNBLOCKED;
|
|
let has_adapter = current_state != GnomeBluetooth.KillswitchState.NO_ADAPTER;
|
|
let can_toggle = current_state != GnomeBluetooth.KillswitchState.NO_ADAPTER &&
|
|
current_state != GnomeBluetooth.KillswitchState.HARD_BLOCKED;
|
|
|
|
this._killswitch.setToggleState(on);
|
|
if (can_toggle)
|
|
this._killswitch.setStatus(null);
|
|
else
|
|
/* TRANSLATORS: this means that bluetooth was disabled by hardware rfkill */
|
|
this._killswitch.setStatus(_("hardware disabled"));
|
|
|
|
this.actor.visible = !this._isLocked && has_adapter;
|
|
|
|
if (on) {
|
|
this._discoverable.actor.show();
|
|
this.setIcon('bluetooth-active');
|
|
} else {
|
|
this._discoverable.actor.hide();
|
|
this.setIcon('bluetooth-disabled');
|
|
}
|
|
},
|
|
|
|
_updateDevices: function() {
|
|
let devices = this._applet.get_devices();
|
|
|
|
let newlist = [ ];
|
|
for (let i = 0; i < this._deviceItems.length; i++) {
|
|
let item = this._deviceItems[i];
|
|
let destroy = true;
|
|
for (let j = 0; j < devices.length; j++) {
|
|
if (item._device.device_path == devices[j].device_path) {
|
|
this._updateDeviceItem(item, devices[j]);
|
|
destroy = false;
|
|
break;
|
|
}
|
|
}
|
|
if (destroy)
|
|
item.destroy();
|
|
else
|
|
newlist.push(item);
|
|
}
|
|
|
|
this._deviceItems = newlist;
|
|
this._hasDevices = newlist.length > 0;
|
|
for (let i = 0; i < devices.length; i++) {
|
|
let d = devices[i];
|
|
if (d._item)
|
|
continue;
|
|
let item = this._createDeviceItem(d);
|
|
if (item) {
|
|
this.menu.addMenuItem(item, this._deviceItemPosition + this._deviceItems.length);
|
|
this._deviceItems.push(item);
|
|
this._hasDevices = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
_updateDeviceItem: function(item, device) {
|
|
if (!device.can_connect && device.capabilities == GnomeBluetoothApplet.Capabilities.NONE) {
|
|
item.destroy();
|
|
return;
|
|
}
|
|
|
|
let prevDevice = item._device;
|
|
let prevCapabilities = prevDevice.capabilities;
|
|
let prevCanConnect = prevDevice.can_connect;
|
|
|
|
// adopt the new device object
|
|
item._device = device;
|
|
device._item = item;
|
|
|
|
// update properties
|
|
item.label.text = device.alias;
|
|
|
|
if (prevCapabilities != device.capabilities ||
|
|
prevCanConnect != device.can_connect) {
|
|
// need to rebuild the submenu
|
|
item.menu.removeAll();
|
|
this._buildDeviceSubMenu(item, device);
|
|
}
|
|
|
|
// update connected property
|
|
if (device.can_connect)
|
|
item._connectedMenuItem.setToggleState(device.connected);
|
|
},
|
|
|
|
_createDeviceItem: function(device) {
|
|
if (!device.can_connect && device.capabilities == GnomeBluetoothApplet.Capabilities.NONE)
|
|
return null;
|
|
let item = new PopupMenu.PopupSubMenuMenuItem(device.alias);
|
|
|
|
// adopt the device object, and add a back link
|
|
item._device = device;
|
|
device._item = item;
|
|
|
|
this._buildDeviceSubMenu(item, device);
|
|
|
|
return item;
|
|
},
|
|
|
|
_buildDeviceSubMenu: function(item, device) {
|
|
if (device.can_connect) {
|
|
let menuitem = new PopupMenu.PopupSwitchMenuItem(_("Connection"), device.connected);
|
|
item._connected = device.connected;
|
|
item._connectedMenuItem = menuitem;
|
|
menuitem.connect('toggled', Lang.bind(this, function() {
|
|
if (item._connected > ConnectionState.CONNECTED) {
|
|
// operation already in progress, revert
|
|
// (should not happen anyway)
|
|
menuitem.setToggleState(menuitem.state);
|
|
}
|
|
if (item._connected) {
|
|
item._connected = ConnectionState.DISCONNECTING;
|
|
menuitem.setStatus(_("disconnecting..."));
|
|
this._applet.disconnect_device(item._device.device_path, function(applet, success) {
|
|
if (success) { // apply
|
|
item._connected = ConnectionState.DISCONNECTED;
|
|
menuitem.setToggleState(false);
|
|
} else { // revert
|
|
item._connected = ConnectionState.CONNECTED;
|
|
menuitem.setToggleState(true);
|
|
}
|
|
menuitem.setStatus(null);
|
|
});
|
|
} else {
|
|
item._connected = ConnectionState.CONNECTING;
|
|
menuitem.setStatus(_("connecting..."));
|
|
this._applet.connect_device(item._device.device_path, function(applet, success) {
|
|
if (success) { // apply
|
|
item._connected = ConnectionState.CONNECTED;
|
|
menuitem.setToggleState(true);
|
|
} else { // revert
|
|
item._connected = ConnectionState.DISCONNECTED;
|
|
menuitem.setToggleState(false);
|
|
}
|
|
menuitem.setStatus(null);
|
|
});
|
|
}
|
|
}));
|
|
|
|
item.menu.addMenuItem(menuitem);
|
|
}
|
|
|
|
if (device.capabilities & GnomeBluetoothApplet.Capabilities.OBEX_PUSH) {
|
|
item.menu.addAction(_("Send Files..."), Lang.bind(this, function() {
|
|
this._applet.send_to_address(device.bdaddr, device.alias);
|
|
}));
|
|
}
|
|
if (device.capabilities & GnomeBluetoothApplet.Capabilities.OBEX_FILE_TRANSFER) {
|
|
item.menu.addAction(_("Browse Files..."), Lang.bind(this, function(event) {
|
|
this._applet.browse_address(device.bdaddr, event.get_time(),
|
|
Lang.bind(this, function(applet, result) {
|
|
try {
|
|
applet.browse_address_finish(result);
|
|
} catch (e) {
|
|
this._ensureSource();
|
|
this._source.notify(new MessageTray.Notification(this._source,
|
|
_("Bluetooth"),
|
|
_("Error browsing device"),
|
|
{ body: _("The requested device cannot be browsed, error is '%s'").format(e) }));
|
|
}
|
|
}));
|
|
}));
|
|
}
|
|
|
|
switch (device.type) {
|
|
case GnomeBluetoothApplet.Type.KEYBOARD:
|
|
item.menu.addSettingsAction(_("Keyboard Settings"), 'gnome-keyboard-panel.desktop');
|
|
break;
|
|
case GnomeBluetoothApplet.Type.MOUSE:
|
|
item.menu.addSettingsAction(_("Mouse Settings"), 'gnome-mouse-panel.desktop');
|
|
break;
|
|
case GnomeBluetoothApplet.Type.HEADSET:
|
|
case GnomeBluetoothApplet.Type.HEADPHONES:
|
|
case GnomeBluetoothApplet.Type.OTHER_AUDIO:
|
|
item.menu.addSettingsAction(_("Sound Settings"), 'gnome-sound-panel.desktop');
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
|
|
_updateFullMenu: function() {
|
|
if (this._applet.show_full_menu) {
|
|
this._showAll(this._fullMenuItems);
|
|
if (this._hasDevices)
|
|
this._showAll(this._deviceItems);
|
|
} else {
|
|
this._hideAll(this._fullMenuItems);
|
|
this._hideAll(this._deviceItems);
|
|
}
|
|
},
|
|
|
|
_showAll: function(items) {
|
|
for (let i = 0; i < items.length; i++)
|
|
items[i].actor.show();
|
|
},
|
|
|
|
_hideAll: function(items) {
|
|
for (let i = 0; i < items.length; i++)
|
|
items[i].actor.hide();
|
|
},
|
|
|
|
_destroyAll: function(items) {
|
|
for (let i = 0; i < items.length; i++)
|
|
items[i].destroy();
|
|
},
|
|
|
|
_ensureSource: function() {
|
|
if (!this._source) {
|
|
this._source = new MessageTray.Source(_("Bluetooth"), 'bluetooth-active', St.IconType.SYMBOLIC);
|
|
Main.messageTray.add(this._source);
|
|
}
|
|
},
|
|
|
|
_authRequest: function(applet, device_path, name, long_name, uuid) {
|
|
this._ensureSource();
|
|
this._source.notify(new AuthNotification(this._source, this._applet, device_path, name, long_name, uuid));
|
|
},
|
|
|
|
_confirmRequest: function(applet, device_path, name, long_name, pin) {
|
|
this._ensureSource();
|
|
this._source.notify(new ConfirmNotification(this._source, this._applet, device_path, name, long_name, pin));
|
|
},
|
|
|
|
_pinRequest: function(applet, device_path, name, long_name, numeric) {
|
|
this._ensureSource();
|
|
this._source.notify(new PinNotification(this._source, this._applet, device_path, name, long_name, numeric));
|
|
},
|
|
|
|
_cancelRequest: function() {
|
|
this._source.destroy();
|
|
}
|
|
});
|
|
|
|
const AuthNotification = new Lang.Class({
|
|
Name: 'AuthNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, applet, device_path, name, long_name, uuid) {
|
|
this.parent(source,
|
|
_("Bluetooth"),
|
|
_("Authorization request from %s").format(name),
|
|
{ customContent: true });
|
|
this.setResident(true);
|
|
|
|
this._applet = applet;
|
|
this._devicePath = device_path;
|
|
this.addBody(_("Device %s wants access to the service '%s'").format(long_name, uuid));
|
|
|
|
this.addButton('always-grant', _("Always grant access"));
|
|
this.addButton('grant', _("Grant this time only"));
|
|
this.addButton('reject', _("Reject"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
switch (action) {
|
|
case 'always-grant':
|
|
this._applet.agent_reply_auth(this._devicePath, true, true);
|
|
break;
|
|
case 'grant':
|
|
this._applet.agent_reply_auth(this._devicePath, true, false);
|
|
break;
|
|
case 'reject':
|
|
default:
|
|
this._applet.agent_reply_auth(this._devicePath, false, false);
|
|
}
|
|
this.destroy();
|
|
}));
|
|
}
|
|
});
|
|
|
|
const ConfirmNotification = new Lang.Class({
|
|
Name: 'ConfirmNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, applet, device_path, name, long_name, pin) {
|
|
this.parent(source,
|
|
_("Bluetooth"),
|
|
_("Pairing confirmation for %s").format(name),
|
|
{ customContent: true });
|
|
this.setResident(true);
|
|
|
|
this._applet = applet;
|
|
this._devicePath = device_path;
|
|
this.addBody(_("Device %s wants to pair with this computer").format(long_name));
|
|
this.addBody(_("Please confirm whether the PIN '%06d' matches the one on the device.").format(pin));
|
|
|
|
this.addButton('matches', _("Matches"));
|
|
this.addButton('does-not-match', _("Does not match"));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
if (action == 'matches')
|
|
this._applet.agent_reply_confirm(this._devicePath, true);
|
|
else
|
|
this._applet.agent_reply_confirm(this._devicePath, false);
|
|
this.destroy();
|
|
}));
|
|
}
|
|
});
|
|
|
|
const PinNotification = new Lang.Class({
|
|
Name: 'PinNotification',
|
|
Extends: MessageTray.Notification,
|
|
|
|
_init: function(source, applet, device_path, name, long_name, numeric) {
|
|
this.parent(source,
|
|
_("Bluetooth"),
|
|
_("Pairing request for %s").format(name),
|
|
{ customContent: true });
|
|
this.setResident(true);
|
|
|
|
this._applet = applet;
|
|
this._devicePath = device_path;
|
|
this._numeric = numeric;
|
|
this.addBody(_("Device %s wants to pair with this computer").format(long_name));
|
|
this.addBody(_("Please enter the PIN mentioned on the device."));
|
|
|
|
this._entry = new St.Entry();
|
|
this._entry.connect('key-release-event', Lang.bind(this, function(entry, event) {
|
|
let key = event.get_key_symbol();
|
|
if (key == Clutter.KEY_Return) {
|
|
if (this._canActivateOkButton())
|
|
this.emit('action-invoked', 'ok');
|
|
return true;
|
|
} else if (key == Clutter.KEY_Escape) {
|
|
this.emit('action-invoked', 'cancel');
|
|
return true;
|
|
}
|
|
return false;
|
|
}));
|
|
this.addActor(this._entry);
|
|
|
|
this.addButton('ok', _("OK"));
|
|
this.addButton('cancel', _("Cancel"));
|
|
|
|
this.setButtonSensitive('ok', this._canActivateOkButton());
|
|
this._entry.clutter_text.connect('text-changed', Lang.bind(this,
|
|
function() {
|
|
this.setButtonSensitive('ok', this._canActivateOkButton());
|
|
}));
|
|
|
|
this.connect('action-invoked', Lang.bind(this, function(self, action) {
|
|
if (action == 'ok') {
|
|
if (this._numeric) {
|
|
let num = parseInt(this._entry.text);
|
|
if (isNaN(num)) {
|
|
// user reply was empty, or was invalid
|
|
// cancel the operation
|
|
num = -1;
|
|
}
|
|
this._applet.agent_reply_passkey(this._devicePath, num);
|
|
} else
|
|
this._applet.agent_reply_pincode(this._devicePath, this._entry.text);
|
|
} else {
|
|
if (this._numeric)
|
|
this._applet.agent_reply_passkey(this._devicePath, -1);
|
|
else
|
|
this._applet.agent_reply_pincode(this._devicePath, null);
|
|
}
|
|
this.destroy();
|
|
}));
|
|
},
|
|
|
|
_canActivateOkButton: function() {
|
|
// PINs have a fixed length of 6
|
|
return this._entry.clutter_text.text.length == 6;
|
|
},
|
|
|
|
grabFocus: function(lockTray) {
|
|
this.parent(lockTray);
|
|
global.stage.set_key_focus(this._entry);
|
|
}
|
|
});
|