From 7931fa48e8ec31d5ce4595aa985e24c0880c9295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Wed, 26 Feb 2025 13:11:30 -0800 Subject: [PATCH 01/11] Update deps --- .github/workflows/main.yml | 2 +- README.md | 4 ++-- meson.build | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ec4d5d..fec90d0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y libgranite-dev meson valac + apt install -y libgranite-7-dev meson valac - name: Build env: DESTDIR: out diff --git a/README.md b/README.md index 969f993..0080f2f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ Send and receive files via Bluetooth You'll need the following dependencies: - libgranite-dev >= 6.0.0 - libgtk3-dev + libgranite-7-dev + libgtk4-dev meson valac diff --git a/meson.build b/meson.build index 5142ead..67d540f 100644 --- a/meson.build +++ b/meson.build @@ -10,8 +10,8 @@ add_global_arguments('-DGETTEXT_PACKAGE="@0@"'.format(meson.project_name()), lan gnome = import('gnome') i18n = import('i18n') -granite_dep = dependency ('granite') -gtk_dep = dependency ('gtk+-3.0') +granite_dep = dependency ('granite-7') +gtk_dep = dependency ('gtk4') subdir('data') subdir('po') From 67bc34ad457b1e54995a22c5bc048a560fad1818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Fri, 17 Apr 2026 13:35:53 -0700 Subject: [PATCH 02/11] More minor syntax --- src/Dialog/ReceiverDialog.vala | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index 6d05164..31a4cbb 100644 --- a/src/Dialog/ReceiverDialog.vala +++ b/src/Dialog/ReceiverDialog.vala @@ -32,7 +32,8 @@ public class ReceiverDialog : Granite.Dialog { notification = new GLib.Notification ("bluetooth"); notification.set_priority (NotificationPriority.NORMAL); - var icon_image = new Gtk.Image.from_icon_name ("io.elementary.bluetooth", Gtk.IconSize.DIALOG) { + var icon_image = new Gtk.Image.from_icon_name ("io.elementary.bluetooth") { + pixel_size = 48, valign = END, halign = END }; @@ -54,7 +55,7 @@ public class ReceiverDialog : Granite.Dialog { wrap = true, xalign = 0 }; - device_label.get_style_context ().add_class ("primary"); + device_label.add_css_class ("primary"); directory_label = new Gtk.Label (null) { max_width_chars = 45, @@ -102,12 +103,12 @@ public class ReceiverDialog : Granite.Dialog { message_grid.attach (progressbar, 1, 4); message_grid.attach (progress_label, 1, 5); - get_content_area ().add (message_grid); + get_content_area ().append (message_grid); add_button (_("Close"), Gtk.ResponseType.CLOSE); var suggested_button = add_button (_("Reject"), Gtk.ResponseType.ACCEPT); - suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + suggested_button.add_css_class (Granite.CssClass.DESTRUCTIVE); response.connect ((response_id) => { if (response_id == Gtk.ResponseType.ACCEPT) { From 211fbfbc58239012ba5510465ee70e760f1df6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Fri, 17 Apr 2026 15:16:44 -0700 Subject: [PATCH 03/11] No more show_all --- src/Dialog/ReceiverDialog.vala | 1 - src/Dialog/SenderDialog.vala | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index bd6ca1d..31a4cbb 100644 --- a/src/Dialog/ReceiverDialog.vala +++ b/src/Dialog/ReceiverDialog.vala @@ -102,7 +102,6 @@ public class ReceiverDialog : Granite.Dialog { message_grid.attach (rate_label, 1, 3); message_grid.attach (progressbar, 1, 4); message_grid.attach (progress_label, 1, 5); - message_grid.show_all (); get_content_area ().append (message_grid); diff --git a/src/Dialog/SenderDialog.vala b/src/Dialog/SenderDialog.vala index 153468d..73bcb2d 100644 --- a/src/Dialog/SenderDialog.vala +++ b/src/Dialog/SenderDialog.vala @@ -107,7 +107,6 @@ public class SenderDialog : Granite.Dialog { message_grid.attach (rate_label, 1, 3); message_grid.attach (progressbar, 1, 4); message_grid.attach (progress_label, 1, 5); - message_grid.show_all (); get_content_area ().append (message_grid); From 2dd506449876fbca54198c371edfad74891166bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Fri, 17 Apr 2026 15:25:18 -0700 Subject: [PATCH 04/11] destroy connect --- src/Application.vala | 6 +++--- src/Dialog/DeviceRow.vala | 2 +- src/Dialog/ReceiverDialog.vala | 4 ++-- src/Dialog/ScanDialog.vala | 2 +- src/Dialog/SenderDialog.vala | 12 ++++++------ 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Application.vala b/src/Application.vala index 577984d..977751e 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -48,7 +48,7 @@ public class BluetoothApp : Gtk.Application { if (bt_scan == null) { bt_scan = new ScanDialog (this, object_manager); - bt_scan.destroy.connect (() => { + ((Gtk.Widget) bt_scan).destroy.connect (() => { bt_scan = null; }); @@ -58,7 +58,7 @@ public class BluetoothApp : Gtk.Application { bt_sender.add_files (files, device); bt_senders.append (bt_sender); bt_sender.present (); - bt_sender.destroy.connect (() => { + ((Gtk.Widget) bt_sender).destroy.connect (() => { bt_senders.foreach ((sender) => { if (sender.device == bt_sender.device) { bt_senders.remove_link (bt_senders.find (sender)); @@ -234,7 +234,7 @@ public class BluetoothApp : Gtk.Application { bt_receiver = new ReceiverDialog (this); bt_receivers.append (bt_receiver); - bt_receiver.destroy.connect (() => { + ((Gtk.Widget) bt_receiver).destroy.connect (() => { bt_receivers.foreach ((receiver) => { if (receiver.transfer.session == bt_receiver.session) { bt_receivers.remove_link (bt_receivers.find (receiver)); diff --git a/src/Dialog/DeviceRow.vala b/src/Dialog/DeviceRow.vala index a02a351..792b088 100644 --- a/src/Dialog/DeviceRow.vala +++ b/src/Dialog/DeviceRow.vala @@ -40,7 +40,7 @@ public class DeviceRow : Gtk.ListBoxRow { var state_label = new Gtk.Label (null) { xalign = 0 }; - state_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + state_label.add_css_class (Granite.CssClass.SMALL); var overlay = new Gtk.Overlay () { child = image, diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index 31a4cbb..f4ecd68 100644 --- a/src/Dialog/ReceiverDialog.vala +++ b/src/Dialog/ReceiverDialog.vala @@ -119,11 +119,11 @@ public class ReceiverDialog : Granite.Dialog { } destroy (); } else { - hide_on_delete (); + hide (); } }); - delete_event.connect (() => { + close_request.connect (() => { if (transfer.status == "active") { return hide_on_delete (); } else { diff --git a/src/Dialog/ScanDialog.vala b/src/Dialog/ScanDialog.vala index a56e66d..2c3182d 100644 --- a/src/Dialog/ScanDialog.vala +++ b/src/Dialog/ScanDialog.vala @@ -45,7 +45,7 @@ public class ScanDialog : Granite.Dialog { }; var empty_alert = new Granite.Placeholder (_("No Devices Found")) { - secondary_text = _("Please ensure that your devices are visible and ready for pairing.") + description = _("Please ensure that your devices are visible and ready for pairing.") }; list_box = new Gtk.ListBox () { diff --git a/src/Dialog/SenderDialog.vala b/src/Dialog/SenderDialog.vala index 73bcb2d..8c80b8c 100644 --- a/src/Dialog/SenderDialog.vala +++ b/src/Dialog/SenderDialog.vala @@ -113,7 +113,7 @@ public class SenderDialog : Granite.Dialog { add_button (_("Close"), Gtk.ResponseType.CLOSE); var reject_transfer = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - reject_transfer.add_css_class (Gtk.STYLE_CLASS_DESTRUCTIVE_ACTION); + reject_transfer.add_css_class (Granite.CssClass.DESTRUCTIVE); response.connect ((response_id) => { if (response_id == Gtk.ResponseType.CANCEL) { @@ -127,17 +127,17 @@ public class SenderDialog : Granite.Dialog { remove_session.begin (); } } - destroy (); + } else { if (transfer.status == "active") { - hide_on_delete (); + hide (); } else { destroy (); } } }); - delete_event.connect (() => { + close_request.connect (() => { if (transfer.status == "active") { return hide_on_delete (); } else { @@ -241,7 +241,7 @@ public class SenderDialog : Granite.Dialog { ); send_file.begin (); } catch (Error e) { - hide_on_delete (); + hide (); var bt_retry = new Granite.MessageDialog ( _("Connecting to '%s' failed.").printf (device.alias), "%s\n%s".printf ( @@ -308,7 +308,7 @@ public class SenderDialog : Granite.Dialog { private void tranfer_progress () { switch (transfer.status) { case "error": - hide_on_delete (); + hide (); var bt_retry = new Granite.MessageDialog ( _("The transfer of '%s' failed.").printf (file_path.get_basename ()), "%s\n%s".printf ( From 6cb2cd2aec7f99b1ddb1f8d3f17b4dc60e2db48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 09:28:12 -0700 Subject: [PATCH 05/11] Don't bind, transfer can be null --- src/Dialog/ReceiverDialog.vala | 9 ++++----- src/Dialog/SenderDialog.vala | 11 +++-------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index 676ae29..1d78cb3 100644 --- a/src/Dialog/ReceiverDialog.vala +++ b/src/Dialog/ReceiverDialog.vala @@ -117,13 +117,10 @@ public class ReceiverDialog : Granite.Dialog { } catch (Error e) { GLib.warning (e.message); } - destroy (); - } else { - hide (); } - }); - transfer.bind_property ("status", this, "hide-on-close",SYNC_CREATE); + close (); + }); } public void set_transfer (string devicename, string deviceicon, string objectpath) { @@ -146,6 +143,8 @@ public class ReceiverDialog : Granite.Dialog { } private void tranfer_progress () { + hide_on_close = transfer.status == "active"; + try { switch (transfer.status) { case "error": diff --git a/src/Dialog/SenderDialog.vala b/src/Dialog/SenderDialog.vala index 52a1e52..a027c8a 100644 --- a/src/Dialog/SenderDialog.vala +++ b/src/Dialog/SenderDialog.vala @@ -127,17 +127,10 @@ public class SenderDialog : Granite.Dialog { remove_session.begin (); } } - } else { - if (transfer.status == "active") { - hide (); - } else { - destroy (); - } + close (); } }); - - transfer.bind_property ("status", this, "hide-on-close",SYNC_CREATE); } public void add_files (Gee.ArrayList files, Bluetooth.Device device) { @@ -300,6 +293,8 @@ public class SenderDialog : Granite.Dialog { } private void tranfer_progress () { + hide_on_close = transfer.status == "active"; + switch (transfer.status) { case "error": hide (); From 6ba8364aec91d2b663ea0700373c89550e4df99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 09:30:39 -0700 Subject: [PATCH 06/11] Close, not destroy --- src/Dialog/ReceiverDialog.vala | 4 ++-- src/Dialog/SenderDialog.vala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index 1d78cb3..960dbf6 100644 --- a/src/Dialog/ReceiverDialog.vala +++ b/src/Dialog/ReceiverDialog.vala @@ -152,7 +152,7 @@ public class ReceiverDialog : Granite.Dialog { notification.set_title (_("File transfer failed")); notification.set_body (GLib.Markup.printf_escaped (_("%s File: %s not received"), device_label.get_label (), transfer.name)); Application.get_default ().send_notification ("io.elementary.bluetooth", notification); - destroy (); + close (); break; case "queued": break; @@ -165,7 +165,7 @@ public class ReceiverDialog : Granite.Dialog { break; case "complete": move_to_folder (path_folder); - destroy (); + close (); break; } } catch (Error e) { diff --git a/src/Dialog/SenderDialog.vala b/src/Dialog/SenderDialog.vala index a027c8a..cb46dc7 100644 --- a/src/Dialog/SenderDialog.vala +++ b/src/Dialog/SenderDialog.vala @@ -251,7 +251,7 @@ public class SenderDialog : Granite.Dialog { create_session.begin (); present (); } else { - destroy (); + close (); } bt_retry.destroy (); @@ -339,7 +339,7 @@ public class SenderDialog : Granite.Dialog { send_notify (); if (!next_file ()) { remove_session.begin (); - destroy (); + close (); } break; } From 2a5c128cd3bafe556aa998600ac375c402e8b693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 10:49:24 -0700 Subject: [PATCH 07/11] Check for input devices --- src/Services/Manager.vala | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index 0b9f757..4b165f9 100644 --- a/src/Services/Manager.vala +++ b/src/Services/Manager.vala @@ -28,6 +28,8 @@ public class Bluetooth.ObjectManager : Object { private GLib.DBusObjectManagerClient object_manager; private RFKillManager rfkill; + private static Gdk.SeatCapabilities capabilities; + construct { settings = new Settings ("io.elementary.desktop.bluetooth"); @@ -54,6 +56,70 @@ public class Bluetooth.ObjectManager : Object { }); register_agent (); + + unowned var display_manager = Gdk.DisplayManager.@get (); + display_manager.display_opened.connect (init_for_display); + + foreach (unowned var display in display_manager.list_displays ()) { + init_for_display (display); + } + } + + private static void init_for_display (Gdk.Display display) { + var seat = display.get_default_seat (); + if (seat == null) { + return; + } + + set_capabilities_for_seat (seat); + identify_missing_capabilities (); + + seat.device_added.connect (() => { + critical ("device added"); + set_capabilities_for_seat (seat); + }); + + seat.device_removed.connect (() => { + critical ("device removed"); + set_capabilities_for_seat (seat); + identify_missing_capabilities (); + }); + } + + private static void set_capabilities_for_seat (Gdk.Seat seat) { + var seat_capabilities = seat.get_capabilities (); + + if (POINTER in seat_capabilities && seat.get_pointer () != null) { + critical ("pointer %s added", seat.get_pointer ().name); + capabilities |= POINTER; + } else { + critical ("pointer removed"); + capabilities -= POINTER; + } + + if (TOUCH in seat_capabilities) { + capabilities |= TOUCH; + } else { + capabilities -= TOUCH; + } + + if (KEYBOARD in seat_capabilities) { + critical ("keyboard added"); + capabilities |= KEYBOARD; + } else { + critical ("keyboard removed"); + capabilities -= KEYBOARD; + } + } + + private static void identify_missing_capabilities () { + if (!(KEYBOARD in capabilities) && !(TOUCH in capabilities)) { + critical ("we need a keyboard"); + } + + if (!(POINTER in capabilities) && !(TOUCH in capabilities)) { + critical ("we need a mouse"); + } } public async void create_manager () { From 46ae07866d6a31730a3742aa74e85ba1309e8aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 10:57:46 -0700 Subject: [PATCH 08/11] try listing all seats --- src/Services/Manager.vala | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index 4b165f9..97182e2 100644 --- a/src/Services/Manager.vala +++ b/src/Services/Manager.vala @@ -66,11 +66,17 @@ public class Bluetooth.ObjectManager : Object { } private static void init_for_display (Gdk.Display display) { - var seat = display.get_default_seat (); - if (seat == null) { - return; + foreach (unowned var seat in display.list_seats ()) { + init_seat (seat); } + display.seat_added.connect (init_seat); + display.seat_removed.connect (() => { + critical ("seat removed"); + }); + } + + private static void init_seat (Gdk.Seat seat) { set_capabilities_for_seat (seat); identify_missing_capabilities (); @@ -86,6 +92,7 @@ public class Bluetooth.ObjectManager : Object { }); } + private static void set_capabilities_for_seat (Gdk.Seat seat) { var seat_capabilities = seat.get_capabilities (); From b9df75cf9047809c6f7885b8156da6e236f12a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 12:13:38 -0700 Subject: [PATCH 09/11] Get device removed events --- src/Services/Manager.vala | 79 +++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index 97182e2..a6dfe95 100644 --- a/src/Services/Manager.vala +++ b/src/Services/Manager.vala @@ -66,59 +66,56 @@ public class Bluetooth.ObjectManager : Object { } private static void init_for_display (Gdk.Display display) { - foreach (unowned var seat in display.list_seats ()) { - init_seat (seat); - } + var seat = display.get_default_seat (); - display.seat_added.connect (init_seat); - display.seat_removed.connect (() => { - critical ("seat removed"); - }); - } + foreach (unowned var device in seat.get_devices (ALL)) { + add_capabilities_for_device (device); + } - private static void init_seat (Gdk.Seat seat) { - set_capabilities_for_seat (seat); identify_missing_capabilities (); - seat.device_added.connect (() => { - critical ("device added"); - set_capabilities_for_seat (seat); - }); - - seat.device_removed.connect (() => { - critical ("device removed"); - set_capabilities_for_seat (seat); - identify_missing_capabilities (); - }); + seat.device_added.connect (add_capabilities_for_device); + seat.device_removed.connect (remove_capabilities_for_device); } - private static void set_capabilities_for_seat (Gdk.Seat seat) { - var seat_capabilities = seat.get_capabilities (); - - if (POINTER in seat_capabilities && seat.get_pointer () != null) { - critical ("pointer %s added", seat.get_pointer ().name); - capabilities |= POINTER; - } else { - critical ("pointer removed"); - capabilities -= POINTER; - } - - if (TOUCH in seat_capabilities) { - capabilities |= TOUCH; - } else { - capabilities -= TOUCH; + private static void add_capabilities_for_device (Gdk.Device device) { + switch (device.source) { + case KEYBOARD: + capabilities += KEYBOARD; + break; + case MOUSE: + capabilities += POINTER; + break; + case PEN: + case TABLET_PAD: + case TOUCHPAD: + case TOUCHSCREEN: + case TRACKPOINT: + break; } + } - if (KEYBOARD in seat_capabilities) { - critical ("keyboard added"); - capabilities |= KEYBOARD; - } else { - critical ("keyboard removed"); - capabilities -= KEYBOARD; + private static void remove_capabilities_for_device (Gdk.Device device) { + critical ("device removed"); + switch (device.source) { + case KEYBOARD: + capabilities -= KEYBOARD; + break; + case MOUSE: + capabilities -= POINTER; + break; + case PEN: + case TABLET_PAD: + case TOUCHPAD: + case TOUCHSCREEN: + case TRACKPOINT: + break; } + identify_missing_capabilities (); } + private static void identify_missing_capabilities () { if (!(KEYBOARD in capabilities) && !(TOUCH in capabilities)) { critical ("we need a keyboard"); From 3fea4c92fe0b78a022b64071d0a3d3f61c353006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 12:38:03 -0700 Subject: [PATCH 10/11] DRY etc --- src/Services/Manager.vala | 75 +++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index a6dfe95..9377d72 100644 --- a/src/Services/Manager.vala +++ b/src/Services/Manager.vala @@ -65,63 +65,54 @@ public class Bluetooth.ObjectManager : Object { } } - private static void init_for_display (Gdk.Display display) { - var seat = display.get_default_seat (); - - foreach (unowned var device in seat.get_devices (ALL)) { - add_capabilities_for_device (device); - } - - identify_missing_capabilities (); - - seat.device_added.connect (add_capabilities_for_device); - seat.device_removed.connect (remove_capabilities_for_device); - } - - - private static void add_capabilities_for_device (Gdk.Device device) { + private static Gdk.SeatCapabilities get_capabilities_for_device (Gdk.Device device) { switch (device.source) { case KEYBOARD: - capabilities += KEYBOARD; - break; + return Gdk.SeatCapabilities.KEYBOARD; case MOUSE: - capabilities += POINTER; - break; - case PEN: - case TABLET_PAD: case TOUCHPAD: - case TOUCHSCREEN: case TRACKPOINT: - break; + return Gdk.SeatCapabilities.POINTER; + case PEN: + return Gdk.SeatCapabilities.TABLET_STYLUS; + case TOUCHSCREEN: + return Gdk.SeatCapabilities.TOUCH; } + + return Gdk.SeatCapabilities.NONE; } - private static void remove_capabilities_for_device (Gdk.Device device) { - critical ("device removed"); - switch (device.source) { - case KEYBOARD: - capabilities -= KEYBOARD; - break; - case MOUSE: - capabilities -= POINTER; - break; - case PEN: - case TABLET_PAD: - case TOUCHPAD: - case TOUCHSCREEN: - case TRACKPOINT: - break; + private static void init_for_display (Gdk.Display display) { + var seat = display.get_default_seat (); + + // set.get_capabilities is unreliable, so we track them ourselves + foreach (unowned var device in seat.get_devices (ALL)) { + capabilities += get_capabilities_for_device (device); } - identify_missing_capabilities (); + + identify_missing_capabilities (seat); + + seat.device_added.connect ((device) => { + capabilities += get_capabilities_for_device (device); + }); + + seat.device_removed.connect ((seat, device) => { + capabilities -= get_capabilities_for_device (device); + identify_missing_capabilities (seat); + }); } + private static void identify_missing_capabilities (Gdk.Seat seat) { + // OSK and touch is viable + if (TOUCH in capabilities) { + return; + } - private static void identify_missing_capabilities () { - if (!(KEYBOARD in capabilities) && !(TOUCH in capabilities)) { + if (!(KEYBOARD in capabilities)) { critical ("we need a keyboard"); } - if (!(POINTER in capabilities) && !(TOUCH in capabilities)) { + if (!(POINTER in capabilities) && !(TABLET_STYLUS in capabilities)) { critical ("we need a mouse"); } } From 486fddb0fe97209379d187d42bc5ee7546a2f3f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 18 Aug 2026 13:21:00 -0700 Subject: [PATCH 11/11] Proof of concept --- src/Services/Manager.vala | 56 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index 9377d72..f6350c9 100644 --- a/src/Services/Manager.vala +++ b/src/Services/Manager.vala @@ -82,7 +82,7 @@ public class Bluetooth.ObjectManager : Object { return Gdk.SeatCapabilities.NONE; } - private static void init_for_display (Gdk.Display display) { + private void init_for_display (Gdk.Display display) { var seat = display.get_default_seat (); // set.get_capabilities is unreliable, so we track them ourselves @@ -102,7 +102,7 @@ public class Bluetooth.ObjectManager : Object { }); } - private static void identify_missing_capabilities (Gdk.Seat seat) { + private void identify_missing_capabilities (Gdk.Seat seat) { // OSK and touch is viable if (TOUCH in capabilities) { return; @@ -113,10 +113,60 @@ public class Bluetooth.ObjectManager : Object { } if (!(POINTER in capabilities) && !(TABLET_STYLUS in capabilities)) { - critical ("we need a mouse"); + find_pairing_mice (); } } + private async void find_pairing_mice () { + yield start_discovery(); + + var devices = get_devices (); + foreach (var device in devices) { + var connected = yield connect_mouse (device); + if (connected) { + return; + }; + } + + // FIXME: disconnect this if we get paired + device_added.connect ((device) => { + connect_mouse (device); + }); + } + + private async bool connect_mouse (Device device) { + if (device.paired) { + return false; + } + + if (device.name == null) { + return false; + } + + if (device.icon == "input-mouse") { + try { + device.pair (); + // If pairing is successful, mark devices as trusted so they autoconnect + device.trusted = device.paired; + stop_discovery (); + + var notification = new Notification (_("Automatically paired ā€œ%sā€").printf (device.name)); + notification.set_body (_("A nearby device in pairing mode was automatically connected")); + notification.set_category ("device.added"); + notification.set_icon (new ThemedIcon (device.icon)); + + var application = GLib.Application.get_default (); + application.send_notification (null, notification); + + return true; + } catch (Error e) { + critical (e.message); + } + } + + return false; + } + public async void create_manager () { try { object_manager = yield new GLib.DBusObjectManagerClient.for_bus.begin (