diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 11a2c1a..0655467 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 systemd-dev meson valac + apt install -y libgranite-7-dev systemd-dev meson valac - name: Build env: DESTDIR: out diff --git a/README.md b/README.md index 51c9a31..00287f8 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 >= 6.0.0 + libgtk4-dev systemd-dev meson valac diff --git a/meson.build b/meson.build index 4cf94a8..f1d5687 100644 --- a/meson.build +++ b/meson.build @@ -13,8 +13,8 @@ i18n = import('i18n') prefix = get_option('prefix') bindir = prefix / get_option('bindir') -granite_dep = dependency ('granite') -gtk_dep = dependency ('gtk+-3.0') +granite_dep = dependency ('granite-7') +gtk_dep = dependency ('gtk4') posix_dep = meson.get_compiler('vala').find_library('posix') subdir('data') 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 7d6d71d..792b088 100644 --- a/src/Dialog/DeviceRow.vala +++ b/src/Dialog/DeviceRow.vala @@ -28,9 +28,11 @@ public class DeviceRow : Gtk.ListBoxRow { } construct { - var image = new Gtk.Image.from_icon_name (device.icon ?? "bluetooth", DND); + var image = new Gtk.Image.from_icon_name (device.icon ?? "bluetooth") { + icon_size = LARGE + }; - state = new Gtk.Image.from_icon_name ("emblem-disabled", MENU) { + state = new Gtk.Image.from_icon_name ("emblem-disabled") { halign = END, valign = END }; @@ -38,7 +40,7 @@ public class DeviceRow : Gtk.ListBoxRow { var state_label = new Gtk.Label (null) { xalign = 0 }; - state_label.get_style_context ().add_class (Granite.STYLE_CLASS_SMALL_LABEL); + state_label.add_css_class (Granite.CssClass.SMALL); var overlay = new Gtk.Overlay () { child = image, @@ -81,7 +83,6 @@ public class DeviceRow : Gtk.ListBoxRow { grid.attach (send_button, 4, 0, 1, 2); child = grid; - show_all (); set_sensitive (adapter.powered); set_status (device.connected); @@ -114,7 +115,7 @@ public class DeviceRow : Gtk.ListBoxRow { send_button.clicked.connect (() => { send_file (device); - get_toplevel ().destroy (); + get_root ().destroy (); }); } diff --git a/src/Dialog/ReceiverDialog.vala b/src/Dialog/ReceiverDialog.vala index ae44e40..960dbf6 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, @@ -101,14 +102,13 @@ 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 ().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) { @@ -117,18 +117,9 @@ public class ReceiverDialog : Granite.Dialog { } catch (Error e) { GLib.warning (e.message); } - destroy (); - } else { - hide_on_delete (); } - }); - delete_event.connect (() => { - if (transfer.status == "active") { - return hide_on_delete (); - } else { - return false; - } + close (); }); } @@ -152,6 +143,8 @@ public class ReceiverDialog : Granite.Dialog { } private void tranfer_progress () { + hide_on_close = transfer.status == "active"; + try { switch (transfer.status) { case "error": @@ -159,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; @@ -172,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/ScanDialog.vala b/src/Dialog/ScanDialog.vala index 98c9ccb..2c3182d 100644 --- a/src/Dialog/ScanDialog.vala +++ b/src/Dialog/ScanDialog.vala @@ -23,7 +23,8 @@ public class ScanDialog : Granite.Dialog { } construct { - 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 = CENTER, halign = CENTER }; @@ -34,7 +35,7 @@ public class ScanDialog : Granite.Dialog { wrap = true, xalign = 0 }; - title_label.get_style_context ().add_class ("primary"); + title_label.add_css_class ("primary"); var info_label = new Gtk.Label (_("Select a Bluetooth Device Below to Send Files")) { max_width_chars = 45, @@ -43,12 +44,9 @@ public class ScanDialog : Granite.Dialog { xalign = 0 }; - var empty_alert = new Granite.Widgets.AlertView ( - _("No Devices Found"), - _("Please ensure that your devices are visible and ready for pairing."), - "" - ); - empty_alert.show_all (); + var empty_alert = new Granite.Placeholder (_("No Devices Found")) { + description = _("Please ensure that your devices are visible and ready for pairing.") + }; list_box = new Gtk.ListBox () { activate_on_single_click = true, @@ -57,8 +55,9 @@ public class ScanDialog : Granite.Dialog { list_box.set_sort_func ((Gtk.ListBoxSortFunc) compare_rows); list_box.set_placeholder (empty_alert); - var scrolled = new Gtk.ScrolledWindow (null, null) { + var scrolled = new Gtk.ScrolledWindow () { child = list_box, + has_frame = true, hexpand = true, vexpand = true, hscrollbar_policy = NEVER, @@ -67,13 +66,12 @@ public class ScanDialog : Granite.Dialog { max_content_height = 350, propagate_natural_height = true }; - scrolled.get_style_context ().add_class (Gtk.STYLE_CLASS_FRAME); var overlay = new Gtk.Overlay () { child = scrolled }; - var overlaybar = new Granite.Widgets.OverlayBar (overlay) { + var overlaybar = new Granite.OverlayBar (overlay) { label = _("Discovering") }; @@ -87,10 +85,10 @@ public class ScanDialog : Granite.Dialog { var content_box = new Gtk.Box (VERTICAL, 0) { valign = CENTER }; - content_box.add (image_grid); - content_box.add (overlay); + content_box.append (image_grid); + content_box.append (overlay); - get_content_area ().add (content_box); + get_content_area ().append (content_box); manager.device_added.connect (add_device); manager.device_removed.connect (device_removed); @@ -124,7 +122,7 @@ public class ScanDialog : Granite.Dialog { } var row = new DeviceRow (device, manager.get_adapter_from_path (device.adapter)); - list_box.add (row); + list_box.append (row); if (list_box.get_selected_row () == null) { list_box.select_row (row); diff --git a/src/Dialog/SenderDialog.vala b/src/Dialog/SenderDialog.vala index 81270ef..cb46dc7 100644 --- a/src/Dialog/SenderDialog.vala +++ b/src/Dialog/SenderDialog.vala @@ -37,7 +37,8 @@ public class SenderDialog : Granite.Dialog { construct { liststore = new Gtk.ListStore (1, typeof (File)); - 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 }; @@ -59,7 +60,7 @@ public class SenderDialog : Granite.Dialog { wrap = true, xalign = 0 }; - path_label.get_style_context ().add_class ("primary"); + path_label.add_css_class ("primary"); device_label = new Gtk.Label (GLib.Markup.printf_escaped ("%s:", _("To"))) { max_width_chars = 45, @@ -106,14 +107,13 @@ 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 ().add (message_grid); + get_content_area ().append (message_grid); add_button (_("Close"), Gtk.ResponseType.CLOSE); var reject_transfer = add_button (_("Cancel"), Gtk.ResponseType.CANCEL); - reject_transfer.get_style_context ().add_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,21 +127,8 @@ public class SenderDialog : Granite.Dialog { remove_session.begin (); } } - destroy (); } else { - if (transfer.status == "active") { - hide_on_delete (); - } else { - destroy (); - } - } - }); - - delete_event.connect (() => { - if (transfer.status == "active") { - return hide_on_delete (); - } else { - return false; + close (); } }); } @@ -241,7 +228,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 ( @@ -257,14 +244,14 @@ public class SenderDialog : Granite.Dialog { }; bt_retry.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); var suggested_button = bt_retry.add_button (_("Retry"), Gtk.ResponseType.ACCEPT); - suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + suggested_button.add_css_class (Granite.CssClass.SUGGESTED); bt_retry.response.connect ((response_id) => { if (response_id == Gtk.ResponseType.ACCEPT) { create_session.begin (); present (); } else { - destroy (); + close (); } bt_retry.destroy (); @@ -306,9 +293,11 @@ public class SenderDialog : Granite.Dialog { } private void tranfer_progress () { + hide_on_close = transfer.status == "active"; + 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 ( @@ -324,7 +313,7 @@ public class SenderDialog : Granite.Dialog { }; bt_retry.add_button (_("Cancel"), Gtk.ResponseType.CANCEL); var suggested_button = bt_retry.add_button (_("Retry"), Gtk.ResponseType.ACCEPT); - suggested_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + suggested_button.add_css_class (Granite.CssClass.SUGGESTED); bt_retry.response.connect ((response_id) => { if (response_id == Gtk.ResponseType.ACCEPT) { @@ -350,7 +339,7 @@ public class SenderDialog : Granite.Dialog { send_notify (); if (!next_file ()) { remove_session.begin (); - destroy (); + close (); } break; } diff --git a/src/Services/Manager.vala b/src/Services/Manager.vala index 0b9f757..f6350c9 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,115 @@ 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 Gdk.SeatCapabilities get_capabilities_for_device (Gdk.Device device) { + switch (device.source) { + case KEYBOARD: + return Gdk.SeatCapabilities.KEYBOARD; + case MOUSE: + case TOUCHPAD: + case TRACKPOINT: + return Gdk.SeatCapabilities.POINTER; + case PEN: + return Gdk.SeatCapabilities.TABLET_STYLUS; + case TOUCHSCREEN: + return Gdk.SeatCapabilities.TOUCH; + } + + return Gdk.SeatCapabilities.NONE; + } + + private 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 (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 void identify_missing_capabilities (Gdk.Seat seat) { + // OSK and touch is viable + if (TOUCH in capabilities) { + return; + } + + if (!(KEYBOARD in capabilities)) { + critical ("we need a keyboard"); + } + + if (!(POINTER in capabilities) && !(TABLET_STYLUS in capabilities)) { + 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 () {