diff --git a/data/applications.gresource.xml b/data/applications.gresource.xml new file mode 100644 index 00000000..067e48d6 --- /dev/null +++ b/data/applications.gresource.xml @@ -0,0 +1,6 @@ + + + + background.svg + + diff --git a/data/background.svg b/data/background.svg new file mode 100644 index 00000000..3c2b63b9 --- /dev/null +++ b/data/background.svg @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/data/meson.build b/data/meson.build index be9909aa..980b974b 100644 --- a/data/meson.build +++ b/data/meson.build @@ -1,3 +1,10 @@ +gresource = gnome.compile_resources( + 'applications-resources', + 'applications.gresource.xml', + source_dir: meson.current_source_dir() +) + + i18n.merge_file( input: 'applications.metainfo.xml.in', output: 'io.elementary.settings.applications.metainfo.xml', diff --git a/src/Permissions/Backend/App.vala b/src/Permissions/Backend/App.vala index 51bd1cf8..85d0f31b 100644 --- a/src/Permissions/Backend/App.vala +++ b/src/Permissions/Backend/App.vala @@ -25,6 +25,7 @@ public class Permissions.Backend.App : GLib.Object { public Flatpak.InstalledRef installed_ref { get; construct; } public string id { get; private set; } public string name { get; private set; } + public Icon icon { get; private set; } public GenericArray settings; private const string GROUP = "Context"; @@ -35,7 +36,15 @@ public class Permissions.Backend.App : GLib.Object { construct { id = installed_ref.get_name (); - name = installed_ref.get_appdata_name () ?? id; + + var appinfo = new GLib.DesktopAppInfo (id + ".desktop"); + if (appinfo != null) { + name = appinfo.get_name (); + icon = appinfo.get_icon () ?? new ThemedIcon ("application-default-icon"); + } else { + icon = new ThemedIcon ("application-default-icon"); + name = id; + } settings = new GenericArray (); diff --git a/src/Permissions/Backend/PermissionStore.vala b/src/Permissions/Backend/PermissionStore.vala new file mode 100644 index 00000000..d32b24b4 --- /dev/null +++ b/src/Permissions/Backend/PermissionStore.vala @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: LGPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. (https://elementary.io) + */ + +[DBus (name = "org.freedesktop.impl.portal.PermissionStore", timeout = 120000)] +public interface PermissionStoreDBus : GLib.Object { + public signal void changed (string table, string id, bool deleted, GLib.Variant data, [DBus (signature = "a{sas}")] GLib.Variant permissions); + public abstract async void set_permission (string table, bool create, string id, string app, string[] permissions) throws DBusError, IOError; + public abstract async string[] get_permission (string table, string id, string app) throws DBusError, IOError; +} + +public class Permissions.PermissionStore : GLib.Object { + public signal void changed (); + + private const string DBUS_NAME = "org.freedesktop.impl.portal.PermissionStore"; + private const string DBUS_PATH = "/org/freedesktop/impl/portal/PermissionStore"; + private const uint RECONNECT_TIMEOUT = 5000U; + + private static PermissionStore? instance; + public static unowned PermissionStore get_default () { + if (instance == null) { + instance = new PermissionStore (); + } + + return instance; + } + + public PermissionStoreDBus dbus { get; private set; default = null; } + + construct { + Bus.watch_name ( + BusType.SESSION, DBUS_NAME, AUTO_START, + () => try_connect (), name_vanished_callback + ); + } + + private PermissionStore () { } + + private void try_connect () { + Bus.get_proxy.begin (SESSION, DBUS_NAME, DBUS_PATH, 0, null, (obj, res) => { + try { + dbus = Bus.get_proxy.end (res); + dbus.changed.connect (() => changed ()); + } catch (Error e) { + critical (e.message); + Timeout.add (RECONNECT_TIMEOUT, () => { + try_connect (); + return GLib.Source.REMOVE; + }); + } + }); + } + + private void name_vanished_callback (DBusConnection connection, string name) { + dbus = null; + } +} diff --git a/src/Permissions/PermissionsPlug.vala b/src/Permissions/PermissionsPlug.vala index d89abc39..c7eafad3 100644 --- a/src/Permissions/PermissionsPlug.vala +++ b/src/Permissions/PermissionsPlug.vala @@ -73,7 +73,10 @@ public class Permissions.Plug : Gtk.Box { child = scrolled_window }; - var sidebar = new Gtk.Box (VERTICAL, 12); + var sidebar = new Gtk.Box (VERTICAL, 12) { + margin_bottom = 12, + margin_start = 12 + }; sidebar.append (search_entry); sidebar.append (frame); @@ -94,12 +97,7 @@ public class Permissions.Plug : Gtk.Box { show_row (row); } - var grid = new Gtk.Grid () { - margin_end = 12, - margin_bottom = 12, - margin_start = 12, - column_spacing = 12 - }; + var grid = new Gtk.Grid (); grid.attach (sidebar, 0, 0, 1, 1); grid.attach (app_settings_view, 1, 0, 2, 1); diff --git a/src/Permissions/Widgets/AppSettingsView.vala b/src/Permissions/Widgets/AppSettingsView.vala index 0e8fdc16..f77bedf6 100644 --- a/src/Permissions/Widgets/AppSettingsView.vala +++ b/src/Permissions/Widgets/AppSettingsView.vala @@ -1,5 +1,5 @@ /* -* Copyright 2020 elementary, Inc. (https://elementary.io) +* Copyright 2020-2024 elementary, Inc. (https://elementary.io) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public @@ -19,15 +19,76 @@ * Authored by: Marius Meisenzahl */ -public class Permissions.Widgets.AppSettingsView : Gtk.Grid { +public class Permissions.Widgets.AppSettingsView : Switchboard.SettingsPage { public Backend.App? selected_app { get; set; default = null; } - private Gtk.ListBox list_box; + private const string BACKGROUND_TABLE = "background"; + private const string BACKGROUND_ID = "background"; + + private Gtk.ListBox sandbox_box; + private Gtk.ListBox permission_box; private Gtk.Button reset_button; + private Gtk.Switch background_switch; + private Gtk.LinkButton notification_button; construct { notify["selected-app"].connect (update_view); + var background_image = new Gtk.Image.from_icon_name ("permissions-background") { + icon_size = LARGE + }; + + var background_label = new Gtk.Label (_("Background Activity")) { + hexpand = true, + xalign = 0 + }; + + var background_description = new Gtk.Label (_("Perform tasks and use system resources while its window is closed.")) { + xalign = 0, + wrap = true + }; + background_description.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + background_description.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); + + background_switch = new Gtk.Switch () { + valign = CENTER + }; + + var background_grid = new Gtk.Grid () { + column_spacing = 6 + }; + background_grid.attach (background_image, 0, 0, 1, 2); + background_grid.attach (background_label, 1, 0); + background_grid.attach (background_description, 1, 1); + background_grid.attach (background_switch, 2, 0, 1, 2); + + var notification_icon = new Gtk.Image.from_icon_name ("preferences-system-notifications") { + icon_size = LARGE + }; + + var notification_label = new Gtk.Label (_("Notifications")) { + hexpand = true, + xalign = 0 + }; + + notification_button = new Gtk.LinkButton ("settings://notifications") { + icon_name = "view-more-horizontal-symbolic" + }; + + var notification_box = new Gtk.Box (HORIZONTAL, 6); + notification_box.append (notification_icon); + notification_box.append (notification_label); + notification_box.append (notification_button); + + permission_box = new Gtk.ListBox () { + hexpand = true, + selection_mode = NONE + }; + permission_box.add_css_class ("boxed-list"); + permission_box.add_css_class (Granite.STYLE_CLASS_RICH_LIST); + permission_box.append (background_grid); + permission_box.append (notification_box); + var homefolder_widget = new PermissionSettingsWidget ( Plug.permission_names["filesystems=home"], _("Access your entire Home folder, including any hidden folders."), @@ -84,36 +145,29 @@ public class Permissions.Widgets.AppSettingsView : Gtk.Grid { new Backend.PermissionSettings ("devices=dri") ); - list_box = new Gtk.ListBox () { + sandbox_box = new Gtk.ListBox () { hexpand = true, - vexpand = true - }; - list_box.add_css_class (Granite.STYLE_CLASS_RICH_LIST); - list_box.append (homefolder_widget); - list_box.append (sysfolders_widget); - list_box.append (devices_widget); - list_box.append (network_widget); - list_box.append (bluetooth_widget); - list_box.append (printing_widget); - list_box.append (ssh_widget); - list_box.append (gpu_widget); - - var scrolled_window = new Gtk.ScrolledWindow () { - child = list_box + vexpand = true, + selection_mode = NONE }; + sandbox_box.add_css_class ("boxed-list"); + sandbox_box.add_css_class (Granite.STYLE_CLASS_RICH_LIST); + sandbox_box.append (homefolder_widget); + sandbox_box.append (sysfolders_widget); + sandbox_box.append (devices_widget); + sandbox_box.append (network_widget); + sandbox_box.append (bluetooth_widget); + sandbox_box.append (printing_widget); + sandbox_box.append (ssh_widget); + sandbox_box.append (gpu_widget); - var frame = new Gtk.Frame (null) { - child = scrolled_window - }; - frame.add_css_class (Granite.STYLE_CLASS_VIEW); + var box = new Gtk.Box (VERTICAL, 24); + box.append (permission_box); + box.append (sandbox_box); - reset_button = new Gtk.Button.with_label (_("Reset to Defaults")) { - halign = Gtk.Align.END - }; + child = box; - row_spacing = 24; - attach (frame, 0, 0); - attach (reset_button, 0, 1); + reset_button = add_button (_("Reset to Defaults")); update_view (); @@ -126,6 +180,32 @@ public class Permissions.Widgets.AppSettingsView : Gtk.Grid { ssh_widget.changed_permission_settings.connect (change_permission_settings); gpu_widget.changed_permission_settings.connect (change_permission_settings); + background_switch.notify["active"].connect (() => { + string[] permissions; + if (background_switch.active) { + permissions += "yes"; + } else { + permissions += "no"; + } + + try { + PermissionStore.get_default ().dbus.set_permission.begin (BACKGROUND_TABLE, true, BACKGROUND_ID, selected_app.id, permissions); + } catch (Error e) { + critical (e.message); + var dialog = new Granite.MessageDialog ( + _("Couldn't set background activity permission"), + e.message, + new ThemedIcon ("preferences-system") + ) { + badge_icon = new ThemedIcon ("dialog-error"), + modal = true, + transient_for = (Gtk.Window) get_root () + }; + dialog.present (); + dialog.response.connect (dialog.destroy); + } + }); + reset_button.clicked.connect (() => { if (selected_app != null) { selected_app.reset_settings_to_standard (); @@ -135,7 +215,7 @@ public class Permissions.Widgets.AppSettingsView : Gtk.Grid { } private void initialize_settings_view () { - var children = list_box.observe_children (); + var children = sandbox_box.observe_children (); for (var iter = 0; iter < children.get_n_items (); iter++) { if (children.get_item (iter) is PermissionSettingsWidget) { var widget = (PermissionSettingsWidget) children.get_item (iter); @@ -151,14 +231,13 @@ public class Permissions.Widgets.AppSettingsView : Gtk.Grid { initialize_settings_view (); if (selected_app == null) { - list_box.sensitive = false; - reset_button.sensitive = false; + sensitive = false; return; } var should_enable_reset = false; selected_app.settings.foreach ((settings) => { - var children = list_box.observe_children (); + var children = sandbox_box.observe_children (); for (var iter = 0; iter < children.get_n_items (); iter++) { if (children.get_item (iter) is PermissionSettingsWidget) { var widget = (PermissionSettingsWidget) children.get_item (iter); @@ -175,11 +254,53 @@ public class Permissions.Widgets.AppSettingsView : Gtk.Grid { } } - list_box.sensitive = true; + sensitive = true; reset_button.sensitive = should_enable_reset; }); + update_permissions (); + var permission_store = PermissionStore.get_default (); + permission_store.notify["dbus"].connect (update_permissions); + permission_store.changed.connect (update_permissions); + + notification_button.uri = "settings://notifications/%s".printf (selected_app.id); + update_property (Gtk.AccessibleProperty.LABEL, _("%s permissions").printf (selected_app.name), -1); + title = selected_app.name; + icon = selected_app.icon; + } + + private void update_permissions () { + var permission_store = PermissionStore.get_default (); + if (permission_store.dbus == null) { + permission_box.sensitive = false; + return; + } + + permission_box.sensitive = true; + + permission_store.dbus.get_permission.begin (BACKGROUND_TABLE, BACKGROUND_ID, selected_app.id, (obj, res) => { + try { + var background_permission = permission_store.dbus.get_permission.end (res); + + // A lack of explicit permission is considered permission + // to allow pre-emptive opt-out + background_switch.active = background_permission[0] != "no"; + } catch (Error e) { + critical (e.message); + var dialog = new Granite.MessageDialog ( + _("Couldn't get background activity permission"), + e.message, + new ThemedIcon ("preferences-system") + ) { + badge_icon = new ThemedIcon ("dialog-error"), + modal = true, + transient_for = (Gtk.Window) get_root () + }; + dialog.present (); + dialog.response.connect (dialog.destroy); + } + }); } private void change_permission_settings (Backend.PermissionSettings settings) { diff --git a/src/Permissions/Widgets/PermissionSettingsWidget.vala b/src/Permissions/Widgets/PermissionSettingsWidget.vala index 63e58d22..b31b0071 100644 --- a/src/Permissions/Widgets/PermissionSettingsWidget.vala +++ b/src/Permissions/Widgets/PermissionSettingsWidget.vala @@ -40,7 +40,7 @@ public class Permissions.Widgets.PermissionSettingsWidget : Gtk.ListBoxRow { construct { var icon = new Gtk.Image.from_icon_name (icon_name) { - pixel_size = 32, + icon_size = LARGE, tooltip_text = settings.context }; @@ -48,19 +48,20 @@ public class Permissions.Widgets.PermissionSettingsWidget : Gtk.ListBoxRow { halign = Gtk.Align.START, hexpand = true }; - name_label.add_css_class (Granite.STYLE_CLASS_H3_LABEL); var description_label = new Gtk.Label (description) { wrap = true, xalign = 0 }; + description_label.add_css_class (Granite.STYLE_CLASS_DIM_LABEL); + description_label.add_css_class (Granite.STYLE_CLASS_SMALL_LABEL); var allow_switch = new Gtk.Switch () { valign = Gtk.Align.CENTER }; var grid = new Gtk.Grid () { - column_spacing = 12 + column_spacing = 6 }; grid.attach (icon, 0, 0, 1, 2); grid.attach (name_label, 1, 0); diff --git a/src/Permissions/Widgets/SidebarRow.vala b/src/Permissions/Widgets/SidebarRow.vala index 134f4c3a..5aa183d2 100644 --- a/src/Permissions/Widgets/SidebarRow.vala +++ b/src/Permissions/Widgets/SidebarRow.vala @@ -30,16 +30,10 @@ public class Permissions.SidebarRow : Gtk.ListBoxRow { construct { hexpand = true; - var appinfo = new GLib.DesktopAppInfo (app.id + ".desktop"); - Gtk.Image image; - if (appinfo != null && appinfo.get_icon () != null) { - image = new Gtk.Image.from_gicon (appinfo.get_icon ()); - } else { - image = new Gtk.Image.from_icon_name ("application-default-icon"); - } - - image.pixel_size = 32; + var image = new Gtk.Image.from_gicon (app.icon) { + pixel_size = 32 + }; var title_label = new Gtk.Label (app.name) { ellipsize = Pango.EllipsizeMode.END, diff --git a/src/meson.build b/src/meson.build index 2a5f6e5e..5a7cedef 100644 --- a/src/meson.build +++ b/src/meson.build @@ -16,6 +16,7 @@ plug_files = files( 'Permissions/Backend/AppManager.vala', 'Permissions/Backend/FlatpakManager.vala', 'Permissions/Backend/PermissionSettings.vala', + 'Permissions/Backend/PermissionStore.vala', 'Permissions/Widgets/AppSettingsView.vala', 'Permissions/Widgets/PermissionSettingsWidget.vala', 'Permissions/Widgets/SidebarRow.vala', @@ -27,6 +28,7 @@ switchboard_plugsdir = switchboard_dep.get_pkgconfig_variable('plugsdir', define shared_module( meson.project_name(), + gresource, plug_files, conf_file, dependencies: [