diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eeff37511e189ee26e1603e50fcbc5f0941a94ba..6d58a8b97d359c376c0089c716c0b94b81814a2b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,7 +52,7 @@ variables: gtk+3.0-dev libgudev-dev libhandy1-dev gcr-dev libsecret-dev gcovr linux-pam-dev meson musl-dev networkmanager networkmanager-dev ninja polkit-elogind-dev pulseaudio-dev upower-dev wayland-dev wayland-protocols ttf-dejavu evolution-data-server-dev evince-dev - libadwaita-dev json-glib-dev + libadwaita-dev json-glib-dev gnome-bluetooth-dev RUST_BINDINGS_BRANCH: main .trixie_vars: &trixie_vars @@ -268,6 +268,10 @@ check-dist: stage: test+docs image: ${DEBIAN_IMAGE} <<: *trixie_vars + before_script: + - export DEBIAN_FRONTEND=noninteractive + - apt -y update + - apt -y build-dep . needs: [] script: - meson setup --werror _build-dist diff --git a/HACKING.md b/HACKING.md index 831d2361e751eed09af196b8e4b373a96236128b..61cbd11777f93a25ab1a874589379a949951b48e 100644 --- a/HACKING.md +++ b/HACKING.md @@ -42,6 +42,8 @@ When submitting a merge request consider checking these first: an added [screenshot test](./tests/test-take-screenshots.c), a tool to exercise new DBus API (see e.g. [tools/check-mount-operation](./tools/check-mount-operation). +- [ ] Are property assignments to default values removed from UI files? (See + `gtk-builder-tool simplify file.ui`) If any of the above criteria aren't met yet it's still fine (and encouraged) to open a merge request marked as draft. Please indicate diff --git a/debian/control b/debian/control index 330044ea3b295a64872d6f5af8fe7ebcb71577c1..603d7803726cef4ed517f3f0075df5bbc05a55b1 100644 --- a/debian/control +++ b/debian/control @@ -15,6 +15,7 @@ Build-Depends: libfeedback-dev (>= 0.2.0), libfribidi-dev, libgcr-3-dev, + libgnome-bluetooth-3.0-dev, libgnome-desktop-3-dev, libgtk-3-dev, libgtk-4-dev, diff --git a/meson.build b/meson.build index 3a9537e95f83332a938636d59cfb92a3df6ed796..ab1030c622af70a3f0914fc24ca58240ec4c79cd 100644 --- a/meson.build +++ b/meson.build @@ -129,6 +129,7 @@ gcr_dep = dependency('gcr-3', version: '>= 3.7.5') glib_dep = dependency('glib-2.0', version: glib_ver_cmp) gio_dep = dependency('gio-2.0', version: glib_ver_cmp) gio_unix_dep = dependency('gio-unix-2.0', version: glib_ver_cmp) +gnome_bluetooth_dep = dependency ('gnome-bluetooth-3.0', version: '>= 46.0') gmobile_dep = dependency('gmobile', version: '>= 0.1.0', fallback: ['gmobile', 'gmobile_dep'], diff --git a/plugins/launcher-box/launcher-box.ui b/plugins/launcher-box/launcher-box.ui index bea7540d583af691e39f4e7edf8220470ff09251..6e14c884897dcf252d0c4e522415bddeb431222b 100644 --- a/plugins/launcher-box/launcher-box.ui +++ b/plugins/launcher-box/launcher-box.ui @@ -30,7 +30,6 @@ True Launchers - True True diff --git a/po/POTFILES.in b/po/POTFILES.in index 7291b355257396a9a955e852eb11d387619057cd..9e90331c737a65f8d89bc64ebe4d76eaadf1db45 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -22,7 +22,9 @@ src/auth.c src/background.c src/background-manager.c src/batteryinfo.c +src/bt-device-row.c src/bt-info.c +src/bt-status-page.c src/call-notification.c src/docked-info.c src/emergency-menu.c @@ -73,6 +75,7 @@ src/ui/app-auth-prompt.ui src/ui/app-grid-button.ui src/ui/app-grid.ui src/ui/audio-settings.ui +src/ui/bt-status-page.ui src/ui/emergency-menu.ui src/ui/emergency-contact-row.ui src/ui/end-session-dialog.ui diff --git a/src/bt-device-row.c b/src/bt-device-row.c new file mode 100644 index 0000000000000000000000000000000000000000..8f06b485eb27be6fa084488a6db6f2dca1ab24bb --- /dev/null +++ b/src/bt-device-row.c @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-device-row" + +#include "bt-device-row.h" +#include "shell.h" + +/** + * PhoshBtDeviceRow: + * + * A widget to display a Bluetooth device + */ + +enum { + PROP_0, + PROP_DEVICE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBtDeviceRow { + HdyActionRow parent; + + GtkWidget *icon; + GtkSpinner *spinner; + GtkRevealer *revealer; + double bat_percentage; + + BluetoothDevice *device; + + GCancellable *cancellable; +}; + +G_DEFINE_TYPE (PhoshBtDeviceRow, phosh_bt_device_row, HDY_TYPE_ACTION_ROW); + + +static void +bat_level_cb (PhoshBtDeviceRow *self, GParamSpec *pspec, BluetoothDevice *device) +{ + double current; + gboolean connected; + BluetoothBatteryType type; + g_autofree char *subtitle = NULL; + + g_assert (PHOSH_IS_BT_DEVICE_ROW (self)); + g_assert (BLUETOOTH_IS_DEVICE (device)); + + g_object_get (device, + "battery-percentage", ¤t, + "battery-type", &type, + "connected", &connected, + NULL); + + if (!connected || type != BLUETOOTH_BATTERY_TYPE_PERCENTAGE) { + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), ""); + self->bat_percentage = -1.0; + return; + } + + current = round (current); + if (G_APPROX_VALUE (round (self->bat_percentage), current, FLT_EPSILON)) + return; + + self->bat_percentage = current; + /* Translators: a battery level in percent */ + subtitle = g_strdup_printf (_("Battery %.0f%%"), self->bat_percentage); + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), subtitle); +} + + +static void +phosh_bt_device_row_set_device (PhoshBtDeviceRow *self, BluetoothDevice *device) +{ + g_set_object (&self->device, device); + + g_object_bind_property (self->device, + "connectable", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "name", + self, + "title", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "icon", + self->icon, + "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "connected", + self->revealer, + "reveal-child", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_connect (self->device, + "swapped-object-signal::notify::battery-percentage", bat_level_cb, self, + "swapped-object-signal::notify::battery-type", bat_level_cb, self, + "swapped-object-signal::notify::connected", bat_level_cb, self, + NULL); + bat_level_cb (self, NULL, device); +} + + +static void +phosh_bt_device_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBtDeviceRow *self = PHOSH_BT_DEVICE_ROW (object); + + switch (property_id) { + case PROP_DEVICE: + phosh_bt_device_row_set_device (self, g_value_dup_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_bt_device_row_dispose (GObject *object) +{ + PhoshBtDeviceRow *self = PHOSH_BT_DEVICE_ROW (object); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + g_clear_object (&self->device); + + G_OBJECT_CLASS (phosh_bt_device_row_parent_class)->dispose (object); +} + + +static void +on_connect_device_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (PhoshBtDeviceRow) self = PHOSH_BT_DEVICE_ROW (user_data); + PhoshBtManager *manager = PHOSH_BT_MANAGER (source_object); + g_autoptr (GError) err = NULL; + gboolean success; + + success = phosh_bt_manager_connect_device_finish (manager, res, &err); + if (!success) + g_warning ("Failed to connect: %s", err->message); + + g_assert (PHOSH_IS_BT_DEVICE_ROW (user_data)); + + gtk_spinner_stop (self->spinner); +} + + +static void +on_bt_row_activated (PhoshBtDeviceRow *self) +{ + PhoshBtManager *manager = phosh_shell_get_bt_manager (phosh_shell_get_default ()); + gboolean connected; + g_autofree char *name = NULL; + + g_object_get (self->device, "connected", &connected, "name", &name, NULL); + + g_cancellable_cancel (self->cancellable); + g_set_object (&self->cancellable, g_cancellable_new ()); + + g_debug ("%sonnecting device %s", !connected ? "C" : "Disc", name); + + gtk_spinner_start (self->spinner); + phosh_bt_manager_connect_device_async (manager, + self->device, + !connected, + on_connect_device_finished, + self->cancellable, + g_object_ref (self)); +} + + +static void +phosh_bt_device_row_class_init (PhoshBtDeviceRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_bt_device_row_set_property; + object_class->dispose = phosh_bt_device_row_dispose; + + /** + * PhoshBtDeviceRow:device: + * + * The bluetooth device represented by the row + */ + props[PROP_DEVICE] = + g_param_spec_object ("device", "", "", + BLUETOOTH_TYPE_DEVICE, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/phosh/ui/bt-device-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, revealer); + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, spinner); + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, icon); + + gtk_widget_class_bind_template_callback (widget_class, on_bt_row_activated); +} + + +static void +phosh_bt_device_row_init (PhoshBtDeviceRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Negative values mean "undefined" */ + self->bat_percentage = -1.0; +} + + +GtkWidget * +phosh_bt_device_row_new (BluetoothDevice *device) +{ + g_assert (BLUETOOTH_IS_DEVICE (device)); + + return GTK_WIDGET (g_object_new (PHOSH_TYPE_BT_DEVICE_ROW, "device", device, NULL)); +} diff --git a/src/bt-device-row.h b/src/bt-device-row.h new file mode 100644 index 0000000000000000000000000000000000000000..dc965e9cd116b997baaaf1052821a31e8d04e946 --- /dev/null +++ b/src/bt-device-row.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "bluetooth-device.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_DEVICE_ROW phosh_bt_device_row_get_type () +G_DECLARE_FINAL_TYPE (PhoshBtDeviceRow, phosh_bt_device_row, PHOSH, BT_DEVICE_ROW, HdyActionRow) + +GtkWidget *phosh_bt_device_row_new (BluetoothDevice *device); + +G_END_DECLS diff --git a/src/bt-info.c b/src/bt-info.c index cc58fdd16a8f3c0fe5f8eccff11b7117a561dd48..6fd3b6b2ec65c813dc14dc61d00296c6eb8d7424 100644 --- a/src/bt-info.c +++ b/src/bt-info.c @@ -14,6 +14,8 @@ #include "bt-info.h" #include "bt-manager.h" +#include "gmobile.h" + /** * PhoshBtInfo: * @@ -82,16 +84,43 @@ update_icon (PhoshBtInfo *self, GParamSpec *pspec, PhoshBtManager *bt) static void update_info (PhoshBtInfo *self) { + g_autofree char *msg = NULL; gboolean enabled; + guint n_connected; g_return_if_fail (PHOSH_IS_BT_INFO (self)); - /* TODO: show number of paired devices */ enabled = phosh_bt_manager_get_enabled (self->bt); - if (enabled) - phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), C_("bluetooth:enabled", "On")); - else + if (!enabled) { phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Bluetooth")); + return; + } + + n_connected = phosh_bt_manager_get_n_connected (self->bt); + switch (n_connected) { + case 0: + break; + case 1: { + const char *info = phosh_bt_manager_get_info (self->bt); + if (gm_str_is_null_or_empty (info)) { + /* Translators: One connected Bluetooth device */ + msg = g_strdup_printf ("One device"); + } else { + msg = g_strdup (info); + } + break; + } + default: + /* Translators: The number of currently connected Bluetooth devices */ + msg = g_strdup_printf ("%d devices", n_connected); + } + + if (msg) { + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), msg); + return; + } + + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), C_("bluetooth:enabled", "On")); } @@ -110,6 +139,8 @@ on_bt_enabled (PhoshBtInfo *self, GParamSpec *pspec, PhoshBtManager *bt) self->enabled = enabled; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); + + update_info (self); } @@ -157,27 +188,12 @@ phosh_bt_info_constructed (GObject *object) return; } - g_signal_connect_swapped (self->bt, - "notify::icon-name", - G_CALLBACK (update_icon), - self); - - /* TODO: track number of BT devices */ - g_signal_connect_swapped (self->bt, - "notify::enabled", - G_CALLBACK (update_info), - self); - - /* We don't use a binding for self->enabled so we can keep - the property r/o */ - g_signal_connect_swapped (self->bt, - "notify::enabled", - G_CALLBACK (on_bt_enabled), - self); - g_signal_connect_swapped (self->bt, - "notify::present", - G_CALLBACK (on_bt_present), - self); + g_object_connect (self->bt, + "swapped-signal::notify::icon-name", update_icon, self, + "swapped-signal::notify::enabled", on_bt_enabled, self, + "swapped-signal::notify::present", on_bt_present, self, + "swapped-signal::notify::n-connected", update_info, self, + NULL); } diff --git a/src/bt-manager.c b/src/bt-manager.c index 7ec1336d7c2ae19f50c4294747eebb43c4f404d5..d8f25d1c039a306f0bd62d9422d840ee194475b5 100644 --- a/src/bt-manager.c +++ b/src/bt-manager.c @@ -1,5 +1,6 @@ /* * Copyright (C) 2020 Purism SPC + * 2024 The Phosh Developers * * SPDX-License-Identifier: GPL-3.0-or-later * @@ -15,10 +16,14 @@ #include "dbus/gsd-rfkill-dbus.h" #include "util.h" +#include "gtk-list-models/gtkfilterlistmodel.h" +#include "gnome-bluetooth-enum-types.h" +#include "bluetooth-client.h" +#include "bluetooth-device.h" + #define BUS_NAME "org.gnome.SettingsDaemon.Rfkill" #define OBJECT_PATH "/org/gnome/SettingsDaemon/Rfkill" - /** * PhoshBtManager: * @@ -33,8 +38,9 @@ enum { PROP_ICON_NAME, PROP_ENABLED, PROP_PRESENT, - /* TODO: keep track of connected devices for quick-settings */ - /* PROP_N_DEVICES */ + PROP_N_DEVICES, + PROP_N_CONNECTED, + PROP_INFO, PROP_LAST_PROP }; static GParamSpec *props[PROP_LAST_PROP]; @@ -42,17 +48,51 @@ static GParamSpec *props[PROP_LAST_PROP]; struct _PhoshBtManager { PhoshManager manager; - /* Whether bt radio is on */ gboolean enabled; - /* Whether we have a bt device is present */ gboolean present; const char *icon_name; + guint n_connected; + guint n_devices; + char *info; + + BluetoothClient *bt_client; + GtkFilterListModel *connectable_devices; PhoshRfkillDBusRfkill *proxy; }; G_DEFINE_TYPE (PhoshBtManager, phosh_bt_manager, PHOSH_TYPE_MANAGER); +static void +on_adapter_setup_mode_changed (PhoshBtManager *self) +{ + gboolean setup_mode; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_CLIENT (self->bt_client)); + + g_object_get (self->bt_client, "default-adapter-setup-mode", &setup_mode, NULL); + + g_debug ("Setup-mode: %d", setup_mode); +} + + +static void +on_adapter_state_changed (PhoshBtManager *self) +{ + BluetoothAdapterState state; + g_autofree char *name = NULL; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_CLIENT (self->bt_client)); + + g_object_get (self->bt_client, "default-adapter-state", &state, NULL); + name = g_enum_to_string (BLUETOOTH_TYPE_ADAPTER_STATE, state); + + g_debug ("State: %s", name); +} + + static void phosh_bt_manager_get_property (GObject *object, guint property_id, @@ -71,6 +111,15 @@ phosh_bt_manager_get_property (GObject *object, case PROP_PRESENT: g_value_set_boolean (value, self->present); break; + case PROP_N_DEVICES: + g_value_set_uint (value, self->n_devices); + break; + case PROP_N_CONNECTED: + g_value_set_uint (value, self->n_connected); + break; + case PROP_INFO: + g_value_set_string (value, self->info); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -188,6 +237,10 @@ phosh_bt_manager_dispose (GObject *object) PhoshBtManager *self = PHOSH_BT_MANAGER (object); g_clear_object (&self->proxy); + g_clear_object (&self->connectable_devices); + g_clear_object (&self->bt_client); + + g_clear_pointer (&self->info, g_free); G_OBJECT_CLASS (phosh_bt_manager_parent_class)->dispose (object); } @@ -205,39 +258,198 @@ phosh_bt_manager_class_init (PhoshBtManagerClass *klass) manager_class->idle_init = phosh_bt_manager_idle_init; + /** + * PhoshBtManager::icon-name: + * + * A icon name that indicates the current Bluetooth status. + */ props[PROP_ICON_NAME] = g_param_spec_string ("icon-name", "icon name", "The bt icon name", "bluetooth-disabled-symbolic", - G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); - + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager::enabled: + * + * Whether a Bluetooth is enabled. + */ props[PROP_ENABLED] = - g_param_spec_boolean ("enabled", - "enabled", - "Whether bluetooth hardware is enabled", + g_param_spec_boolean ("enabled", "", "", FALSE, - G_PARAM_READABLE | - G_PARAM_EXPLICIT_NOTIFY | - G_PARAM_STATIC_STRINGS); - + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager::present: + * + * Whether a Bluetooth adapter is present + */ props[PROP_PRESENT] = - g_param_spec_boolean ("present", - "Present", - "Whether bluettoh hardware is present", + g_param_spec_boolean ("present", "", "", FALSE, - G_PARAM_READABLE | - G_PARAM_EXPLICIT_NOTIFY | - G_PARAM_STATIC_STRINGS); + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:n-devices: + * + * The number of connectable Bluetooth devices + */ + props[PROP_N_DEVICES] = + g_param_spec_uint ("n-devices", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:n-connected: + * + * The number of currently connected Bluetooth devices + */ + props[PROP_N_CONNECTED] = + g_param_spec_uint ("n-connected", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:info: + * + * If only a single device is connected this gives details about it. + */ + props[PROP_INFO] = + g_param_spec_string ("info", "", "", + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, PROP_LAST_PROP, props); } +static gboolean +filter_devices (gpointer item, gpointer data) +{ + gboolean connectable; + BluetoothDevice *device = BLUETOOTH_DEVICE (item); + + g_object_get (device, "connectable", &connectable, NULL); + + return connectable; +} + + +static void +refilter_cb (PhoshBtManager *self) +{ + g_assert (PHOSH_IS_BT_MANAGER (self)); + + gtk_filter_list_model_refilter (self->connectable_devices); +} + + +static void +recount_cb (PhoshBtManager *self) +{ + g_autofree char *last_info = NULL; + guint n_devices, n_connected = 0; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + + n_devices = g_list_model_get_n_items (G_LIST_MODEL (self->connectable_devices)); + for (int i = 0; i < n_devices; i++) { + g_autoptr (BluetoothDevice) device = NULL; + g_autofree char *info = NULL; + gboolean connected; + + device = g_list_model_get_item (G_LIST_MODEL (self->connectable_devices), i); + g_object_get (device, "connected", &connected, "alias", &info, NULL); + + if (connected) { + n_connected++; + last_info = g_steal_pointer (&info); + } + } + + if (g_strcmp0 (self->info, last_info)) { + g_debug ("New info: %s", last_info); + g_free (self->info); + self->info = g_steal_pointer (&last_info); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INFO]); + } + + if (self->n_connected != n_connected) { + g_debug ("%d Bluetooth devices connected", n_connected); + self->n_connected = n_connected; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_CONNECTED]); + } + + if (self->n_devices != n_devices) { + g_debug ("%d Bluetooth devices", n_devices); + self->n_devices = n_devices; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_DEVICES]); + } +} + + +static void +on_device_added (PhoshBtManager *self, BluetoothDevice *device) +{ + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_DEVICE (device)); + + g_signal_connect_swapped (device, "notify::connectable", G_CALLBACK (refilter_cb), self); + g_signal_connect_swapped (device, "notify::connected", G_CALLBACK (recount_cb), self); + + refilter_cb (self); + recount_cb (self); +} + + +static void +on_device_removed (PhoshBtManager *self, BluetoothDevice *device) +{ + refilter_cb (self); + recount_cb (self); +} + + +static void +setup_devices (PhoshBtManager *self) +{ + g_autoptr (GListStore) devices = NULL; + guint n_items; + + /* Keep a list of connectable devices */ + devices = bluetooth_client_get_devices (self->bt_client); + self->connectable_devices = gtk_filter_list_model_new (G_LIST_MODEL (devices), + filter_devices, + self, + NULL); + g_object_connect (self->bt_client, + "swapped-object-signal::device-added", on_device_added, self, + "swapped-object-signal::device-removed", on_device_removed, self, + NULL); + + /* cold plug existing devices */ + n_items = g_list_model_get_n_items (G_LIST_MODEL (devices)); + for (int i = 0; i < n_items; i++) { + g_autoptr (BluetoothDevice) device = NULL; + + device = g_list_model_get_item (G_LIST_MODEL (devices), i); + on_device_added (self, device); + } + + recount_cb (self); +} + + static void phosh_bt_manager_init (PhoshBtManager *self) { self->icon_name = "bluetooth-disabled-symbolic"; + + self->bt_client = bluetooth_client_new (); + g_object_connect (self->bt_client, + "swapped-signal::notify::default-adapter-state", + on_adapter_state_changed, self, + "swapped-signal::notify::default-adapter-setup-mode", + on_adapter_setup_mode_changed, self, + NULL); + + setup_devices (self); } @@ -291,3 +503,93 @@ phosh_bt_manager_get_present (PhoshBtManager *self) return self->present; } + +/** + * phosh_bt_manager_get_connectable_devices: + * @self: The Bluetooth manager + * + * Gets the currently connectable devices. + * + * Returns:(transfer none): The connectable devices + */ +GListModel * +phosh_bt_manager_get_connectable_devices (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), NULL); + + return G_LIST_MODEL (self->connectable_devices); +} + + +guint +phosh_bt_manager_get_n_connected (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), 0); + + return self->n_connected; +} + + +const char * +phosh_bt_manager_get_info (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), NULL); + + return self->info; +} + + +static void +on_service_connected (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GError *err = NULL; + GTask *task = G_TASK (user_data); + gboolean success; + + success = bluetooth_client_connect_service_finish (BLUETOOTH_CLIENT (source_object), res, &err); + if (!success) + g_debug ("Failed to connect: %s", err->message); + + if (!success) { + g_task_return_error (task, err); + return; + } + + g_task_return_boolean (task, success); +} + + +void +phosh_bt_manager_connect_device_async (PhoshBtManager *self, + BluetoothDevice *device, + gboolean connect, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data) +{ + const char *object_path; + GTask *task = g_task_new (self, cancellable, callback, user_data); + + object_path = bluetooth_device_get_object_path (device); + + g_debug ("%s device %s", connect ? "Connecting" : "Disconnecting", object_path); + bluetooth_client_connect_service (self->bt_client, + object_path, + connect, + cancellable, + on_service_connected, + task); +} + + +gboolean +phosh_bt_manager_connect_device_finish (PhoshBtManager *self, + GAsyncResult *result, + GError **error) +{ + g_autoptr (GTask) task = G_TASK (result); + + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + + return g_task_propagate_boolean (task, error); +} diff --git a/src/bt-manager.h b/src/bt-manager.h index 325ce0bb5a58df4663dcbe0d606da0f770523dc6..b76f4744c3ed81c5f77ab5422ecf1da4ff1c91c8 100644 --- a/src/bt-manager.h +++ b/src/bt-manager.h @@ -8,7 +8,10 @@ #include +#include + #include +#include G_BEGIN_DECLS @@ -21,5 +24,17 @@ const char *phosh_bt_manager_get_icon_name (PhoshBtManager *self); gboolean phosh_bt_manager_get_enabled (PhoshBtManager *self); void phosh_bt_manager_set_enabled (PhoshBtManager *self, gboolean enabled); gboolean phosh_bt_manager_get_present (PhoshBtManager *self); +GListModel *phosh_bt_manager_get_connectable_devices (PhoshBtManager *self); +guint phosh_bt_manager_get_n_connected (PhoshBtManager *self); +const char *phosh_bt_manager_get_info (PhoshBtManager *self); +void phosh_bt_manager_connect_device_async (PhoshBtManager *self, + BluetoothDevice *device, + gboolean connect, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data); +gboolean phosh_bt_manager_connect_device_finish (PhoshBtManager *self, + GAsyncResult *result, + GError **error); G_END_DECLS diff --git a/src/bt-status-page.c b/src/bt-status-page.c new file mode 100644 index 0000000000000000000000000000000000000000..13f6b1762d6e6ac2a9c28cb2f7242484c613f527 --- /dev/null +++ b/src/bt-status-page.c @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-status-page" + +#include "bt-device-row.h" +#include "bt-status-page.h" +#include "status-page-placeholder.h" + +#include "bluetooth-device.h" + +#include + +/** + * PhoshBtStatusPage: + * + * A Quick setting status page widget to show Bluetooth devices + */ + +struct _PhoshBtStatusPage { + PhoshStatusPage parent_instance; + + GtkListBox *devices_list_box; + GtkStack *stack; + PhoshStatusPagePlaceholder *empty_state; + + PhoshBtManager *bt_manager; +}; + +G_DEFINE_TYPE (PhoshBtStatusPage, phosh_bt_status_page, PHOSH_TYPE_STATUS_PAGE); + + +static GtkWidget * +create_bt_device_row (BluetoothDevice *device) +{ + return phosh_bt_device_row_new (device); +} + + +static void +phosh_bt_status_page_dispose (GObject *object) +{ + PhoshBtStatusPage *self = PHOSH_BT_STATUS_PAGE (object); + + g_clear_object (&self->bt_manager); + + G_OBJECT_CLASS (phosh_bt_status_page_parent_class)->dispose (object); +} + + +static void +phosh_bt_status_page_class_init (PhoshBtStatusPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_bt_status_page_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/phosh/ui/bt-status-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, empty_state); + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, devices_list_box); + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, stack); +} + + +static void +update_stack_page_cb (PhoshBtStatusPage *self) +{ + const char *page_name, *title = NULL; + GListModel *devices = phosh_bt_manager_get_connectable_devices (self->bt_manager); + guint n_devices; + + if (!phosh_bt_manager_get_enabled (self->bt_manager)) { + title = _("Bluetooth disabled"); + page_name = "empty-state"; + } else { + n_devices = g_list_model_get_n_items (devices); + if (n_devices) { + page_name = "devices"; + } else { + page_name = "empty-state"; + title = _("No connectable Bluetooth Devices found"); + } + } + + phosh_status_page_placeholder_set_title (self->empty_state, title); + gtk_stack_set_visible_child_name (self->stack, page_name); +} + + +static void +phosh_bt_status_page_init (PhoshBtStatusPage *self) +{ + PhoshShell *shell; + + gtk_widget_init_template (GTK_WIDGET (self)); + + shell = phosh_shell_get_default (); + self->bt_manager = g_object_ref (phosh_shell_get_bt_manager (shell)); + g_return_if_fail (PHOSH_IS_BT_MANAGER (self->bt_manager)); + + gtk_list_box_bind_model (self->devices_list_box, + phosh_bt_manager_get_connectable_devices (self->bt_manager), + (GtkListBoxCreateWidgetFunc)create_bt_device_row, + NULL, + NULL); + + g_signal_connect_object (self->bt_manager, "notify::n-devices", + G_CALLBACK (update_stack_page_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->bt_manager, "notify::enabled", + G_CALLBACK (update_stack_page_cb), self, G_CONNECT_SWAPPED); + update_stack_page_cb (self); +} + + +GtkWidget * +phosh_bt_status_page_new (void) +{ + return g_object_new (PHOSH_TYPE_BT_STATUS_PAGE, NULL); +} diff --git a/src/bt-status-page.h b/src/bt-status-page.h new file mode 100644 index 0000000000000000000000000000000000000000..e33a1a6920cfb4501c8f0ee19123c7c347cc459d --- /dev/null +++ b/src/bt-status-page.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "quick-setting.h" +#include "shell.h" +#include "status-page.h" + +#include + + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_STATUS_PAGE phosh_bt_status_page_get_type () +G_DECLARE_FINAL_TYPE (PhoshBtStatusPage, phosh_bt_status_page, PHOSH, BT_STATUS_PAGE, + PhoshStatusPage) + +GtkWidget *phosh_bt_status_page_new (void); + +G_END_DECLS diff --git a/src/contrib/gnome-bluetooth/bluetooth-client.h b/src/contrib/gnome-bluetooth/bluetooth-client.h new file mode 100644 index 0000000000000000000000000000000000000000..c79c460dcdaa7f4b3a12cd0bac0bbecbb23de031 --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-client.h @@ -0,0 +1,48 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2005-2008 Marcel Holtmann + * Copyright (C) 2009-2021 Red Hat Inc. + * + * + * 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 + * + */ + +#pragma once + +#include +#include + +#define BLUETOOTH_TYPE_CLIENT (bluetooth_client_get_type()) +G_DECLARE_FINAL_TYPE (BluetoothClient, bluetooth_client, BLUETOOTH, CLIENT, GObject) + +BluetoothClient *bluetooth_client_new(void); + +GListStore *bluetooth_client_get_devices (BluetoothClient *client); + +void bluetooth_client_connect_service (BluetoothClient *client, + const char *path, + gboolean connect, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean bluetooth_client_connect_service_finish (BluetoothClient *client, + GAsyncResult *res, + GError **error); + +gboolean bluetooth_client_has_connected_input_devices (BluetoothClient *client); diff --git a/src/contrib/gnome-bluetooth/bluetooth-device.h b/src/contrib/gnome-bluetooth/bluetooth-device.h new file mode 100644 index 0000000000000000000000000000000000000000..82d8dc7f77384921e261458aac4df125a8784db0 --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-device.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2021 Bastien Nocera + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include +#include + +#define BLUETOOTH_TYPE_DEVICE (bluetooth_device_get_type()) +G_DECLARE_FINAL_TYPE (BluetoothDevice, bluetooth_device, BLUETOOTH, DEVICE, GObject) + +const char *bluetooth_device_get_object_path (BluetoothDevice *device); +void bluetooth_device_dump (BluetoothDevice *device); +char *bluetooth_device_to_string (BluetoothDevice *device); diff --git a/src/contrib/gnome-bluetooth/bluetooth-enums.h b/src/contrib/gnome-bluetooth/bluetooth-enums.h new file mode 100644 index 0000000000000000000000000000000000000000..3c972d62f15f1cdd3904f2bf66361662d80563c4 --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-enums.h @@ -0,0 +1,132 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2005-2008 Marcel Holtmann + * + * + * 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 + * + */ + +#pragma once + +#include + +/** + * SECTION:bluetooth-enums + * @short_description: Bluetooth related enumerations + * @stability: Stable + * @include: bluetooth-enums.h + * + * Enumerations related to Bluetooth. + **/ + +/** + * BluetoothType: + * @BLUETOOTH_TYPE_ANY: any device, or a device of an unknown type + * @BLUETOOTH_TYPE_PHONE: a telephone (usually a cell/mobile phone) + * @BLUETOOTH_TYPE_MODEM: a modem + * @BLUETOOTH_TYPE_COMPUTER: a computer, can be a laptop, a wearable computer, etc. + * @BLUETOOTH_TYPE_NETWORK: a network device, such as a router + * @BLUETOOTH_TYPE_HEADSET: a headset (usually a hands-free device) + * @BLUETOOTH_TYPE_HEADPHONES: headphones (covers two ears) + * @BLUETOOTH_TYPE_OTHER_AUDIO: another type of audio device + * @BLUETOOTH_TYPE_KEYBOARD: a keyboard + * @BLUETOOTH_TYPE_MOUSE: a mouse + * @BLUETOOTH_TYPE_CAMERA: a camera (still or moving) + * @BLUETOOTH_TYPE_PRINTER: a printer + * @BLUETOOTH_TYPE_JOYPAD: a joypad, joystick, or other game controller + * @BLUETOOTH_TYPE_TABLET: a drawing tablet + * @BLUETOOTH_TYPE_VIDEO: a video device, such as a webcam + * @BLUETOOTH_TYPE_REMOTE_CONTROL: a remote control + * @BLUETOOTH_TYPE_SCANNER: a scanner + * @BLUETOOTH_TYPE_DISPLAY: a display + * @BLUETOOTH_TYPE_WEARABLE: a wearable computer + * @BLUETOOTH_TYPE_TOY: a toy or game + * @BLUETOOTH_TYPE_SPEAKERS: audio speaker or speakers + * + * The type of a Bluetooth device. See also %BLUETOOTH_TYPE_INPUT and %BLUETOOTH_TYPE_AUDIO + **/ +typedef enum { + BLUETOOTH_TYPE_ANY = 1 << 0, + BLUETOOTH_TYPE_PHONE = 1 << 1, + BLUETOOTH_TYPE_MODEM = 1 << 2, + BLUETOOTH_TYPE_COMPUTER = 1 << 3, + BLUETOOTH_TYPE_NETWORK = 1 << 4, + BLUETOOTH_TYPE_HEADSET = 1 << 5, + BLUETOOTH_TYPE_HEADPHONES = 1 << 6, + BLUETOOTH_TYPE_OTHER_AUDIO = 1 << 7, + BLUETOOTH_TYPE_KEYBOARD = 1 << 8, + BLUETOOTH_TYPE_MOUSE = 1 << 9, + BLUETOOTH_TYPE_CAMERA = 1 << 10, + BLUETOOTH_TYPE_PRINTER = 1 << 11, + BLUETOOTH_TYPE_JOYPAD = 1 << 12, + BLUETOOTH_TYPE_TABLET = 1 << 13, + BLUETOOTH_TYPE_VIDEO = 1 << 14, + BLUETOOTH_TYPE_REMOTE_CONTROL = 1 << 15, + BLUETOOTH_TYPE_SCANNER = 1 << 16, + BLUETOOTH_TYPE_DISPLAY = 1 << 17, + BLUETOOTH_TYPE_WEARABLE = 1 << 18, + BLUETOOTH_TYPE_TOY = 1 << 19, + BLUETOOTH_TYPE_SPEAKERS = 1 << 20, +} BluetoothType; + +#define _BLUETOOTH_TYPE_NUM_TYPES 21 + +/** + * BLUETOOTH_TYPE_INPUT: + * + * Use this value to select any Bluetooth input device where a #BluetoothType enum is required. + */ +#define BLUETOOTH_TYPE_INPUT (BLUETOOTH_TYPE_KEYBOARD | BLUETOOTH_TYPE_MOUSE | BLUETOOTH_TYPE_TABLET | BLUETOOTH_TYPE_JOYPAD) +/** + * BLUETOOTH_TYPE_AUDIO: + * + * Use this value to select any Bluetooth audio device where a #BluetoothType enum is required. + */ +#define BLUETOOTH_TYPE_AUDIO (BLUETOOTH_TYPE_HEADSET | BLUETOOTH_TYPE_HEADPHONES | BLUETOOTH_TYPE_OTHER_AUDIO | BLUETOOTH_TYPE_SPEAKERS) + +/** + * BluetoothBatteryType: + * @BLUETOOTH_BATTERY_TYPE_NONE: no battery reporting + * @BLUETOOTH_BATTERY_TYPE_PERCENTAGE: battery reported in percentage + * @BLUETOOTH_BATTERY_TYPE_COARSE: battery reported coarsely + * + * The type of battery reporting supported by the device. + **/ +typedef enum { + BLUETOOTH_BATTERY_TYPE_NONE, + BLUETOOTH_BATTERY_TYPE_PERCENTAGE, + BLUETOOTH_BATTERY_TYPE_COARSE +} BluetoothBatteryType; + +/** + * BluetoothAdapterState: + * @BLUETOOTH_ADAPTER_STATE_ABSENT: Bluetooth adapter is missing. + * @BLUETOOTH_ADAPTER_STATE_ON: Bluetooth adapter is on. + * @BLUETOOTH_ADAPTER_STATE_TURNING_ON: Bluetooth adapter is being turned on. + * @BLUETOOTH_ADAPTER_STATE_TURNING_OFF: Bluetooth adapter is being turned off. + * @BLUETOOTH_ADAPTER_STATE_OFF: Bluetooth adapter is off. + * + * A more precise power state for a Bluetooth adapter. + **/ +typedef enum { + BLUETOOTH_ADAPTER_STATE_ABSENT = 0, + BLUETOOTH_ADAPTER_STATE_ON, + BLUETOOTH_ADAPTER_STATE_TURNING_ON, + BLUETOOTH_ADAPTER_STATE_TURNING_OFF, + BLUETOOTH_ADAPTER_STATE_OFF, +} BluetoothAdapterState; diff --git a/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h b/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h new file mode 100644 index 0000000000000000000000000000000000000000..c0616e07e2fcecb2febd79f30949fffbf9fcf9ff --- /dev/null +++ b/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h @@ -0,0 +1,28 @@ + +/* This file is generated by glib-mkenums, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ + +#pragma once + + #include + + + G_BEGIN_DECLS + +/* enumerations from "bluetooth-enums.h" */ + + +GType bluetooth_type_get_type (void); +#define BLUETOOTH_TYPE_TYPE (bluetooth_type_get_type()) + + +GType bluetooth_battery_type_get_type (void); +#define BLUETOOTH_TYPE_BATTERY_TYPE (bluetooth_battery_type_get_type()) + + +GType bluetooth_adapter_state_get_type (void); +#define BLUETOOTH_TYPE_ADAPTER_STATE (bluetooth_adapter_state_get_type()) + +G_END_DECLS + +/* Generated data ends here */ + diff --git a/src/meson.build b/src/meson.build index da7d46d831616d59ceab86d3a6fb2f014a6b3a61..5a772b897795004750d5c27761927fe5ca381741 100644 --- a/src/meson.build +++ b/src/meson.build @@ -239,8 +239,10 @@ libphosh_headers = files( 'auth.h', 'background-manager.h', 'batteryinfo.h', + 'bt-device-row.h', 'bt-info.h', 'bt-manager.h', + 'bt-status-page.h', 'emergency-calls-manager.h', 'fader.h', 'feedbackinfo.h', @@ -274,6 +276,7 @@ libphosh_headers = files( 'session-manager.h', 'shell.h', 'status-page.h', + 'status-page-placeholder.h', 'system-prompt.h', 'system-prompter.h', 'thumbnail.h', @@ -303,8 +306,10 @@ libphosh_sources = files( 'auth.c', 'background-manager.c', 'batteryinfo.c', + 'bt-device-row.c', 'bt-info.c', 'bt-manager.c', + 'bt-status-page.c', 'contrib/shell-network-agent.c', 'emergency-calls-manager.c', 'fader.c', @@ -339,6 +344,7 @@ libphosh_sources = files( 'session-manager.c', 'shell.c', 'status-page.c', + 'status-page-placeholder.c', 'system-prompt.c', 'system-prompter.c', 'thumbnail.c', @@ -367,6 +373,20 @@ if libsoup_dep.version().version_compare('< 3.5.1') ) endif + +# Headers are bundled as they're not shipped by gnome-bluetooth +# https://gitlab.gnome.org/GNOME/gnome-bluetooth/-/merge_requests/200 +gnome_bluetooth_headers_dep = declare_dependency( + include_directories: 'contrib/gnome-bluetooth', +) +# We build our own dep to avoid `-Wmissing-include-dirs` as the +# directory in the Cflags of gnome-bluetooth-3.0.pc do not +# necessarily exist +gnome_bluetooth_custom_dep = gnome_bluetooth_dep.partial_dependency( + link_args: true, + links: true, +) + phosh_deps = [ libsoup_dep, fribidi_dep, @@ -376,6 +396,8 @@ phosh_deps = [ glib_dep, gmodule_dep, gmobile_dep, + gnome_bluetooth_custom_dep, + gnome_bluetooth_headers_dep, gnome_desktop_dep, gobject_dep, gsettings_desktop_schemas_dep, @@ -471,7 +493,8 @@ if enable_introspection symbol_prefix : 'phosh', identifier_prefix : 'Phosh', link_with : phosh_lib, - includes : ['Gcr-3', 'Gio-2.0', 'Gtk-3.0', 'GnomeDesktop-3.0', 'Handy-1', 'NM-1.0'], + includes : ['Gcr-3', 'Gio-2.0', 'Gtk-3.0', 'GnomeDesktop-3.0', + 'Handy-1', 'NM-1.0', 'GnomeBluetooth-3.0'], extra_args : phosh_gir_extra_args, dependencies : phosh_static_lib_dep, fatal_warnings : true, diff --git a/src/phosh.gresources.xml b/src/phosh.gresources.xml index 718e91c433a2d1d07d63025dc48c781397ceba47..f83cfa780689d52058f2a844c571b1ef6cc31377 100644 --- a/src/phosh.gresources.xml +++ b/src/phosh.gresources.xml @@ -9,6 +9,8 @@ ui/app-grid.ui ui/audio-device-row.ui ui/audio-settings.ui + ui/bt-device-row.ui + ui/bt-status-page.ui ui/call-notification.ui ui/emergency-contact-row.ui ui/emergency-menu.ui @@ -32,6 +34,7 @@ ui/settings.ui ui/splash.ui ui/status-page.ui + ui/status-page-placeholder.ui ui/system-modal-dialog.ui ui/system-prompt.ui ui/top-panel.ui diff --git a/src/settings.c b/src/settings.c index dc978500dc269237d0811ed3168a6b5ce027f8af..715b72ea973565cc61e428e5cb5cf45319c00ec6 100644 --- a/src/settings.c +++ b/src/settings.c @@ -23,6 +23,7 @@ #include "torch-info.h" #include "torch-manager.h" #include "vpn-manager.h" +#include "bt-status-page.h" #include "wwan/wwan-manager.h" #include "notifications/notify-manager.h" #include "notifications/notification-frame.h" @@ -409,9 +410,11 @@ wwan_setting_long_pressed_cb (PhoshSettings *self) open_settings_panel (self, "wwan"); } + static void -bt_setting_clicked_cb (PhoshSettings *self) +on_toggle_bt_activated (GSimpleAction *action, GVariant *param, gpointer data) { + PhoshSettings *self = PHOSH_SETTINGS (data); PhoshShell *shell = phosh_shell_get_default (); PhoshBtManager *manager; gboolean enabled; @@ -429,7 +432,14 @@ bt_setting_clicked_cb (PhoshSettings *self) static void bt_setting_long_pressed_cb (PhoshSettings *self) { - open_settings_panel (self, "bluetooth"); + GtkStack *stack = GTK_STACK (self->stack); + GtkStack *status_page_stack = GTK_STACK (self->status_page_stack); + + if (self->on_lockscreen) + return; + + gtk_stack_set_visible_child_name (stack, "status_page"); + gtk_stack_set_visible_child_name (status_page_stack, "bt_status_page"); } @@ -797,6 +807,7 @@ phosh_settings_class_init (PhoshSettingsClass *klass) object_class->set_property = phosh_settings_set_property; object_class->get_property = phosh_settings_get_property; + g_type_ensure (PHOSH_TYPE_BT_STATUS_PAGE); g_type_ensure (PHOSH_TYPE_WIFI_STATUS_PAGE); gtk_widget_class_set_template_from_resource (widget_class, @@ -851,7 +862,6 @@ phosh_settings_class_init (PhoshSettingsClass *klass) gtk_widget_class_bind_template_child (widget_class, PhoshSettings, scrolled_window); gtk_widget_class_bind_template_callback (widget_class, battery_setting_clicked_cb); - gtk_widget_class_bind_template_callback (widget_class, bt_setting_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, bt_setting_long_pressed_cb); gtk_widget_class_bind_template_callback (widget_class, docked_setting_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, docked_setting_long_pressed_cb); @@ -877,6 +887,7 @@ phosh_settings_class_init (PhoshSettingsClass *klass) static const GActionEntry entries[] = { { .name = "launch-panel", .activate = on_launch_panel_activated, .parameter_type = "s" }, { .name = "close-status-page", .activate = on_close_status_page_activated }, + { .name = "toggle-bt", .activate = on_toggle_bt_activated }, }; diff --git a/src/status-page-placeholder.c b/src/status-page-placeholder.c new file mode 100644 index 0000000000000000000000000000000000000000..c876cbda7ae9c27161643115c8031f6d04365480 --- /dev/null +++ b/src/status-page-placeholder.c @@ -0,0 +1,304 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-status-page-placeholder" + +#include "phosh-config.h" + +#include "status-page-placeholder.h" + +#include + +/** + * PhoshStatusPagePlaceholder: + * + * A placeholder in a [class@StatusPage]. + * + * The placeholder page has a title and an icon and can have a single + * child which is put below the title. + * + * This widget can be replaced with `AdwStatusPage` and a bit of styling + * once we switch to GTK4. + */ + +enum { + PROP_0, + PROP_TITLE, + PROP_ICON_NAME, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshStatusPagePlaceholder { + GtkBin parent; + + GtkBox *toplevel_box; + + GtkImage *icon; + char *icon_name; + GtkLabel *title_label; + + GtkWidget *extra_widget; +}; +G_DEFINE_TYPE (PhoshStatusPagePlaceholder, phosh_status_page_placeholder, GTK_TYPE_BIN) + + +static void +update_title_visibility (PhoshStatusPagePlaceholder *self) +{ + const char *text = gtk_label_get_text (self->title_label); + + gtk_widget_set_visible (GTK_WIDGET (self->title_label), !gm_str_is_null_or_empty (text)); +} + + +static void +phosh_status_page_placeholder_destroy (GtkWidget *widget) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (widget); + + if (self->toplevel_box) { + /* Trigger destruction of all contained widgets */ + gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (self->toplevel_box)); + self->toplevel_box = NULL; + self->icon = NULL; + self->title_label = NULL; + self->extra_widget = NULL; + } + + GTK_WIDGET_CLASS (phosh_status_page_placeholder_parent_class)->destroy (widget); +} + + +static void +phosh_status_page_placeholder_add (GtkContainer *container, + GtkWidget *child) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (container); + + if (!self->toplevel_box) { + GTK_CONTAINER_CLASS (phosh_status_page_placeholder_parent_class)->add (container, child); + } else if (!self->extra_widget) { + gtk_container_add (GTK_CONTAINER (self->toplevel_box), child); + self->extra_widget = child; + } else { + g_warning ("Attempting to add a second child to a PhoshStatusPagePlaceholder," + "but a PhoshStatusPagePlaceholder can only have one child"); + } +} + + +static void +phosh_status_page_placeholder_remove (GtkContainer *container, + GtkWidget *child) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (container); + + if (child == GTK_WIDGET (self->toplevel_box)) { + GTK_CONTAINER_CLASS (phosh_status_page_placeholder_parent_class)->remove (container, child); + } else if (child == self->extra_widget) { + gtk_container_remove (GTK_CONTAINER (self->toplevel_box), child); + self->extra_widget = NULL; + } else { + g_return_if_reached (); + } +} + + +static void +phosh_status_page_placeholder_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (container); + + if (include_internals) + GTK_CONTAINER_CLASS (phosh_status_page_placeholder_parent_class)->forall (container, + include_internals, + callback, + callback_data); + else if (self->extra_widget) + callback (self->extra_widget, callback_data); +} + + +static void +phosh_status_page_placeholder_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + switch (property_id) { + case PROP_TITLE: + phosh_status_page_placeholder_set_title (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + phosh_status_page_placeholder_set_icon_name (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_status_page_placeholder_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + switch (property_id) { + case PROP_TITLE: + g_value_set_string (value, phosh_status_page_placeholder_get_title (self)); + break; + case PROP_ICON_NAME: + g_value_set_string (value, phosh_status_page_placeholder_get_icon_name (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_status_page_placeholder_dispose (GObject *object) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + g_clear_pointer (&self->icon_name, g_free); + + G_OBJECT_CLASS (phosh_status_page_placeholder_parent_class)->dispose (object); +} + + +static void +phosh_status_page_placeholder_class_init (PhoshStatusPagePlaceholderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = phosh_status_page_placeholder_get_property; + object_class->set_property = phosh_status_page_placeholder_set_property; + object_class->dispose = phosh_status_page_placeholder_dispose; + + widget_class->destroy = phosh_status_page_placeholder_destroy; + + container_class->add = phosh_status_page_placeholder_add; + container_class->remove = phosh_status_page_placeholder_remove; + container_class->forall = phosh_status_page_placeholder_forall; + + /** + * PhoshStatusPagePlaceholder:title: + * + * The title of the placeholder page, displayed below the icon. + */ + props[PROP_TITLE] = + g_param_spec_string ("title", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPagePlaceholder:icon-name: + * + * The name of the icon on the placeholder page + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/sm/puri/phosh/ui/status-page-placeholder.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, icon); + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, title_label); + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, toplevel_box); + + gtk_widget_class_set_css_name (widget_class, "phosh-status-page-placeholder"); +} + + +static void +phosh_status_page_placeholder_init (PhoshStatusPagePlaceholder *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + update_title_visibility (self); +} + + +PhoshStatusPagePlaceholder * +phosh_status_page_placeholder_new (void) +{ + return g_object_new (PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER, NULL); +} + + +void +phosh_status_page_placeholder_set_title (PhoshStatusPagePlaceholder *self, const char *title) +{ + const char *current; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self)); + + current = gtk_label_get_label (self->title_label); + if (g_strcmp0 (current, title) == 0) + return; + + gtk_label_set_label (self->title_label, title); + update_title_visibility (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + + +const char * +phosh_status_page_placeholder_get_title (PhoshStatusPagePlaceholder *self) +{ + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self), NULL); + + return gtk_label_get_label (self->title_label); +} + + +void +phosh_status_page_placeholder_set_icon_name (PhoshStatusPagePlaceholder *self, const char *icon_name) +{ + g_return_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self)); + + if (g_strcmp0 (self->icon_name, icon_name) == 0) + return; + + g_free (self->icon_name); + self->icon_name = g_strdup (icon_name); + + if (!icon_name) + g_object_set (G_OBJECT (self->icon), "icon-name", "image-missing", NULL); + else + g_object_set (G_OBJECT (self->icon), "icon-name", icon_name, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +const char * +phosh_status_page_placeholder_get_icon_name (PhoshStatusPagePlaceholder *self) +{ + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self), NULL); + + return self->icon_name; +} diff --git a/src/status-page-placeholder.h b/src/status-page-placeholder.h new file mode 100644 index 0000000000000000000000000000000000000000..8bab316b35a7e4873be3325c3d96e2680b70f5cd --- /dev/null +++ b/src/status-page-placeholder.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER (phosh_status_page_placeholder_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshStatusPagePlaceholder, phosh_status_page_placeholder, PHOSH, STATUS_PAGE_PLACEHOLDER, GtkBin) + +PhoshStatusPagePlaceholder *phosh_status_page_placeholder_new (void); +void phosh_status_page_placeholder_set_title (PhoshStatusPagePlaceholder *self, + const char *title); +const char *phosh_status_page_placeholder_get_title (PhoshStatusPagePlaceholder *self); +void phosh_status_page_placeholder_set_icon_name (PhoshStatusPagePlaceholder *self, + const char *icon_name); +const char *phosh_status_page_placeholder_get_icon_name (PhoshStatusPagePlaceholder *self); + +G_END_DECLS diff --git a/src/status-page.c b/src/status-page.c index 78ac9d7b9ae826aacaae22d3c7215aac8704e4c4..72ec668b427d03049e46c903e5d5eea649d357cd 100644 --- a/src/status-page.c +++ b/src/status-page.c @@ -1,37 +1,55 @@ /* * Copyright (C) 2023 Tether Operations Limited + * 2024 The Phosh Developers * * SPDX-License-Identifier: GPL-3.0-or-later * - * Author: Arun Mani J + * Authors: Arun Mani J + * Guido Günther */ #define G_LOG_DOMAIN "phosh-status-page" #include "status-page.h" +#include "status-page-placeholder.h" /** * PhoshStatusPage: * - * A widget to show more details about a status indicator like WiFi, Bluetooth etc. + * Additional status information associated with a [class@QuickSetting]. * - * PhoshStatusPage is used to show more details about a status indicator. It must be subclassed to - * display the required information. PhoshSettings will show this information when the respective - * PhoshQuickSetting is activated. + * This is displayed when the quick setting is long pressed. */ enum { PROP_0, + PROP_TITLE, PROP_HEADER, + PROP_FOOTER, PROP_LAST_PROP, }; static GParamSpec *props[PROP_LAST_PROP]; typedef struct { - GtkWidget *header; + GtkBox *toplevel_box; + + /* Header */ + GtkLabel *title_label; + GtkBox *header_bin; + GtkWidget *header_widget; + + /* Content */ + GtkBox *content_bin; + GtkWidget *content_widget; + + /* Footer */ + GtkSeparator *footer_separator; + GtkBox *footer_bin; + GtkWidget *footer_widget; } PhoshStatusPagePrivate; -G_DEFINE_TYPE_WITH_PRIVATE (PhoshStatusPage, phosh_status_page, GTK_TYPE_BOX); +G_DEFINE_TYPE_WITH_PRIVATE (PhoshStatusPage, phosh_status_page, GTK_TYPE_BIN); + static void phosh_status_page_set_property (GObject *object, @@ -42,9 +60,15 @@ phosh_status_page_set_property (GObject *object, PhoshStatusPage *self = PHOSH_STATUS_PAGE (object); switch (property_id) { + case PROP_TITLE: + phosh_status_page_set_title (self, g_value_get_string (value)); + break; case PROP_HEADER: phosh_status_page_set_header (self, g_value_get_object (value)); break; + case PROP_FOOTER: + phosh_status_page_set_footer (self, g_value_get_object (value)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } @@ -60,73 +84,233 @@ phosh_status_page_get_property (GObject *object, PhoshStatusPage *self = PHOSH_STATUS_PAGE (object); switch (property_id) { + case PROP_TITLE: + g_value_set_string (value, phosh_status_page_get_title (self)); + break; case PROP_HEADER: g_value_set_object (value, phosh_status_page_get_header (self)); break; + case PROP_FOOTER: + g_value_set_object (value, phosh_status_page_get_footer (self)); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } } + +static void +phosh_status_page_destroy (GtkWidget *widget) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (widget); + PhoshStatusPagePrivate *priv = phosh_status_page_get_instance_private (self); + + if (priv->toplevel_box) { + /* Trigger destruction of all contained widgets */ + gtk_container_remove (GTK_CONTAINER (self), GTK_WIDGET (priv->toplevel_box)); + priv->toplevel_box = NULL; + priv->footer_widget = NULL; + priv->header_widget = NULL; + priv->content_widget = NULL; + priv->footer_bin = NULL; + priv->header_bin = NULL; + priv->content_bin = NULL; + } + + GTK_WIDGET_CLASS (phosh_status_page_parent_class)->destroy (widget); +} + + +static void +phosh_status_page_add (GtkContainer *container, + GtkWidget *child) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (container); + PhoshStatusPagePrivate *priv = phosh_status_page_get_instance_private (self); + + if (!priv->toplevel_box) { + GTK_CONTAINER_CLASS (phosh_status_page_parent_class)->add (container, child); + } else if (!priv->content_widget) { + gtk_container_add (GTK_CONTAINER (priv->content_bin), child); + priv->content_widget = child; + } else { + g_warning ("Attempting to add a second child to a PhoshStatusPage," + "but a PhoshStatusPage can only have one child"); + } +} + + +static void +phosh_status_page_remove (GtkContainer *container, + GtkWidget *child) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (container); + PhoshStatusPagePrivate *priv = phosh_status_page_get_instance_private (self); + + if (child == GTK_WIDGET (priv->toplevel_box)) { + GTK_CONTAINER_CLASS (phosh_status_page_parent_class)->remove (container, child); + } else if (child == priv->content_widget) { + gtk_container_remove (GTK_CONTAINER (priv->content_bin), child); + priv->content_widget = NULL; + } else { + g_return_if_reached (); + } +} + + +static void +phosh_status_page_forall (GtkContainer *container, + gboolean include_internals, + GtkCallback callback, + gpointer callback_data) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (container); + PhoshStatusPagePrivate *priv = phosh_status_page_get_instance_private (self); + + if (include_internals) { + GTK_CONTAINER_CLASS (phosh_status_page_parent_class)->forall (container, + include_internals, + callback, + callback_data); + } else if (priv->content_widget) { + callback (priv->content_widget, callback_data); + } +} + + static void phosh_status_page_class_init (PhoshStatusPageClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); object_class->set_property = phosh_status_page_set_property; object_class->get_property = phosh_status_page_get_property; + widget_class->destroy = phosh_status_page_destroy; + + container_class->add = phosh_status_page_add; + container_class->remove = phosh_status_page_remove; + container_class->forall = phosh_status_page_forall; + + /** + * PhoshStatusPage:title: + * + * The status page title + */ + props[PROP_TITLE] = + g_param_spec_string ("title", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); /** * PhoshStatusPage:header: * - * The header widget + * An extra widget to add to end of the status page's header */ props[PROP_HEADER] = g_param_spec_object ("header", "", "", GTK_TYPE_WIDGET, - G_PARAM_READWRITE | - G_PARAM_STATIC_STRINGS); + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPage:footer: + * + * Widget displayed at the very bottom - usually a button. + */ + props[PROP_FOOTER] = + g_param_spec_object ("footer", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + g_type_ensure (PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER); gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/phosh/ui/status-page.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, content_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, footer_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, footer_separator); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, header_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, title_label); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, toplevel_box); + gtk_widget_class_set_css_name (widget_class, "phosh-status-page"); } + static void phosh_status_page_init (PhoshStatusPage *self) { gtk_widget_init_template (GTK_WIDGET (self)); } + PhoshStatusPage * phosh_status_page_new (void) { return g_object_new (PHOSH_TYPE_STATUS_PAGE, NULL); } + void -phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header) +phosh_status_page_set_title (PhoshStatusPage *self, const char *title) { PhoshStatusPagePrivate *priv; + const char *current; g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); - g_return_if_fail (GTK_IS_WIDGET (header)); + priv = phosh_status_page_get_instance_private (self); + + current = gtk_label_get_label (priv->title_label); + if (g_strcmp0 (current, title) == 0) + return; + + gtk_label_set_label (priv->title_label, title); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + + +const char * +phosh_status_page_get_title (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), NULL); + priv = phosh_status_page_get_instance_private (self); + + return gtk_label_get_label (priv->title_label); +} + +/** + * phosh_status_page_set_header: + * @self: A quick setting status page + * + * Set the header widget of the status page. See + * [property@StatusPage:header]. + */ +void +phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header_widget) +{ + PhoshStatusPagePrivate *priv; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + g_return_if_fail (GTK_IS_WIDGET (header_widget)); priv = phosh_status_page_get_instance_private (self); - if (priv->header == header) + if (priv->header_widget == header_widget) return; - if (priv->header) - gtk_container_remove (GTK_CONTAINER (self), priv->header); + if (priv->header_widget) + gtk_container_remove (GTK_CONTAINER (priv->header_bin), priv->header_widget); + + priv->header_widget = header_widget; - priv->header = header; + if (priv->header_widget) + gtk_container_add (GTK_CONTAINER (priv->header_bin), priv->header_widget); - if (priv->header) - gtk_box_pack_start (GTK_BOX (self), priv->header, FALSE, FALSE, 0); + gtk_widget_set_visible (GTK_WIDGET (priv->header_bin), !!header_widget); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HEADER]); } @@ -135,7 +319,7 @@ phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header) * phosh_status_page_get_header: * @self: A quick setting status page * - * Get the header of the status page + * Get the header widget of the status page * * Returns:(transfer none): The status page header */ @@ -148,5 +332,58 @@ phosh_status_page_get_header (PhoshStatusPage *self) priv = phosh_status_page_get_instance_private (self); - return priv->header; + return priv->header_widget; +} + +/** + * phosh_status_page_set_footer: + * @self: A quick setting status page + * + * Set the footer widget shown at the bottom of a status page + */ +void +phosh_status_page_set_footer (PhoshStatusPage *self, GtkWidget *footer_widget) +{ + PhoshStatusPagePrivate *priv; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + g_return_if_fail (GTK_IS_WIDGET (footer_widget)); + + priv = phosh_status_page_get_instance_private (self); + + if (priv->footer_widget == footer_widget) + return; + + if (priv->footer_widget) + gtk_container_remove (GTK_CONTAINER (priv->footer_bin), priv->footer_widget); + + priv->footer_widget = footer_widget; + + if (priv->footer_widget) + gtk_container_add (GTK_CONTAINER (priv->footer_bin), priv->footer_widget); + + gtk_widget_set_visible (GTK_WIDGET (priv->footer_separator), !!footer_widget); + gtk_widget_set_visible (GTK_WIDGET (priv->footer_bin), !!footer_widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FOOTER]); +} + +/** + * phosh_status_page_get_footer: + * @self: A quick setting status page + * + * Get the footer of the status page + * + * Returns:(transfer none): The status page footer + */ +GtkWidget * +phosh_status_page_get_footer (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), 0); + + priv = phosh_status_page_get_instance_private (self); + + return priv->footer_widget; } diff --git a/src/status-page.h b/src/status-page.h index 35856a2bf70109253234cb64a2ce0c0359961ac8..8d053a93c8192672747cee4d1ff67b9a55029c98 100644 --- a/src/status-page.h +++ b/src/status-page.h @@ -11,15 +11,19 @@ G_BEGIN_DECLS #define PHOSH_TYPE_STATUS_PAGE phosh_status_page_get_type () -G_DECLARE_DERIVABLE_TYPE (PhoshStatusPage, phosh_status_page, PHOSH, STATUS_PAGE, GtkBox) +G_DECLARE_DERIVABLE_TYPE (PhoshStatusPage, phosh_status_page, PHOSH, STATUS_PAGE, GtkBin) struct _PhoshStatusPageClass { - GtkBoxClass parent_class; + GtkBinClass parent_class; }; PhoshStatusPage *phosh_status_page_new (void); +void phosh_status_page_set_title (PhoshStatusPage *self, const char *title); +const char *phosh_status_page_get_title (PhoshStatusPage *self); void phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header); GtkWidget *phosh_status_page_get_header (PhoshStatusPage *self); +void phosh_status_page_set_footer (PhoshStatusPage *self, GtkWidget *footer); +GtkWidget *phosh_status_page_get_footer (PhoshStatusPage *self); G_END_DECLS diff --git a/src/stylesheet/common.css b/src/stylesheet/common.css index fae26dd17484fee8bbbf9b62e0d78190fa57816f..68d2e59f168338258763f452d68d21b67528b77f 100644 --- a/src/stylesheet/common.css +++ b/src/stylesheet/common.css @@ -51,13 +51,6 @@ phosh-top-panel .phosh-topbar-date { border-radius: 99px; } -.phosh-osk-button { - border-radius: 50%; - min-width: 0; - min-height: 0; - padding: 6px; -} - /* * Settings menu */ @@ -72,10 +65,14 @@ phosh-top-panel .phosh-topbar-date { border: 2px solid @theme_bg_color; } -.phosh-settings-menu scrolledwindow > viewport { +.phosh-settings-menu > scrolledwindow > viewport { padding: 0 16px; } +.phosh-settings-menu separator { + background: @phosh_borders_color; +} + #phosh_quick_settings flowboxchild > button { border-radius: 99px; border: 0; @@ -141,7 +138,7 @@ button.phosh-settings-details:focus { background: none; } -/* Listboxes in settings (e.g. audio details, quick setting status pages) */ +/* Listboxes in settings (e.g. audio details) */ .phosh-settings-list-box { border-radius: 18px; padding: 12px; @@ -153,10 +150,6 @@ button.phosh-settings-details:focus { background-color: transparent; } -.phosh-settings-list-box separator { - background: @phosh_borders_color; -} - .phosh-settings-list-box list row { border-radius: 8px; } @@ -172,6 +165,30 @@ button.phosh-settings-details:focus { -gtk-icon-style: symbolic; } +/* Status pages of quick settings (e.g. audio wifi or bt status pages) */ +.phosh-status-page { + border-radius: 18px; + padding: 12px 12px 6px; + border: none; + background-color: @phosh_notification_bg_color; +} + +.phosh-status-page list row:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background: mix(@phosh_button_bg_color, @theme_selected_bg_color, 0.1); + outline-style: none; + transition: none; +} + +.phosh-status-page list row image { + -gtk-icon-style: symbolic; +} + +phosh-status-page-placeholder > box > label.title { + font-weight: 400; + font-size: 16px; +} + /* * Audio devices listbox */ diff --git a/src/ui/bt-device-row.ui b/src/ui/bt-device-row.ui new file mode 100644 index 0000000000000000000000000000000000000000..dff9c18f900ebb37273739420016ddae63faa58d --- /dev/null +++ b/src/ui/bt-device-row.ui @@ -0,0 +1,35 @@ + + + + + diff --git a/src/ui/bt-status-page.ui b/src/ui/bt-status-page.ui new file mode 100644 index 0000000000000000000000000000000000000000..e0ca6730a7cece596b10ef09e748c5811e5df226 --- /dev/null +++ b/src/ui/bt-status-page.ui @@ -0,0 +1,64 @@ + + + + + + 1 + 1 + settings.launch-panel + "bluetooth" + + + 1 + end + Bluetooth Settings + + + + diff --git a/src/ui/settings.ui b/src/ui/settings.ui index 47ead5a8668dd6ab3999d70be849a4849c0e2adc..448e8ffbf56cb9c4f1db0937c0f6265cb3fbf95e 100644 --- a/src/ui/settings.ui +++ b/src/ui/settings.ui @@ -115,8 +115,9 @@ True + - + settings.toggle-bt @@ -245,6 +246,14 @@ 1 + + + True + + + bt_status_page + + status_page diff --git a/src/ui/status-page-placeholder.ui b/src/ui/status-page-placeholder.ui new file mode 100644 index 0000000000000000000000000000000000000000..9778ec87acda2f3d3c9c547be15ae5fd99d49405 --- /dev/null +++ b/src/ui/status-page-placeholder.ui @@ -0,0 +1,37 @@ + + + + + diff --git a/src/ui/status-page.ui b/src/ui/status-page.ui index d3957d9dd28731390b6120d3143b304bcbbea07a..60d0677737844d93805b3de8c926e34a7f55a772 100644 --- a/src/ui/status-page.ui +++ b/src/ui/status-page.ui @@ -1,15 +1,76 @@ -