diff options
Diffstat (limited to 'lib/bluetooth-settings-widget.c')
-rw-r--r-- | lib/bluetooth-settings-widget.c | 1692 |
1 files changed, 1692 insertions, 0 deletions
diff --git a/lib/bluetooth-settings-widget.c b/lib/bluetooth-settings-widget.c new file mode 100644 index 00000000..9f83f036 --- /dev/null +++ b/lib/bluetooth-settings-widget.c @@ -0,0 +1,1692 @@ +/* + * + * Copyright (C) 2013 Bastien Nocera <hadess@hadess.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <gtk/gtk.h> + +#include <glib/gi18n-lib.h> +#include <math.h> + +#include "bluetooth-client.h" +#include "bluetooth-client-private.h" +#include "bluetooth-client-glue.h" +#include "bluetooth-agent.h" +#include "bluetooth-utils.h" +#include "bluetooth-settings-widget.h" +#include "bluetooth-settings-resources.h" +#include "bluetooth-settings-row.h" +#include "bluetooth-pairing-dialog.h" +#include "pin.h" + +#define BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE((obj), BLUETOOTH_TYPE_SETTINGS_WIDGET, BluetoothSettingsWidgetPrivate)) + +typedef struct _BluetoothSettingsWidgetPrivate BluetoothSettingsWidgetPrivate; + +struct _BluetoothSettingsWidgetPrivate { + GtkBuilder *builder; + BluetoothClient *client; + GtkTreeModel *model; + gboolean debug; + GCancellable *cancellable; + + /* Pairing */ + BluetoothAgent *agent; + GtkWidget *pairing_dialog; + + /* Properties */ + GtkWidget *properties_dialog; + GtkWidget *properties_header; + GtkWidget *properties_title; + char *selected_bdaddr; + char *selected_name; + char *selected_object_path; + + /* Device section */ + GtkWidget *device_list; + GtkAdjustment *focus_adjustment; + GtkSizeGroup *row_sizegroup; + GtkWidget *device_revealer; + GtkWidget *device_spinner; + GHashTable *connecting_devices; + + GtkWidget *visible_label; + + GList *boxes; + GList *boxes_reverse; +}; + +G_DEFINE_TYPE(BluetoothSettingsWidget, bluetooth_settings_widget, GTK_TYPE_BOX) + +enum { + PANEL_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +#define WID(s) GTK_WIDGET (gtk_builder_get_object (priv->builder, s)) + +#define KEYBOARD_PREFS "keyboard" +#define MOUSE_PREFS "mouse" +#define SOUND_PREFS "sound" + +#define ICON_SIZE 128 + +/* We'll try to connect to the device repeatedly for that + * amount of time before we bail out */ +#define CONNECT_TIMEOUT 3.0 + +#define BLUEZ_SERVICE "org.bluez" +#define ADAPTER_IFACE "org.bluez.Adapter1" + +#define AGENT_PATH "/org/gnome/bluetooth/settings" + +enum { + CONNECTING_NOTEBOOK_PAGE_SWITCH = 0, + CONNECTING_NOTEBOOK_PAGE_SPINNER = 1 +}; + +static void +set_connecting_page (BluetoothSettingsWidget *self, + int page) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + + if (page == CONNECTING_NOTEBOOK_PAGE_SPINNER) + gtk_spinner_start (GTK_SPINNER (WID ("connecting_spinner"))); + gtk_notebook_set_current_page (GTK_NOTEBOOK (WID ("connecting_notebook")), page); + if (page == CONNECTING_NOTEBOOK_PAGE_SWITCH) + gtk_spinner_start (GTK_SPINNER (WID ("connecting_spinner"))); +} + +static void +remove_connecting (BluetoothSettingsWidget *self, + const char *bdaddr) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + g_hash_table_remove (priv->connecting_devices, bdaddr); +} + +static void +add_connecting (BluetoothSettingsWidget *self, + const char *bdaddr) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + g_hash_table_insert (priv->connecting_devices, + g_strdup (bdaddr), + GINT_TO_POINTER (1)); +} + +static gboolean +is_connecting (BluetoothSettingsWidget *self, + const char *bdaddr) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + return GPOINTER_TO_INT (g_hash_table_lookup (priv->connecting_devices, + bdaddr)); +} + +typedef struct { + char *bdaddr; + BluetoothSettingsWidget *self; +} ConnectData; + +static void +connect_done (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + BluetoothSettingsWidget *self; + BluetoothSettingsWidgetPrivate *priv; + gboolean success; + GError *error = NULL; + ConnectData *data = (ConnectData *) user_data; + + success = bluetooth_client_connect_service_finish (BLUETOOTH_CLIENT (source_object), + res, &error); + if (!success && g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + goto out; + + self = data->self; + priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + + /* Check whether the same device is now selected, and update the UI */ + if (g_strcmp0 (priv->selected_bdaddr, data->bdaddr) == 0) { + GtkSwitch *button; + + button = GTK_SWITCH (WID ("switch_connection")); + /* Reset the switch if it failed */ + if (success == FALSE) + gtk_switch_set_active (button, !gtk_switch_get_active (button)); + set_connecting_page (self, CONNECTING_NOTEBOOK_PAGE_SWITCH); + } + + remove_connecting (self, data->bdaddr); + + //FIXME show an error if it failed? + +out: + g_clear_error (&error); + g_free (data->bdaddr); + g_free (data); +} + +static void +setup_pairing_dialog (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *toplevel; + + g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy); + priv->pairing_dialog = bluetooth_pairing_dialog_new (); + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + gtk_window_set_transient_for (GTK_WINDOW (priv->pairing_dialog), GTK_WINDOW (toplevel)); + gtk_window_set_modal (GTK_WINDOW (priv->pairing_dialog), TRUE); +} + +static gboolean +get_properties_for_device (GDBusProxy *device, + char **name, + char **bdaddr, + BluetoothType *type) +{ + GVariant *value; + + g_return_val_if_fail (name != NULL, FALSE); + + value = g_dbus_proxy_get_cached_property (device, "Name"); + *name = g_variant_dup_string (value, NULL); + g_variant_unref (value); + + if (bdaddr) { + value = g_dbus_proxy_get_cached_property (device, "Address"); + *bdaddr = g_variant_dup_string (value, NULL); + g_variant_unref (value); + } + + if (type) { + value = g_dbus_proxy_get_cached_property (device, "Class"); + *type = bluetooth_class_to_type (g_variant_get_uint32 (value)); + g_variant_unref (value); + } + + return TRUE; +} + +static char * +get_random_pincode (guint num_digits) +{ + if (num_digits == 0) + num_digits = PIN_NUM_DIGITS; + return g_strdup_printf ("%d", g_random_int_range (pow (10, num_digits - 1), + pow (10, num_digits))); +} + +static char * +get_icade_pincode (char **pin_display_str) +{ + GString *pin, *pin_display; + guint i; + static char *arrows[] = { + NULL, + "⬆", /* up = 1 */ + "⬇", /* down = 2 */ + "⬅", /* left = 3 */ + "➡" /* right = 4 */ + }; + + pin = g_string_new (NULL); + pin_display = g_string_new (NULL); + + for (i = 0; i < PIN_NUM_DIGITS; i++) { + int r; + char *c; + + r = g_random_int_range (1, 5); + + c = g_strdup_printf ("%d", r); + g_string_append (pin, c); + g_free (c); + + g_string_append (pin_display, arrows[r]); + } + g_string_append (pin_display, "❍"); + + *pin_display_str = g_string_free (pin_display, FALSE); + return g_string_free (pin, FALSE); +} + +static void +display_cb (GtkDialog *dialog, + int response, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GDBusMethodInvocation *invocation; + + invocation = g_object_get_data (G_OBJECT (dialog), "invocation"); + + g_dbus_method_invocation_return_dbus_error (invocation, + "org.bluez.Error.Canceled", + "User cancelled pairing"); + + g_object_set_data (G_OBJECT (priv->pairing_dialog), "invocation", NULL); +} + +static void +enter_pin_cb (GtkDialog *dialog, + int response, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GDBusMethodInvocation *invocation; + + invocation = g_object_get_data (G_OBJECT (dialog), "invocation"); + + if (response == GTK_RESPONSE_ACCEPT) { + const char *name; + char *pin; + BluetoothPairingMode mode; + + g_dbus_method_invocation_return_value (invocation, NULL); + + mode = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (dialog), "mode")); + name = g_object_get_data (G_OBJECT (dialog), "name"); + pin = bluetooth_pairing_dialog_get_pin (BLUETOOTH_PAIRING_DIALOG (dialog)); + g_dbus_method_invocation_return_value (invocation, + g_variant_new ("(s)", pin)); + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + mode, pin, name); + g_free (pin); + g_signal_connect (G_OBJECT (priv->pairing_dialog), "response", + G_CALLBACK (display_cb), user_data); + } else { + g_dbus_method_invocation_return_dbus_error (invocation, + "org.bluez.Error.Canceled", + "User cancelled pairing"); + } + + g_object_set_data (G_OBJECT (priv->pairing_dialog), "invocation", NULL); + g_object_set_data (G_OBJECT (priv->pairing_dialog), "mode", NULL); + g_object_set_data (G_OBJECT (priv->pairing_dialog), "name", NULL); +} + +static void +pincode_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + BluetoothType type; + char *name, *bdaddr; + guint max_digits; + gboolean confirm_pin = TRUE; + char *default_pin; + char *display_pin = NULL; + BluetoothPairingMode mode; + + g_debug ("pincode_callback (%s)", g_dbus_proxy_get_object_path (device)); + + if (!get_properties_for_device (device, &name, &bdaddr, &type)) { + char *msg; + + msg = g_strdup_printf ("Missing information for %s", g_dbus_proxy_get_object_path (device)); + g_dbus_method_invocation_return_dbus_error (invocation, "org.bluez.Error.Rejected", msg); + g_free (msg); + return; + } + + default_pin = get_pincode_for_device (type, bdaddr, name, &max_digits, &confirm_pin); + if (g_strcmp0 (default_pin, "KEYBOARD") == 0) { + mode = BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_KEYBOARD; + g_free (default_pin); + default_pin = get_random_pincode (max_digits); + display_pin = g_strdup_printf ("%s⏎", default_pin); + } else if (g_strcmp0 (default_pin, "ICADE") == 0) { + mode = BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_ICADE; + confirm_pin = FALSE; + g_free (default_pin); + default_pin = get_icade_pincode (&display_pin); + } else if (default_pin == NULL) { + mode = BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_NORMAL; + confirm_pin = TRUE; + default_pin = get_random_pincode (0); + } else if (g_strcmp0 (default_pin, "NULL") == 0) { + g_assert_not_reached (); + } else { + mode = BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_NORMAL; + confirm_pin = TRUE; + } + + setup_pairing_dialog (BLUETOOTH_SETTINGS_WIDGET (user_data)); + + g_object_set_data (G_OBJECT (priv->pairing_dialog), "invocation", invocation); + g_object_set_data_full (G_OBJECT (priv->pairing_dialog), "name", g_strdup (name), g_free); + g_object_set_data (G_OBJECT (priv->pairing_dialog), "mode", GUINT_TO_POINTER (mode)); + + if (confirm_pin) { + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + BLUETOOTH_PAIRING_MODE_PIN_CONFIRMATION, + default_pin, + name); + g_signal_connect (G_OBJECT (priv->pairing_dialog), "response", + G_CALLBACK (enter_pin_cb), user_data); + } else { + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + mode, display_pin, name); + g_signal_connect (G_OBJECT (priv->pairing_dialog), "response", + G_CALLBACK (display_cb), user_data); + } + + gtk_widget_show (priv->pairing_dialog); +} + +static void +display_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + guint pin, + guint entered, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + char *pin_str, *name; + + g_debug ("display_callback (%s, %i, %i)", g_dbus_proxy_get_object_path (device), pin, entered); + + if (priv->pairing_dialog == NULL || + bluetooth_pairing_dialog_get_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog)) != BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_KEYBOARD) + setup_pairing_dialog (BLUETOOTH_SETTINGS_WIDGET (user_data)); + + pin_str = g_strdup_printf ("%06d", pin); + get_properties_for_device (device, &name, NULL, NULL); + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + BLUETOOTH_PAIRING_MODE_PIN_DISPLAY_KEYBOARD, + pin_str, + name); + bluetooth_pairing_dialog_set_pin_entered (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + entered); + g_free (pin_str); + g_free (name); +} + +static void +display_pincode_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + const char *pincode, + gpointer user_data) +{ + g_debug ("display_pincode_callback (%s, %s)", g_dbus_proxy_get_object_path (device), pincode); + + /* Reject all the calls here, so that we'll get asked about the + * pincode instead of being told the pincode */ + g_dbus_method_invocation_return_dbus_error (invocation, + "org.bluez.Error.Rejected", + "Rejected bluetoothd generated PIN code"); +} + +static gboolean +cancel_callback (GDBusMethodInvocation *invocation, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GList *l, *children; + + g_debug ("cancel_callback ()"); + + g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy); + + children = gtk_container_get_children (GTK_CONTAINER (priv->device_list)); + for (l = children; l != NULL; l = l->next) + g_object_set (l->data, "pairing", FALSE, NULL); + g_list_free (children); + + g_dbus_method_invocation_return_value (invocation, NULL); + + return TRUE; +} + +static void +confirm_cb (GtkDialog *dialog, + int response, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GDBusMethodInvocation *invocation; + + invocation = g_object_get_data (G_OBJECT (dialog), "invocation"); + if (response == GTK_RESPONSE_ACCEPT) { + g_dbus_method_invocation_return_value (invocation, NULL); + } else { + g_dbus_method_invocation_return_dbus_error (invocation, + "org.bluez.Error.Canceled", + "User cancelled pairing"); + } + g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy); +} + +static void +confirm_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + guint pin, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + char *name, *pin_str; + + g_debug ("confirm_callback (%s, %i)", g_dbus_proxy_get_object_path (device), pin); + + setup_pairing_dialog (BLUETOOTH_SETTINGS_WIDGET (user_data)); + + pin_str = g_strdup_printf ("%06d", pin); + get_properties_for_device (device, &name, NULL, NULL); + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + BLUETOOTH_PAIRING_MODE_PIN_MATCH, + pin_str, name); + + g_signal_connect (G_OBJECT (priv->pairing_dialog), "response", + G_CALLBACK (confirm_cb), user_data); + g_object_set_data (G_OBJECT (priv->pairing_dialog), "invocation", invocation); + + gtk_widget_show (priv->pairing_dialog); + + g_free (pin_str); + g_free (name); +} + +static void +authorize_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + char *name; + + g_debug ("authorize_callback (%s)", g_dbus_proxy_get_object_path (device)); + + setup_pairing_dialog (BLUETOOTH_SETTINGS_WIDGET (user_data)); + get_properties_for_device (device, &name, NULL, NULL); + bluetooth_pairing_dialog_set_mode (BLUETOOTH_PAIRING_DIALOG (priv->pairing_dialog), + BLUETOOTH_PAIRING_MODE_YES_NO, + NULL, name); + + g_signal_connect (G_OBJECT (priv->pairing_dialog), "response", + G_CALLBACK (confirm_cb), user_data); + g_object_set_data (G_OBJECT (priv->pairing_dialog), "invocation", invocation); + + gtk_widget_show (priv->pairing_dialog); + + g_free (name); +} + +static void +authorize_service_callback (GDBusMethodInvocation *invocation, + GDBusProxy *device, + const char *uuid, + gpointer user_data) +{ + char *msg; + + g_debug ("authorize_service_callback (%s, %s)", g_dbus_proxy_get_object_path (device), uuid); + + msg = g_strdup_printf ("Rejecting service auth (%s) for %s", + uuid, g_dbus_proxy_get_object_path (device)); + g_dbus_method_invocation_return_dbus_error (invocation, "org.bluez.Error.Rejected", msg); + g_free (msg); +} + +static void +turn_off_pairing (BluetoothSettingsWidget *self, + const char *object_path) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GList *l, *children; + + children = gtk_container_get_children (GTK_CONTAINER (priv->device_list)); + for (l = children; l != NULL; l = l->next) { + GDBusProxy *proxy; + + g_object_get (l->data, "proxy", &proxy, NULL); + if (g_strcmp0 (g_dbus_proxy_get_object_path (proxy), object_path) == 0) { + g_object_set (l->data, "pairing", FALSE, NULL); + g_object_unref (proxy); + break; + } + g_object_unref (proxy); + } + g_list_free (children); +} + +typedef struct { + BluetoothSettingsWidget *self; + char *device; + GTimer *timer; + guint timeout_id; +} SetupConnectData; + +static void connect_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data); + +static gboolean +connect_timeout_cb (gpointer user_data) +{ + SetupConnectData *data = (SetupConnectData *) user_data; + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (data->self); + + bluetooth_client_connect_service (priv->client, data->device, TRUE, NULL, connect_callback, data); + data->timeout_id = 0; + + return G_SOURCE_REMOVE; +} + +static void +connect_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SetupConnectData *data = (SetupConnectData *) user_data; + GError *error = NULL; + gboolean success; + + success = bluetooth_client_connect_service_finish (BLUETOOTH_CLIENT (source_object), res, &error); + + if (success == FALSE) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free (error); + goto bail; + } else if (g_timer_elapsed (data->timer, NULL) < CONNECT_TIMEOUT) { + g_assert (data->timeout_id == 0); + data->timeout_id = g_timeout_add (500, connect_timeout_cb, data); + g_error_free (error); + return; + } + g_debug ("Failed to connect to device %s", data->device); + } + + turn_off_pairing (user_data, data->device); + +bail: + if (data->timeout_id > 0) + g_source_remove (data->timeout_id); + + g_timer_destroy (data->timer); + g_free (data); +} + +static void +create_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + SetupConnectData *data; + GError *error = NULL; + gboolean ret; + char *path; + + ret = bluetooth_client_setup_device_finish (BLUETOOTH_CLIENT (source_object), + res, &path, &error); + + g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy); + + /* Create failed */ + if (ret == FALSE) { + //char *text; + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_error_free (error); + g_free (path); + return; + } + + turn_off_pairing (user_data, path); + + //FIXME show an error? + + /* translators: + * The '%s' is the device name, for example: + * Setting up 'Sony Bluetooth Headset' failed + */ + //text = g_strdup_printf(_("Setting up '%s' failed"), target_name); + + g_warning ("Setting up %s failed: %s", path, error->message); + + //gtk_label_set_markup(GTK_LABEL(label_summary), text); + //g_free (text); + + g_error_free (error); + g_free (path); + return; + } + + priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + + bluetooth_client_set_trusted (BLUETOOTH_CLIENT (source_object), path, TRUE); + + data = g_new0 (SetupConnectData, 1); + data->self = user_data; + data->device = path; + data->timer = g_timer_new (); + + bluetooth_client_connect_service (BLUETOOTH_CLIENT (source_object), + path, TRUE, priv->cancellable, connect_callback, data); + //gtk_assistant_set_current_page (window_assistant, PAGE_FINISHING); +} + +static void +start_pairing (BluetoothSettingsWidget *self, + GtkListBoxRow *row) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GDBusProxy *proxy; + gboolean pair = TRUE; + BluetoothType type; + char *bdaddr, *name; + gboolean legacy_pairing; + + g_object_set (G_OBJECT (row), "pairing", TRUE, NULL); + g_object_get (G_OBJECT (row), + "proxy", &proxy, + "type", &type, + "address", &bdaddr, + "name", &name, + "legacy-pairing", &legacy_pairing, + NULL); + + if (legacy_pairing) { + const char *pincode; + + pincode = get_pincode_for_device (type, + bdaddr, + name, + NULL, + NULL); + if (g_strcmp0 (pincode, "NULL") == 0) + pair = FALSE; + } + + g_debug ("About to setup %s (legacy pairing: %d pair: %d)", + g_dbus_proxy_get_object_path (proxy), + legacy_pairing, pair); + + bluetooth_client_setup_device (priv->client, + g_dbus_proxy_get_object_path (proxy), + pair, + priv->cancellable, + (GAsyncReadyCallback) create_callback, + self); + g_object_unref (proxy); +} + +static void +switch_connected_active_changed (GtkSwitch *button, + GParamSpec *spec, + BluetoothSettingsWidget *self) +{ + ConnectData *data; + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + + if (is_connecting (self, priv->selected_bdaddr)) + return; + + data = g_new0 (ConnectData, 1); + data->bdaddr = g_strdup (priv->selected_bdaddr); + data->self = self; + + bluetooth_client_connect_service (priv->client, + priv->selected_object_path, + gtk_switch_get_active (button), + priv->cancellable, + connect_done, + data); + + add_connecting (self, data->bdaddr); + set_connecting_page (self, CONNECTING_NOTEBOOK_PAGE_SPINNER); +} + +static void +update_properties (BluetoothSettingsWidget *self, + GDBusProxy *proxy) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkSwitch *button; + GtkTreeModel *model; + GtkTreeIter iter; + gboolean ret; + BluetoothType type; + gboolean connected, paired; + char **uuids, *bdaddr, *name, *icon; + guint i; + + model = bluetooth_client_get_device_model (priv->client); + g_assert (model); + + ret = gtk_tree_model_get_iter_first (model, &iter); + while (ret) { + GDBusProxy *p; + + gtk_tree_model_get (model, &iter, + BLUETOOTH_COLUMN_PROXY, &p, + -1); + + if (g_strcmp0 (g_dbus_proxy_get_object_path (proxy), + g_dbus_proxy_get_object_path (p)) == 0) { + g_object_unref (p); + break; + } + + g_object_unref (p); + + ret = gtk_tree_model_iter_next (model, &iter); + } + + /* This means we've found the device */ + g_assert (ret); + + gtk_tree_model_get (model, &iter, + BLUETOOTH_COLUMN_ADDRESS, &bdaddr, + BLUETOOTH_COLUMN_NAME, &name, + BLUETOOTH_COLUMN_ICON, &icon, + BLUETOOTH_COLUMN_PAIRED, &paired, + BLUETOOTH_COLUMN_CONNECTED, &connected, + BLUETOOTH_COLUMN_UUIDS, &uuids, + BLUETOOTH_COLUMN_TYPE, &type, + -1); + if (priv->debug) + bluetooth_client_dump_device (model, &iter); + g_object_unref (model); + + g_free (priv->selected_object_path); + priv->selected_object_path = g_strdup (g_dbus_proxy_get_object_path (proxy)); + + /* Hide all the buttons now, and show them again if we need to */ + gtk_widget_hide (WID ("keyboard_button")); + gtk_widget_hide (WID ("sound_button")); + gtk_widget_hide (WID ("mouse_button")); + gtk_widget_hide (WID ("send_button")); + + /* Name */ + gtk_label_set_text (GTK_LABEL (priv->properties_title), name); + g_free (priv->selected_name); + priv->selected_name = name; + + /* Icon */ + gtk_image_set_from_icon_name (GTK_IMAGE (WID ("image")), icon, GTK_ICON_SIZE_DIALOG); + + /* Connection */ + button = GTK_SWITCH (WID ("switch_connection")); + g_signal_handlers_block_by_func (button, switch_connected_active_changed, self); + + if (is_connecting (self, bdaddr)) { + gtk_switch_set_active (button, TRUE); + set_connecting_page (self, CONNECTING_NOTEBOOK_PAGE_SPINNER); + } else { + gtk_switch_set_active (button, connected); + set_connecting_page (self, CONNECTING_NOTEBOOK_PAGE_SWITCH); + } + + g_signal_handlers_unblock_by_func (button, switch_connected_active_changed, self); + + /* Paired */ + gtk_label_set_text (GTK_LABEL (WID ("paired_label")), + paired ? _("Yes") : _("No")); + + /* UUIDs */ + gtk_widget_set_sensitive (GTK_WIDGET (button), + bluetooth_client_get_connectable ((const char **) uuids)); + for (i = 0; uuids && uuids[i] != NULL; i++) { + if (g_str_equal (uuids[i], "OBEXObjectPush")) { + gtk_widget_show (WID ("send_button")); + break; + } + } + + /* Type */ + gtk_label_set_text (GTK_LABEL (WID ("type_label")), bluetooth_type_to_string (type)); + switch (type) { + case BLUETOOTH_TYPE_KEYBOARD: + gtk_widget_show (WID ("keyboard_button")); + break; + case BLUETOOTH_TYPE_MOUSE: + case BLUETOOTH_TYPE_TABLET: + gtk_widget_show (WID ("mouse_button")); + break; + case BLUETOOTH_TYPE_HEADSET: + case BLUETOOTH_TYPE_HEADPHONES: + case BLUETOOTH_TYPE_OTHER_AUDIO: + gtk_widget_show (WID ("sound_button")); + default: + /* others? */ + ; + } + + /* Address */ + gtk_label_set_text (GTK_LABEL (WID ("address_label")), bdaddr); + + g_free (priv->selected_bdaddr); + priv->selected_bdaddr = bdaddr; + + g_free (icon); + g_strfreev (uuids); +} + +static void +switch_panel (BluetoothSettingsWidget *self, + const char *panel) +{ + g_signal_emit (G_OBJECT (self), + signals[PANEL_CHANGED], + 0, panel); +} + +static gboolean +keyboard_callback (GtkButton *button, + BluetoothSettingsWidget *self) +{ + switch_panel (self, KEYBOARD_PREFS); + return TRUE; +} + +static gboolean +mouse_callback (GtkButton *button, + BluetoothSettingsWidget *self) +{ + switch_panel (self, MOUSE_PREFS); + return TRUE; +} + +static gboolean +sound_callback (GtkButton *button, + BluetoothSettingsWidget *self) +{ + switch_panel (self, SOUND_PREFS); + return TRUE; +} + +static void +send_callback (GtkButton *button, + BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + bluetooth_send_to_address (priv->selected_bdaddr, priv->selected_name); +} + +/* Visibility/Discoverable */ +static void +update_visibility (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + char *name, *label; + + g_object_get (G_OBJECT (priv->client), "default-adapter-name", &name, NULL); + /* translators: %s is the name of the computer, for example: + * Now visible as “Bastien Nocera's Computer” */ + label = g_strdup_printf (_("Now visible as “%s”"), name); + g_free (name); + gtk_label_set_text (GTK_LABEL (priv->visible_label), label); + g_free (label); +} + +static void +name_changed (BluetoothClient *client, + GParamSpec *spec, + BluetoothSettingsWidget *self) +{ + update_visibility (self); +} + +static gboolean +show_confirm_dialog (BluetoothSettingsWidget *self, + const char *name) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *dialog; + gint response; + + dialog = gtk_message_dialog_new (GTK_WINDOW (priv->properties_dialog), GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, + _("Remove '%s' from the list of devices?"), name); + g_object_set (G_OBJECT (dialog), "secondary-text", + _("If you remove the device, you will have to set it up again before next use."), + NULL); + + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Cancel"), GTK_RESPONSE_CANCEL); + gtk_dialog_add_button (GTK_DIALOG (dialog), _("_Remove"), GTK_RESPONSE_ACCEPT); + + response = gtk_dialog_run (GTK_DIALOG (dialog)); + + gtk_widget_destroy (dialog); + + if (response == GTK_RESPONSE_ACCEPT) + return TRUE; + + return FALSE; +} + +static gboolean +remove_selected_device (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + char *adapter; + GDBusProxy *adapter_proxy; + GError *error = NULL; + GVariant *ret; + + g_object_get (G_OBJECT (priv->client), "default-adapter", &adapter, NULL); + adapter_proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + NULL, + BLUEZ_SERVICE, + adapter, + ADAPTER_IFACE, + NULL, + &error); + g_free (adapter); + if (adapter_proxy == NULL) { + g_warning ("Failed to create a GDBusProxy for the default adapter: %s", error->message); + g_error_free (error); + return FALSE; + } + + //FIXME use adapter1_call_remove_device_sync() + ret = g_dbus_proxy_call_sync (G_DBUS_PROXY (adapter_proxy), + "RemoveDevice", + g_variant_new ("(o)", priv->selected_object_path), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &error); + + if (ret == NULL) { + g_warning ("Failed to remove device '%s': %s", + priv->selected_object_path, error->message); + g_error_free (error); + } else { + g_variant_unref (ret); + } + + g_object_unref (adapter_proxy); + + return (ret != NULL); +} + +static void +delete_clicked (GtkButton *button, + BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + if (show_confirm_dialog (self, priv->selected_name) != FALSE) { + remove_selected_device (self); + gtk_widget_hide (priv->properties_dialog); + } +} + +static void +default_adapter_changed (BluetoothClient *client, + GParamSpec *spec, + BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + char *default_adapter; + + g_debug ("Default adapter changed"); + + g_object_get (priv->client, "default-adapter", &default_adapter, NULL); + + g_object_set (G_OBJECT (client), "default-adapter-discovering", default_adapter != NULL, NULL); + + /* FIXME: This should turn off automatically when + * the settings panel goes away */ + g_object_set (G_OBJECT (client), "default-adapter-discoverable", default_adapter != NULL, NULL); +} + +static gint +device_sort_func (gconstpointer a, gconstpointer b, gpointer data) +{ + GObject *row_a = (GObject*)a; + GObject *row_b = (GObject*)b; + gboolean setup_a, setup_b; + gboolean paired_a, paired_b; + gboolean trusted_a, trusted_b; + gboolean connected_a, connected_b; + char *name_a, *name_b; + int ret; + + g_object_get (row_a, + "paired", &paired_a, + "trusted", &trusted_a, + "connected", &connected_a, + "name", &name_a, + NULL); + g_object_get (row_b, + "paired", &paired_b, + "trusted", &trusted_b, + "connected", &connected_b, + "name", &name_b, + NULL); + + /* First, paired or trusted devices (setup devices) */ + setup_a = paired_a || trusted_a; + setup_b = paired_b || trusted_b; + if (setup_a != setup_b) { + if (setup_a) + ret = -1; + else + ret = 1; + goto out; + } + + /* Then connected ones */ + if (connected_a != connected_b) { + if (connected_a) + ret = -1; + else + ret = 1; + goto out; + } + + /* And all being equal, alphabetically */ + ret = g_utf8_collate (name_a, name_b); + +out: + g_free (name_a); + g_free (name_b); + return ret; +} + +static void +update_header_func (GtkListBoxRow *row, + GtkListBoxRow *before, + gpointer user_data) +{ + GtkWidget *current; + + if (before == NULL) + return; + + current = gtk_list_box_row_get_header (row); + if (current == NULL) { + current = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_show (current); + gtk_list_box_row_set_header (row, current); + } +} + +static gboolean +keynav_failed (GtkWidget *list, GtkDirectionType direction, BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *next_list = NULL; + GList *item, *boxes_list; + gdouble value, lower, upper, page; + + /* Find the list in the list of GtkListBoxes */ + if (direction == GTK_DIR_DOWN) + boxes_list = priv->boxes; + else + boxes_list = priv->boxes_reverse; + + item = g_list_find (boxes_list, list); + g_assert (item); + item = item->next; + while (1) + { + if (item == NULL) + item = boxes_list; + + /* Avoid looping */ + if (item->data == list) + break; + + if (gtk_widget_is_visible (item->data)) + { + next_list = item->data; + break; + } + + item = item->next; + } + + if (next_list) + { + gtk_widget_child_focus (next_list, direction); + return TRUE; + } + + value = gtk_adjustment_get_value (priv->focus_adjustment); + lower = gtk_adjustment_get_lower (priv->focus_adjustment); + upper = gtk_adjustment_get_upper (priv->focus_adjustment); + page = gtk_adjustment_get_page_size (priv->focus_adjustment); + + if (direction == GTK_DIR_UP && value > lower) + { + gtk_adjustment_set_value (priv->focus_adjustment, lower); + return TRUE; + } + else if (direction == GTK_DIR_DOWN && value < upper - page) + { + gtk_adjustment_set_value (priv->focus_adjustment, upper - page); + return TRUE; + } + + return FALSE; +} + +static void +activate_row (BluetoothSettingsWidget *self, + GtkListBoxRow *row) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *w; + GtkWidget *toplevel; + gboolean paired, trusted, is_setup; + + g_object_get (G_OBJECT (row), + "paired", &paired, + "trusted", &trusted, + NULL); + is_setup = paired || trusted; + + if (is_setup) { + GDBusProxy *proxy; + + //FIXME pass the row + //FIXME add UUIDs to the row + //FIXME add icon to the row + g_object_get (G_OBJECT (proxy), "proxy", &proxy, NULL); + update_properties (self, proxy); + g_object_unref (proxy); + + w = priv->properties_dialog; + toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self)); + gtk_window_set_transient_for (GTK_WINDOW (w), GTK_WINDOW (toplevel)); + gtk_window_set_modal (GTK_WINDOW (w), TRUE); + gtk_window_present (GTK_WINDOW (w)); + } else { + char *name; + + g_object_get (G_OBJECT (row), "name", &name, NULL); + g_debug ("Start pairing '%s'", name); + g_free (name); + + start_pairing (self, row); + } +} + +static void +add_device_section (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *vbox; + GtkWidget *widget, *box, *hbox, *spinner; + GtkWidget *frame; + gchar *s; + + vbox = WID ("vbox_bluetooth"); + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + gtk_widget_set_margin_start (box, 50); + gtk_widget_set_margin_end (box, 50); + gtk_widget_set_margin_top (box, 6); + gtk_widget_set_margin_bottom (box, 24); + gtk_box_pack_start (GTK_BOX (vbox), box, FALSE, TRUE, 0); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, TRUE, 0); + + s = g_markup_printf_escaped ("<b>%s</b>", _("Devices")); + widget = gtk_label_new (s); + g_free (s); + gtk_label_set_use_markup (GTK_LABEL (widget), TRUE); + gtk_misc_set_alignment (GTK_MISC (widget), 0, 0.5); + gtk_widget_set_margin_start (widget, 6); + gtk_widget_set_margin_end (widget, 6); + gtk_widget_set_margin_bottom (widget, 6); + gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, TRUE, 0); + + priv->device_spinner = spinner = gtk_spinner_new (); + g_object_bind_property (G_OBJECT (priv->client), "default-adapter-discovering", + G_OBJECT (priv->device_spinner), "active", + G_BINDING_SYNC_CREATE); + gtk_widget_set_margin_bottom (spinner, 6); + gtk_box_pack_start (GTK_BOX (hbox), spinner, FALSE, TRUE, 0); + + priv->device_list = widget = gtk_list_box_new (); + priv->boxes_reverse = g_list_prepend (priv->boxes_reverse, priv->device_list); + g_signal_connect (widget, "keynav-failed", G_CALLBACK (keynav_failed), self); + gtk_list_box_set_selection_mode (GTK_LIST_BOX (widget), GTK_SELECTION_NONE); + gtk_list_box_set_header_func (GTK_LIST_BOX (widget), + update_header_func, + NULL, NULL); + gtk_list_box_set_sort_func (GTK_LIST_BOX (widget), + (GtkListBoxSortFunc)device_sort_func, NULL, NULL); + g_signal_connect_swapped (widget, "row-activated", + G_CALLBACK (activate_row), self); + + priv->device_revealer = gtk_revealer_new (); + + frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_IN); + gtk_container_add (GTK_CONTAINER (frame), widget); + gtk_container_add (GTK_CONTAINER (priv->device_revealer), frame); + gtk_box_pack_start (GTK_BOX (box), priv->device_revealer, FALSE, TRUE, 0); + + gtk_widget_show_all (box); +} + +static void +on_content_size_changed (GtkWidget *widget, GtkAllocation *allocation, gpointer data) +{ + GtkWidget *box; + + box = gtk_widget_get_parent (gtk_widget_get_parent (widget)); + if (allocation->height < 490) { + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_NEVER, GTK_POLICY_NEVER); + } else { + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (box), 490); + } +} + +static gboolean +is_interesting_device (GtkTreeModel *model, + GtkTreeIter *iter) +{ + GtkTreeIter parent_iter; + gboolean is_default; + + /* Not a child */ + if (gtk_tree_model_iter_parent (model, &parent_iter, iter) == FALSE) + return FALSE; + + /* Not the default adapter */ + gtk_tree_model_get (model, &parent_iter, + BLUETOOTH_COLUMN_DEFAULT, &is_default, + -1); + return is_default; +} + +static void +row_inserted_cb (GtkTreeModel *tree_model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GDBusProxy *proxy; + char *name, *bdaddr; + BluetoothType type; + gboolean paired, trusted, connected, legacy_pairing; + GtkWidget *row; + + if (is_interesting_device (tree_model, iter) == FALSE) { + gtk_tree_model_get (tree_model, iter, + BLUETOOTH_COLUMN_NAME, &name, + -1); + g_debug ("Not adding device '%s'", name); + g_free (name); + return; + } + + gtk_tree_model_get (tree_model, iter, + BLUETOOTH_COLUMN_PROXY, &proxy, + BLUETOOTH_COLUMN_NAME, &name, + BLUETOOTH_COLUMN_PAIRED, &paired, + BLUETOOTH_COLUMN_TRUSTED, &trusted, + BLUETOOTH_COLUMN_CONNECTED, &connected, + BLUETOOTH_COLUMN_ADDRESS, &bdaddr, + BLUETOOTH_COLUMN_TYPE, &type, + BLUETOOTH_COLUMN_LEGACYPAIRING, &legacy_pairing, + -1); + + g_debug ("Adding device %s (%s)", name, g_dbus_proxy_get_object_path (proxy)); + + row = g_object_new (BLUETOOTH_TYPE_SETTINGS_ROW, + "proxy", proxy, + "paired", paired, + "trusted", trusted, + "type", type, + "connected", connected, + "name", name, + "address", bdaddr, + "legacy-pairing", legacy_pairing, + NULL); + g_object_set_data_full (G_OBJECT (row), "object-path", g_strdup (g_dbus_proxy_get_object_path (proxy)), g_free); + + gtk_container_add (GTK_CONTAINER (priv->device_list), row); + gtk_size_group_add_widget (priv->row_sizegroup, row); + + g_object_unref (proxy); + g_free (name); + g_free (bdaddr); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->device_revealer), TRUE); +} + +static void +row_changed_cb (GtkTreeModel *tree_model, + GtkTreePath *path, + GtkTreeIter *iter, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GDBusProxy *proxy; + GList *l, *children; + const char *object_path; + + if (is_interesting_device (tree_model, iter) == FALSE) { + char *name; + + gtk_tree_model_get (tree_model, iter, + BLUETOOTH_COLUMN_NAME, &name, + -1); + g_debug ("Not interested in device '%s'", name); + g_free (name); + return; + } + + gtk_tree_model_get (tree_model, iter, + BLUETOOTH_COLUMN_PROXY, &proxy, + -1); + object_path = g_dbus_proxy_get_object_path (proxy); + + children = gtk_container_get_children (GTK_CONTAINER (priv->device_list)); + for (l = children; l != NULL; l = l->next) { + const char *path; + + path = g_object_get_data (G_OBJECT (l->data), "object-path"); + if (g_str_equal (object_path, path)) { + char *name, *bdaddr; + BluetoothType type; + gboolean paired, trusted, connected, legacy_pairing; + + gtk_tree_model_get (tree_model, iter, + BLUETOOTH_COLUMN_NAME, &name, + BLUETOOTH_COLUMN_PAIRED, &paired, + BLUETOOTH_COLUMN_TRUSTED, &trusted, + BLUETOOTH_COLUMN_CONNECTED, &connected, + BLUETOOTH_COLUMN_ADDRESS, &bdaddr, + BLUETOOTH_COLUMN_TYPE, &type, + BLUETOOTH_COLUMN_LEGACYPAIRING, &legacy_pairing, + -1); + + g_object_set (G_OBJECT (l->data), + "paired", paired, + "trusted", trusted, + "type", type, + "connected", connected, + "name", name, + "legacy-pairing", legacy_pairing, + NULL); + break; + } + } + g_list_free (children); + g_object_unref (proxy); +} + +static void +device_removed_cb (BluetoothClient *client, + const char *object_path, + gpointer user_data) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (user_data); + GList *children, *l; + + children = gtk_container_get_children (GTK_CONTAINER (priv->device_list)); + for (l = children; l != NULL; l = l->next) { + const char *path; + + path = g_object_get_data (G_OBJECT (l->data), "object-path"); + if (g_str_equal (path, object_path)) { + char *name; + + g_object_get (G_OBJECT (l->data), "name", &name, NULL); + g_debug ("Removing device '%s'", name); + g_free (name); + + gtk_widget_destroy (GTK_WIDGET (l->data)); + return; + } + } + + g_debug ("Didn't find a row to remove for tree path %s", object_path); +} + +static void +setup_properties_dialog (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *container, *buttonbox, *header, *done; + GtkStyleContext *context; + + priv->properties_dialog = gtk_dialog_new (); + gtk_widget_set_size_request (priv->properties_dialog, 380, -1); + gtk_window_set_resizable (GTK_WINDOW (priv->properties_dialog), FALSE); + container = gtk_dialog_get_content_area (GTK_DIALOG (priv->properties_dialog)); + buttonbox = gtk_dialog_get_action_area (GTK_DIALOG (priv->properties_dialog)); + priv->properties_header = header = gtk_header_bar_new (); + done = gtk_button_new_with_label (_("Done")); + gtk_header_bar_pack_end (GTK_HEADER_BAR (header), done); + gtk_widget_show_all (header); + priv->properties_title = gtk_label_new (""); + gtk_header_bar_set_custom_title (GTK_HEADER_BAR (header), priv->properties_title); + gtk_window_set_titlebar (GTK_WINDOW (priv->properties_dialog), header); + gtk_container_add (GTK_CONTAINER (container), WID ("properties_vbox")); + gtk_widget_hide (buttonbox); + gtk_widget_set_no_show_all (buttonbox, FALSE); + + g_signal_connect (G_OBJECT (priv->properties_dialog), "delete-event", + G_CALLBACK (gtk_widget_hide_on_delete), NULL); + g_signal_connect_swapped (G_OBJECT (done), "clicked", + G_CALLBACK (gtk_widget_hide_on_delete), priv->properties_dialog); + g_signal_connect (G_OBJECT (WID ("delete_button")), "clicked", + G_CALLBACK (delete_clicked), self); + g_signal_connect (G_OBJECT (WID ("mouse_button")), "clicked", + G_CALLBACK (mouse_callback), self); + g_signal_connect (G_OBJECT (WID ("keyboard_button")), "clicked", + G_CALLBACK (keyboard_callback), self); + g_signal_connect (G_OBJECT (WID ("sound_button")), "clicked", + G_CALLBACK (sound_callback), self); + g_signal_connect (G_OBJECT (WID ("send_button")), "clicked", + G_CALLBACK (send_callback), self); + g_signal_connect (G_OBJECT (WID ("switch_connection")), "notify::active", + G_CALLBACK (switch_connected_active_changed), self); + + /* Styling */ + gtk_image_set_pixel_size (GTK_IMAGE (WID ("image")), ICON_SIZE); + + context = gtk_widget_get_style_context (WID ("delete_button")); + gtk_style_context_add_class (context, "destructive-action"); + context = gtk_widget_get_style_context (done); + gtk_style_context_add_class (context, "suggested-action"); + context = gtk_widget_get_style_context (priv->properties_title); + gtk_style_context_add_class (context, "title"); +} + +static void +setup_pairing_agent (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + + priv->agent = bluetooth_agent_new (); + if (bluetooth_agent_register (priv->agent) == FALSE) + return; + + g_object_add_weak_pointer (G_OBJECT (priv->agent), (gpointer *) (&priv->agent)); + + bluetooth_agent_set_pincode_func (priv->agent, pincode_callback, self); + bluetooth_agent_set_display_func (priv->agent, display_callback, self); + bluetooth_agent_set_display_pincode_func (priv->agent, display_pincode_callback, self); + bluetooth_agent_set_cancel_func (priv->agent, cancel_callback, self); + bluetooth_agent_set_confirm_func (priv->agent, confirm_callback, self); + bluetooth_agent_set_authorize_func (priv->agent, authorize_callback, self); + bluetooth_agent_set_authorize_service_func (priv->agent, authorize_service_callback, self); + + bluetooth_agent_setup (priv->agent, AGENT_PATH); +} + +static void +bluetooth_settings_widget_init (BluetoothSettingsWidget *self) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (self); + GtkWidget *widget, *box; + GError *error = NULL; + + priv->cancellable = g_cancellable_new (); + priv->debug = g_getenv ("BLUETOOTH_DEBUG") != NULL; + + g_resources_register (bluetooth_settings_get_resource ()); + priv->builder = gtk_builder_new (); + gtk_builder_set_translation_domain (priv->builder, GETTEXT_PACKAGE); + gtk_builder_add_from_resource (priv->builder, + "/org/gnome/bluetooth/settings.ui", + &error); + if (error != NULL) { + g_warning ("Could not load ui: %s", error->message); + g_error_free (error); + return; + } + + widget = WID ("vbox_bluetooth"); + + priv->connecting_devices = g_hash_table_new_full (g_str_hash, + g_str_equal, + (GDestroyNotify) g_free, + NULL); + + setup_pairing_agent (self); + priv->client = bluetooth_client_new (); + g_signal_connect (G_OBJECT (priv->client), "notify::default-adapter-name", + G_CALLBACK (name_changed), self); + priv->model = bluetooth_client_get_model (priv->client); + g_signal_connect (priv->model, "row-changed", + G_CALLBACK (row_changed_cb), self); + g_signal_connect (priv->model, "row-inserted", + G_CALLBACK (row_inserted_cb), self); + g_signal_connect (priv->client, "device-removed", + G_CALLBACK (device_removed_cb), self); + g_signal_connect (G_OBJECT (priv->client), "notify::default-adapter", + G_CALLBACK (default_adapter_changed), self); + default_adapter_changed (priv->client, NULL, self); + + priv->row_sizegroup = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL); + + add_device_section (self); + + priv->boxes = g_list_copy (priv->boxes_reverse); + priv->boxes = g_list_reverse (priv->boxes); + + box = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_hexpand (box, TRUE); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (box), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + g_signal_connect (widget, "size-allocate", + G_CALLBACK (on_content_size_changed), NULL); + gtk_widget_show (box); + gtk_container_add (GTK_CONTAINER (self), box); + gtk_container_add (GTK_CONTAINER (box), widget); + + priv->focus_adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (box)); + gtk_container_set_focus_vadjustment (GTK_CONTAINER (widget), priv->focus_adjustment); + + /* Discoverable label */ + priv->visible_label = gtk_label_new ("Now visible as “Foobar”"); + gtk_label_set_use_markup (GTK_LABEL (priv->visible_label), TRUE); + gtk_misc_set_alignment (GTK_MISC (priv->visible_label), 0.5, 0.5); + gtk_box_pack_start (GTK_BOX (widget), priv->visible_label, FALSE, TRUE, 0); + update_visibility (self); + + setup_properties_dialog (self); + + gtk_widget_show_all (GTK_WIDGET (self)); +} + +static void +bluetooth_settings_widget_finalize (GObject *object) +{ + BluetoothSettingsWidgetPrivate *priv = BLUETOOTH_SETTINGS_WIDGET_GET_PRIVATE (object); + + g_clear_pointer (&priv->properties_dialog, gtk_widget_destroy); + g_clear_pointer (&priv->pairing_dialog, gtk_widget_destroy); + + /* See default_adapter_changed () */ + if (priv->client) + g_object_set (G_OBJECT (priv->client), "default-adapter-discoverable", FALSE, NULL); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + + g_clear_object (&priv->model); + g_clear_object (&priv->client); + g_clear_object (&priv->builder); + + g_clear_pointer (&priv->connecting_devices, g_hash_table_destroy); + g_clear_pointer (&priv->selected_name, g_free); + g_clear_pointer (&priv->selected_object_path, g_free); + + G_OBJECT_CLASS(bluetooth_settings_widget_parent_class)->finalize(object); +} + +static void +bluetooth_settings_widget_class_init (BluetoothSettingsWidgetClass *klass) +{ + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + + g_type_class_add_private (klass, sizeof (BluetoothSettingsWidgetPrivate)); + + G_OBJECT_CLASS (klass)->finalize = bluetooth_settings_widget_finalize; + + /** + * BluetoothSettingsWidget::panel-changed: + * @chooser: a #BluetoothSettingsWidget widget which received the signal + * @panel: the new panel that the Settings application should now open + * + * The #BluetoothChooser::selected-device-changed signal is launched when a + * link to another settings panel is clicked. + **/ + signals[PANEL_CHANGED] = + g_signal_new ("panel-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, G_TYPE_STRING); +} + +/** + * bluetooth_settings_widget_new: + * + * Returns a new #BluetoothSettingsWidget widget. + * + * Return value: A #BluetoothSettingsWidget widget + **/ +GtkWidget * +bluetooth_settings_widget_new (void) +{ + return g_object_new (BLUETOOTH_TYPE_SETTINGS_WIDGET, NULL); +} |