diff --git a/data/dock.metainfo.xml.in b/data/dock.metainfo.xml.in
index 385784e1..a8fe6734 100644
--- a/data/dock.metainfo.xml.in
+++ b/data/dock.metainfo.xml.in
@@ -29,6 +29,8 @@
Updated translations
+ Uninstall Menu Item
+ Add View in AppCenter menu item
Give workspaces an activate state/animation
diff --git a/src/AppSystem/App.vala b/src/AppSystem/App.vala
index 5ebadc82..5ad6b65f 100644
--- a/src/AppSystem/App.vala
+++ b/src/AppSystem/App.vala
@@ -5,7 +5,10 @@
public class Dock.App : Object {
public const string ACTION_GROUP_PREFIX = "app-actions";
- private const string ACTION_PREFIX = ACTION_GROUP_PREFIX + ".";
+ public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + ".";
+ public const string UNINSTALL_ACTION = "uninstall";
+ public const string VIEW_ACTION = "view-in-appcenter";
+
private const string SWITCHEROO_ACTION = "switcheroo";
private const string APP_ACTION = "action.%s";
@@ -51,6 +54,10 @@ public class Dock.App : Object {
public GLib.GenericArray windows { get; private owned set; } // Ordered by stacking order with topmost at 0
private static Dock.SwitcherooControl switcheroo_control;
+ private GLib.SimpleAction uninstall_action;
+ private GLib.SimpleAction view_action;
+
+ private string appstream_comp_id = "";
public App (GLib.DesktopAppInfo app_info, bool pinned) {
Object (app_info: app_info, pinned: pinned);
@@ -101,6 +108,21 @@ public class Dock.App : Object {
app_action_group.add_action (simple_action);
}
+ if (Environment.find_program_in_path ("io.elementary.appcenter") != null) {
+ uninstall_action = new SimpleAction (UNINSTALL_ACTION, null);
+ uninstall_action.activate.connect (action_uninstall);
+
+ view_action = new SimpleAction (VIEW_ACTION, null);
+ view_action.activate.connect (open_in_appcenter);
+
+ app_action_group.add_action (uninstall_action);
+ app_action_group.add_action (view_action);
+
+ var appcenter = Dock.AppCenter.get_default ();
+ appcenter.notify["dbus"].connect (() => on_appcenter_dbus_changed.begin (appcenter));
+ on_appcenter_dbus_changed.begin (appcenter);
+ }
+
notify["pinned"].connect (() => {
check_remove ();
ItemManager.get_default ().sync_pinned ();
@@ -269,4 +291,53 @@ public class Dock.App : Object {
return Source.REMOVE;
});
}
+
+ private void action_uninstall () {
+ var appcenter = Dock.AppCenter.get_default ();
+ if (appcenter.dbus == null || appstream_comp_id == "") {
+ return;
+ }
+
+ appcenter.dbus.uninstall.begin (appstream_comp_id, (obj, res) => {
+ try {
+ appcenter.dbus.uninstall.end (res);
+ } catch (GLib.Error e) {
+ warning (e.message);
+ }
+ });
+ }
+
+ private void open_in_appcenter () {
+ AppInfo.launch_default_for_uri_async.begin ("appstream://" + appstream_comp_id, null, null, (obj, res) => {
+ try {
+ AppInfo.launch_default_for_uri_async.end (res);
+ } catch (Error error) {
+ var message_dialog = new Granite.MessageDialog.with_image_from_icon_name (
+ "Unable to open %s in AppCenter".printf (app_info.get_display_name ()),
+ "",
+ "dialog-error",
+ Gtk.ButtonsType.CLOSE
+ );
+ message_dialog.show_error_details (error.message);
+ message_dialog.response.connect (message_dialog.destroy);
+ message_dialog.present ();
+ }
+ });
+ }
+
+ private async void on_appcenter_dbus_changed (Dock.AppCenter appcenter) {
+ if (appcenter.dbus != null) {
+ try {
+ appstream_comp_id = yield appcenter.dbus.get_component_from_desktop_id (app_info.get_id ());
+ } catch (GLib.Error e) {
+ appstream_comp_id = "";
+ warning (e.message);
+ }
+ } else {
+ appstream_comp_id = "";
+ }
+
+ uninstall_action.set_enabled (appstream_comp_id != "");
+ view_action.set_enabled (appstream_comp_id != "");
+ }
}
diff --git a/src/AppSystem/Launcher.vala b/src/AppSystem/Launcher.vala
index 4ecef3b7..62ca0855 100644
--- a/src/AppSystem/Launcher.vala
+++ b/src/AppSystem/Launcher.vala
@@ -76,12 +76,17 @@ public class Dock.Launcher : BaseItem {
insert_action_group (App.ACTION_GROUP_PREFIX, app.app_action_group);
- var pinned_section = new Menu ();
- pinned_section.append (_("Keep in Dock"), ACTION_PREFIX + PINNED_ACTION);
+ var shell_section = new Menu ();
+ shell_section.append (_("Keep in Dock"), ACTION_PREFIX + PINNED_ACTION);
+
+ if (Environment.find_program_in_path ("io.elementary.appcenter") != null) {
+ shell_section.append (_("Uninstall"), App.ACTION_PREFIX + App.UNINSTALL_ACTION);
+ shell_section.append (_("View in AppCenter"), App.ACTION_PREFIX + App.VIEW_ACTION);
+ }
var menu = new Menu ();
menu.append_section (null, app.app_action_menu);
- menu.append_section (null, pinned_section);
+ menu.append_section (null, shell_section);
popover_menu = new Gtk.PopoverMenu.from_model (menu) {
autohide = true,
diff --git a/src/DBus/AppCenter.vala b/src/DBus/AppCenter.vala
new file mode 100644
index 00000000..b3788de0
--- /dev/null
+++ b/src/DBus/AppCenter.vala
@@ -0,0 +1,57 @@
+/*
+ * SPDX-License-Identifier: GPL-3.0
+ * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io)
+ */
+
+[DBus (name = "io.elementary.appcenter")]
+public interface AppCenterDBus : Object {
+ public abstract async void install (string component_id) throws GLib.Error;
+ public abstract async void update (string component_id) throws GLib.Error;
+ public abstract async void uninstall (string component_id) throws GLib.Error;
+ public abstract async string get_component_from_desktop_id (string desktop_id) throws GLib.Error;
+ public abstract async string[] search_components (string query) throws GLib.Error;
+}
+
+public class Dock.AppCenter : Object {
+ private const string DBUS_NAME = "io.elementary.appcenter";
+ private const string DBUS_PATH = "/io/elementary/appcenter";
+ private const uint RECONNECT_TIMEOUT = 5000U;
+
+ private static AppCenter? instance;
+ public static unowned AppCenter get_default () {
+ if (instance == null) {
+ instance = new AppCenter ();
+ }
+
+ return instance;
+ }
+
+ public AppCenterDBus? dbus { public get; private set; default = null; }
+
+ construct {
+ Bus.watch_name (BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.AUTO_START,
+ () => try_connect (), name_vanished_callback);
+ }
+
+ private AppCenter () {
+
+ }
+
+ private void try_connect () {
+ Bus.get_proxy.begin (BusType.SESSION, DBUS_NAME, DBUS_PATH, 0, null, (obj, res) => {
+ try {
+ dbus = Bus.get_proxy.end (res);
+ } catch (Error e) {
+ warning (e.message);
+ Timeout.add (RECONNECT_TIMEOUT, () => {
+ try_connect ();
+ return false;
+ });
+ }
+ });
+ }
+
+ private void name_vanished_callback (DBusConnection connection, string name) {
+ dbus = null;
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index 1b84b18f..b6a8f052 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -15,6 +15,7 @@ sources = [
'AppSystem' / 'Background' / 'BackgroundAppRow.vala',
'AppSystem' / 'Background' / 'BackgroundItem.vala',
'AppSystem' / 'Background' / 'BackgroundMonitor.vala',
+ 'DBus' / 'AppCenter.vala',
'DBus' / 'GalaDBus.vala',
'DBus' / 'ItemInterface.vala',
'DBus' / 'ShellKeyGrabber.vala',