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 () {