[gnome-software] Show addons on the details page



commit 3cc3e2ca6d72e01010b16a6c4a30353d6ff2fad2
Author: Kalev Lember <kalevlember gmail com>
Date:   Tue Jun 24 16:00:21 2014 +0300

    Show addons on the details page

 po/POTFILES.in                     |    1 +
 src/Makefile.am                    |    3 +
 src/gnome-software.gresource.xml   |    1 +
 src/gs-app-addon-row.c             |  298 ++++++++++++++++++++++++++++++++++++
 src/gs-app-addon-row.h             |   69 +++++++++
 src/gs-app-addon-row.ui            |  102 ++++++++++++
 src/gs-app.c                       |   63 ++++++++
 src/gs-app.h                       |    7 +
 src/gs-plugin-loader.c             |   71 +++++++++-
 src/gs-plugin.h                    |    1 +
 src/gs-shell-details.c             |  186 +++++++++++++++++++++--
 src/gs-shell-details.ui            |   65 ++++++++-
 src/plugins/gs-plugin-appstream.c  |   40 +++++
 src/plugins/gs-plugin-packagekit.c |   42 ++++-
 14 files changed, 924 insertions(+), 25 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 34ebddc..3d3e737 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -7,6 +7,7 @@ src/gnome-software-local-file.desktop.in
 [type: gettext/glade]src/gnome-software.ui
 src/gs-app-folder-dialog.c
 src/gs-application.c
+src/gs-app-addon-row.c
 src/gs-app-row.c
 src/gs-app-tile.c
 src/gs-category.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 0f9526c..34e8091 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -31,6 +31,7 @@ UI_FILES =                                            \
        category-tile.ui                                \
        feature-tile.ui                                 \
        gnome-software.ui                               \
+       gs-app-addon-row.ui                             \
        gs-app-row.ui                                   \
        gs-history-dialog.ui                            \
        gs-shell-category.ui                            \
@@ -95,6 +96,8 @@ gnome_software_SOURCES =                              \
        gs-app.h                                        \
        gs-category.c                                   \
        gs-category.h                                   \
+       gs-app-addon-row.c                              \
+       gs-app-addon-row.h                              \
        gs-app-row.c                                    \
        gs-app-row.h                                    \
        gs-star-widget.c                                \
diff --git a/src/gnome-software.gresource.xml b/src/gnome-software.gresource.xml
index 70cee3a..92a1866 100644
--- a/src/gnome-software.gresource.xml
+++ b/src/gnome-software.gresource.xml
@@ -9,6 +9,7 @@
   <file preprocess="xml-stripblanks">app-tile.ui</file>
   <file preprocess="xml-stripblanks">app-folder-dialog.ui</file>
   <file preprocess="xml-stripblanks">screenshot-image.ui</file>
+  <file preprocess="xml-stripblanks">gs-app-addon-row.ui</file>
   <file preprocess="xml-stripblanks">gs-app-row.ui</file>
   <file preprocess="xml-stripblanks">gs-history-dialog.ui</file>
   <file preprocess="xml-stripblanks">gs-shell-category.ui</file>
diff --git a/src/gs-app-addon-row.c b/src/gs-app-addon-row.c
new file mode 100644
index 0000000..91fa471
--- /dev/null
+++ b/src/gs-app-addon-row.c
@@ -0,0 +1,298 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2012-2013 Richard Hughes <richard hughsie com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gs-app-addon-row.h"
+#include "gs-utils.h"
+
+struct _GsAppAddonRowPrivate
+{
+       GsApp           *app;
+       GtkWidget       *name_box;
+       GtkWidget       *name_label;
+       GtkWidget       *description_label;
+       GtkWidget       *spinner;
+       GtkWidget       *label;
+       GtkWidget       *checkbox;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsAppAddonRow, gs_app_addon_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+       PROP_ZERO,
+       PROP_SELECTED
+};
+
+/**
+ * gs_app_addon_row_get_summary:
+ *
+ * Return value: PangoMarkup
+ **/
+static GString *
+gs_app_addon_row_get_summary (GsAppAddonRow *row)
+{
+       GsAppAddonRowPrivate *priv = row->priv;
+       GString *str = NULL;
+       const gchar *tmp = NULL;
+       gchar *escaped;
+
+       /* try all these things in order */
+       if (gs_app_get_kind (priv->app) == GS_APP_KIND_MISSING)
+               tmp = gs_app_get_summary_missing (priv->app);
+       if (tmp == NULL || (tmp != NULL && tmp[0] == '\0'))
+               tmp = gs_app_get_summary (priv->app);
+       if (tmp == NULL || (tmp != NULL && tmp[0] == '\0'))
+               tmp = gs_app_get_description (priv->app);
+
+       escaped = g_markup_escape_text (tmp, -1);
+       str = g_string_new (escaped);
+       g_free (escaped);
+
+       return str;
+}
+
+void
+gs_app_addon_row_refresh (GsAppAddonRow *row)
+{
+       GsAppAddonRowPrivate *priv = row->priv;
+       GString *str;
+
+       if (row->priv->app == NULL)
+               return;
+
+       /* join the lines */
+       str = gs_app_addon_row_get_summary (row);
+       gs_string_replace (str, "\n", " ");
+       gtk_label_set_markup (GTK_LABEL (priv->description_label), str->str);
+       g_string_free (str, TRUE);
+
+       gtk_label_set_label (GTK_LABEL (priv->name_label),
+                            gs_app_get_name (priv->app));
+
+       /* update the state label */
+       switch (gs_app_get_state (row->priv->app)) {
+       case GS_APP_STATE_QUEUED:
+               gtk_widget_set_visible (priv->label, TRUE);
+               gtk_label_set_label (GTK_LABEL (priv->label), _("Pending"));
+               break;
+       case GS_APP_STATE_UPDATABLE:
+       case GS_APP_STATE_INSTALLED:
+               gtk_widget_set_visible (priv->label, TRUE);
+               gtk_label_set_label (GTK_LABEL (priv->label), _("Installed"));
+               break;
+       case GS_APP_STATE_INSTALLING:
+               gtk_widget_set_visible (priv->label, TRUE);
+               gtk_label_set_label (GTK_LABEL (priv->label), _("Installing"));
+               break;
+       case GS_APP_STATE_REMOVING:
+               gtk_widget_set_visible (priv->label, TRUE);
+               gtk_label_set_label (GTK_LABEL (priv->label), _("Removing"));
+               break;
+       default:
+               gtk_widget_set_visible (priv->label, FALSE);
+               break;
+       }
+
+       /* update the spinner */
+       switch (gs_app_get_state (row->priv->app)) {
+       case GS_APP_STATE_INSTALLING:
+       case GS_APP_STATE_REMOVING:
+               gtk_spinner_start (GTK_SPINNER (priv->spinner));
+               gtk_widget_set_visible (priv->spinner, TRUE);
+               break;
+       default:
+               gtk_spinner_stop (GTK_SPINNER (priv->spinner));
+               gtk_widget_set_visible (priv->spinner, FALSE);
+               break;
+       }
+
+       /* update the checkbox */
+       switch (gs_app_get_state (row->priv->app)) {
+       case GS_APP_STATE_QUEUED:
+               gtk_widget_set_sensitive (priv->checkbox, TRUE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), TRUE);
+               break;
+       case GS_APP_STATE_AVAILABLE:
+       case GS_APP_STATE_LOCAL:
+               gtk_widget_set_sensitive (priv->checkbox, TRUE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), FALSE);
+               break;
+       case GS_APP_STATE_UPDATABLE:
+       case GS_APP_STATE_INSTALLED:
+               gtk_widget_set_sensitive (priv->checkbox, TRUE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), TRUE);
+               break;
+       case GS_APP_STATE_INSTALLING:
+               gtk_widget_set_sensitive (priv->checkbox, FALSE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), TRUE);
+               break;
+       case GS_APP_STATE_REMOVING:
+               gtk_widget_set_sensitive (priv->checkbox, FALSE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), FALSE);
+               break;
+       default:
+               gtk_widget_set_sensitive (priv->checkbox, FALSE);
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), FALSE);
+               break;
+       }
+}
+
+GsApp *
+gs_app_addon_row_get_addon (GsAppAddonRow *row)
+{
+       g_return_val_if_fail (GS_IS_APP_ADDON_ROW (row), NULL);
+       return row->priv->app;
+}
+
+static void
+gs_app_addon_row_notify_props_changed_cb (GsApp *app,
+                                          GParamSpec *pspec,
+                                          GsAppAddonRow *row)
+{
+       gs_app_addon_row_refresh (row);
+}
+
+void
+gs_app_addon_row_set_addon (GsAppAddonRow *row, GsApp *app)
+{
+       g_return_if_fail (GS_IS_APP_ADDON_ROW (row));
+       g_return_if_fail (GS_IS_APP (app));
+
+       row->priv->app = g_object_ref (app);
+
+       g_signal_connect_object (row->priv->app, "notify::state",
+                                G_CALLBACK (gs_app_addon_row_notify_props_changed_cb),
+                                row, 0);
+       gs_app_addon_row_refresh (row);
+}
+
+static void
+gs_app_addon_row_destroy (GtkWidget *object)
+{
+       GsAppAddonRow *row = GS_APP_ADDON_ROW (object);
+       GsAppAddonRowPrivate *priv = row->priv;
+
+       g_clear_object (&priv->app);
+
+       GTK_WIDGET_CLASS (gs_app_addon_row_parent_class)->destroy (object);
+}
+
+static void
+gs_app_addon_row_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+       GsAppAddonRow *row = GS_APP_ADDON_ROW (object);
+
+       switch (prop_id) {
+       case PROP_SELECTED:
+               gs_app_addon_row_set_selected (row, g_value_get_boolean (value));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_addon_row_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+       GsAppAddonRow *row = GS_APP_ADDON_ROW (object);
+
+       switch (prop_id) {
+       case PROP_SELECTED:
+               g_value_set_boolean (value, gs_app_addon_row_get_selected (row));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+gs_app_addon_row_class_init (GsAppAddonRowClass *klass)
+{
+       GParamSpec *pspec;
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+       object_class->set_property = gs_app_addon_row_set_property;
+       object_class->get_property = gs_app_addon_row_get_property;
+
+       widget_class->destroy = gs_app_addon_row_destroy;
+
+       pspec = g_param_spec_boolean ("selected", NULL, NULL,
+                                     FALSE, G_PARAM_READWRITE);
+       g_object_class_install_property (object_class, PROP_SELECTED, pspec);
+
+       gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/software/gs-app-addon-row.ui");
+
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, name_box);
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, name_label);
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, description_label);
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, spinner);
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, label);
+       gtk_widget_class_bind_template_child_private (widget_class, GsAppAddonRow, checkbox);
+}
+
+static void
+checkbox_toggled (GtkWidget *widget, GsAppAddonRow *row)
+{
+       g_object_notify (G_OBJECT (row), "selected");
+}
+
+static void
+gs_app_addon_row_init (GsAppAddonRow *row)
+{
+       GsAppAddonRowPrivate *priv;
+
+       priv = gs_app_addon_row_get_instance_private (row);
+       row->priv = priv;
+
+       gtk_widget_set_has_window (GTK_WIDGET (row), FALSE);
+       gtk_widget_init_template (GTK_WIDGET (row));
+
+       g_signal_connect (priv->checkbox, "toggled",
+                         G_CALLBACK (checkbox_toggled), row);
+}
+
+void
+gs_app_addon_row_set_selected (GsAppAddonRow *row, gboolean selected)
+{
+       gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (row->priv->checkbox), selected);
+}
+
+gboolean
+gs_app_addon_row_get_selected (GsAppAddonRow *row)
+{
+       return gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (row->priv->checkbox));
+}
+
+GtkWidget *
+gs_app_addon_row_new (void)
+{
+       return g_object_new (GS_TYPE_APP_ADDON_ROW, NULL);
+}
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-addon-row.h b/src/gs-app-addon-row.h
new file mode 100644
index 0000000..601de6c
--- /dev/null
+++ b/src/gs-app-addon-row.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2012 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef GS_APP_ADDON_ROW_H
+#define GS_APP_ADDON_ROW_H
+
+#include <gtk/gtk.h>
+
+#include "gs-app.h"
+
+#define GS_TYPE_APP_ADDON_ROW          (gs_app_addon_row_get_type())
+#define GS_APP_ADDON_ROW(obj)          (G_TYPE_CHECK_INSTANCE_CAST((obj), GS_TYPE_APP_ADDON_ROW, 
GsAppAddonRow))
+#define GS_APP_ADDON_ROW_CLASS(cls)    (G_TYPE_CHECK_CLASS_CAST((cls), GS_TYPE_APP_ADDON_ROW, 
GsAppAddonRowClass))
+#define GS_IS_APP_ADDON_ROW(obj)       (G_TYPE_CHECK_INSTANCE_TYPE((obj), GS_TYPE_APP_ADDON_ROW))
+#define GS_IS_APP_ADDON_ROW_CLASS(cls) (G_TYPE_CHECK_CLASS_TYPE((cls), GS_TYPE_APP_ADDON_ROW))
+#define GS_APP_ADDON_ROW_GET_CLASS(obj)        (G_TYPE_INSTANCE_GET_CLASS((obj), GS_TYPE_APP_ADDON_ROW, 
GsAppAddonRowClass))
+
+G_BEGIN_DECLS
+
+typedef struct _GsAppAddonRow          GsAppAddonRow;
+typedef struct _GsAppAddonRowClass     GsAppAddonRowClass;
+typedef struct _GsAppAddonRowPrivate   GsAppAddonRowPrivate;
+
+struct _GsAppAddonRow
+{
+       GtkListBoxRow            parent;
+
+       /*< private >*/
+       GsAppAddonRowPrivate    *priv;
+};
+
+struct _GsAppAddonRowClass
+{
+       GtkListBoxRowClass       parent_class;
+};
+
+GType           gs_app_addon_row_get_type              (void);
+GtkWidget      *gs_app_addon_row_new                   (void);
+void            gs_app_addon_row_refresh               (GsAppAddonRow  *row);
+void            gs_app_addon_row_set_selected          (GsAppAddonRow  *row,
+                                                        gboolean        selected);
+gboolean        gs_app_addon_row_get_selected          (GsAppAddonRow  *row);
+GsApp          *gs_app_addon_row_get_addon             (GsAppAddonRow  *row);
+void            gs_app_addon_row_set_addon             (GsAppAddonRow  *row,
+                                                        GsApp          *app);
+
+G_END_DECLS
+
+#endif /* GS_APP_ADDON_ROW_H */
+
+/* vim: set noexpandtab: */
diff --git a/src/gs-app-addon-row.ui b/src/gs-app-addon-row.ui
new file mode 100644
index 0000000..f518d45
--- /dev/null
+++ b/src/gs-app-addon-row.ui
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.10 -->
+  <template class="GsAppAddonRow" parent="GtkListBoxRow">
+    <property name="visible">True</property>
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="visible">True</property>
+        <property name="margin-top">12</property>
+        <property name="margin-bottom">12</property>
+        <property name="margin-start">18</property>
+        <property name="margin-end">18</property>
+        <property name="orientation">horizontal</property>
+        <child>
+          <object class="GtkCheckButton" id="checkbox">
+            <property name="visible">True</property>
+            <property name="valign">center</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="name_box">
+            <property name="visible">True</property>
+            <property name="margin_start">12</property>
+            <property name="orientation">vertical</property>
+            <property name="valign">start</property>
+            <child>
+              <object class="GtkLabel" id="name_label">
+                <property name="visible">True</property>
+                <property name="max_width_chars">20</property>
+                <property name="xalign">0.0</property>
+                <property name="yalign">0.5</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="description_label">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="yalign">0.5</property>
+                <property name="ellipsize">end</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="end_box">
+            <property name="visible">True</property>
+            <property name="orientation">horizontal</property>
+            <property name="width_request">180</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <child>
+              <object class="GtkSpinner" id="spinner">
+                <property name="margin_start">12</property>
+                <property name="halign">end</property>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="label">
+                <property name="margin_start">12</property>
+                <property name="halign">end</property>
+              </object>
+              <packing>
+                <property name="pack_type">end</property>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gs-app.c b/src/gs-app.c
index 3ead682..e2d84f7 100644
--- a/src/gs-app.c
+++ b/src/gs-app.c
@@ -89,11 +89,14 @@ struct GsAppPrivate
        GHashTable              *metadata;
        GdkPixbuf               *pixbuf;
        GdkPixbuf               *featured_pixbuf;
+       GPtrArray               *addons; /* of GsApp */
+       GHashTable              *addons_hash; /* of "id" */
        GPtrArray               *related; /* of GsApp */
        GHashTable              *related_hash; /* of "id-source" */
        GPtrArray               *history; /* of GsApp */
        guint64                  install_date;
        guint64                  kudos;
+       gboolean                 to_be_installed;
 };
 
 enum {
@@ -1484,6 +1487,39 @@ gs_app_set_metadata (GsApp *app, const gchar *key, const gchar *value)
 }
 
 /**
+ * gs_app_get_addons:
+ */
+GPtrArray *
+gs_app_get_addons (GsApp *app)
+{
+       g_return_val_if_fail (GS_IS_APP (app), NULL);
+       return app->priv->addons;
+}
+
+/**
+ * gs_app_add_addon:
+ */
+void
+gs_app_add_addon (GsApp *app, GsApp *addon)
+{
+       gpointer found;
+       const gchar *id;
+
+       g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (GS_IS_APP (addon));
+
+       id = gs_app_get_id (addon);
+       found = g_hash_table_lookup (app->priv->addons_hash, id);
+       if (found != NULL) {
+               g_debug ("Already added %s as an addon", id);
+               return;
+       }
+       g_hash_table_insert (app->priv->addons_hash, g_strdup (id), GINT_TO_POINTER (1));
+
+       g_ptr_array_add (app->priv->addons, g_object_ref (addon));
+}
+
+/**
  * gs_app_get_related:
  */
 GPtrArray *
@@ -1701,6 +1737,26 @@ gs_app_get_kudos_percentage (GsApp *app)
 }
 
 /**
+ * gs_app_get_to_be_installed:
+ */
+gboolean
+gs_app_get_to_be_installed (GsApp *app)
+{
+       GsAppPrivate *priv = app->priv;
+       return priv->to_be_installed;
+}
+
+/**
+ * gs_app_set_to_be_installed:
+ */
+void
+gs_app_set_to_be_installed (GsApp *app, gboolean to_be_installed)
+{
+       GsAppPrivate *priv = app->priv;
+       priv->to_be_installed = to_be_installed;
+}
+
+/**
  * gs_app_subsume:
  *
  * Imports all the useful data from @other into @app.
@@ -1962,6 +2018,7 @@ gs_app_init (GsApp *app)
        app->priv->sources = g_ptr_array_new_with_free_func (g_free);
        app->priv->source_ids = g_ptr_array_new_with_free_func (g_free);
        app->priv->categories = g_ptr_array_new_with_free_func (g_free);
+       app->priv->addons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->priv->related = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->priv->history = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
        app->priv->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
@@ -1969,6 +2026,10 @@ gs_app_init (GsApp *app)
                                                     g_str_equal,
                                                     g_free,
                                                     g_free);
+       app->priv->addons_hash = g_hash_table_new_full (g_str_hash,
+                                                       g_str_equal,
+                                                       g_free,
+                                                       NULL);
        app->priv->related_hash = g_hash_table_new_full (g_str_hash,
                                                         g_str_equal,
                                                         g_free,
@@ -2011,6 +2072,8 @@ gs_app_finalize (GObject *object)
        g_free (priv->update_details);
        g_free (priv->management_plugin);
        g_hash_table_unref (priv->metadata);
+       g_hash_table_unref (priv->addons_hash);
+       g_ptr_array_unref (priv->addons);
        g_hash_table_unref (priv->related_hash);
        g_ptr_array_unref (priv->related);
        g_ptr_array_unref (priv->history);
diff --git a/src/gs-app.h b/src/gs-app.h
index 43bcba5..fe73838 100644
--- a/src/gs-app.h
+++ b/src/gs-app.h
@@ -87,6 +87,7 @@ typedef enum {
        GS_APP_ID_KIND_FONT,
        GS_APP_ID_KIND_CODEC,
        GS_APP_ID_KIND_WEBAPP,
+       GS_APP_ID_KIND_ADDON,
        GS_APP_ID_KIND_LAST
 } GsAppIdKind;
 
@@ -246,6 +247,9 @@ void                 gs_app_set_rating_kind         (GsApp          *app,
 guint64                 gs_app_get_size                (GsApp          *app);
 void            gs_app_set_size                (GsApp          *app,
                                                 guint64         size);
+GPtrArray      *gs_app_get_addons              (GsApp          *app);
+void            gs_app_add_addon               (GsApp          *app,
+                                                GsApp          *addon);
 GPtrArray      *gs_app_get_related             (GsApp          *app);
 void            gs_app_add_related             (GsApp          *app,
                                                 GsApp          *app2);
@@ -270,6 +274,9 @@ void                 gs_app_add_kudo                (GsApp          *app,
 guint64                 gs_app_get_kudos               (GsApp          *app);
 guint           gs_app_get_kudos_weight        (GsApp          *app);
 guint           gs_app_get_kudos_percentage    (GsApp          *app);
+gboolean        gs_app_get_to_be_installed     (GsApp          *app);
+void            gs_app_set_to_be_installed     (GsApp          *app,
+                                                gboolean        to_be_installed);
 
 G_END_DECLS
 
diff --git a/src/gs-plugin-loader.c b/src/gs-plugin-loader.c
index f338332..3f4b952 100644
--- a/src/gs-plugin-loader.c
+++ b/src/gs-plugin-loader.c
@@ -225,7 +225,9 @@ gs_plugin_loader_run_refine (GsPluginLoader *plugin_loader,
                             GError **error)
 {
        GList *l;
+       GList *addons_list = NULL;
        GList *related_list = NULL;
+       GPtrArray *addons;
        GPtrArray *related;
        GsApp *app;
        GsPlugin *plugin;
@@ -254,6 +256,32 @@ gs_plugin_loader_run_refine (GsPluginLoader *plugin_loader,
                        goto out;
        }
 
+       /* refine addons one layer deep */
+       if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) > 0) {
+               flags &= ~GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS;
+               for (l = *list; l != NULL; l = l->next) {
+                       app = GS_APP (l->data);
+                       addons = gs_app_get_addons (app);
+                       for (i = 0; i < addons->len; i++) {
+                               GsApp *addon = g_ptr_array_index (addons, i);
+                               g_debug ("refining app %s addon %s",
+                                        gs_app_get_id (app),
+                                        gs_app_get_id (addon));
+                               gs_plugin_add_app (&addons_list, addon);
+                       }
+               }
+               if (addons_list != NULL) {
+                       ret = gs_plugin_loader_run_refine (plugin_loader,
+                                                          function_name_parent,
+                                                          &addons_list,
+                                                          flags,
+                                                          cancellable,
+                                                          error);
+                       if (!ret)
+                               goto out;
+               }
+       }
+
        /* also do related packages one layer deep */
        if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED) > 0) {
                flags &= ~GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED;
@@ -287,6 +315,7 @@ gs_plugin_loader_run_refine (GsPluginLoader *plugin_loader,
        /* dedupe applications we already know about */
        gs_plugin_loader_list_dedupe (plugin_loader, *list);
 out:
+       gs_plugin_list_free (addons_list);
        gs_plugin_list_free (related_list);
        gs_plugin_list_free (freeze_list);
        return ret;
@@ -1888,7 +1917,9 @@ gs_plugin_loader_app_action_thread_cb (GTask *task,
        GError *error = NULL;
        GsPluginLoaderAsyncState *state = (GsPluginLoaderAsyncState *) task_data;
        GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+       GPtrArray *addons;
        gboolean ret;
+       guint i;
        guint id;
 
        /* add to list */
@@ -1905,11 +1936,28 @@ gs_plugin_loader_app_action_thread_cb (GTask *task,
                                           cancellable,
                                           &error);
        if (ret) {
-               if (state->state_success != GS_APP_STATE_UNKNOWN)
+               if (state->state_success != GS_APP_STATE_UNKNOWN) {
                        gs_app_set_state (state->app, state->state_success);
+                       addons = gs_app_get_addons (state->app);
+                       for (i = 0; i < addons->len; i++) {
+                               GsApp *addon = g_ptr_array_index (addons, i);
+                               if (gs_app_get_to_be_installed (addon)) {
+                                       gs_app_set_state (addon, state->state_success);
+                                       gs_app_set_to_be_installed (addon, FALSE);
+                               }
+                       }
+               }
                g_task_return_boolean (task, TRUE);
        } else {
                gs_app_set_state (state->app, state->state_failure);
+               addons = gs_app_get_addons (state->app);
+               for (i = 0; i < addons->len; i++) {
+                       GsApp *addon = g_ptr_array_index (addons, i);
+                       if (gs_app_get_to_be_installed (addon)) {
+                               gs_app_set_state (addon, state->state_failure);
+                               gs_app_set_to_be_installed (addon, FALSE);
+                       }
+               }
                g_task_return_error (task, error);
        }
 
@@ -2029,9 +2077,11 @@ save_install_queue (GsPluginLoader *plugin_loader)
 static void
 add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
 {
+       GPtrArray *addons;
+       guint i;
        guint id;
 
-       /* FIXME: persist this */
+       /* queue the app itself */
        g_mutex_lock (&plugin_loader->priv->pending_apps_mutex);
        g_ptr_array_add (plugin_loader->priv->pending_apps, g_object_ref (app));
        g_mutex_unlock (&plugin_loader->priv->pending_apps_mutex);
@@ -2040,12 +2090,22 @@ add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
        id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
        g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
        save_install_queue (plugin_loader);
+
+       /* recursively queue any addons */
+       addons = gs_app_get_addons (app);
+       for (i = 0; i < addons->len; i++) {
+               GsApp *addon = g_ptr_array_index (addons, i);
+               if (gs_app_get_to_be_installed (addon))
+                       add_app_to_install_queue (plugin_loader, addon);
+       }
 }
 
 static gboolean
 remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
 {
+       GPtrArray *addons;
        gboolean ret;
+       guint i;
        guint id;
 
        g_mutex_lock (&plugin_loader->priv->pending_apps_mutex);
@@ -2057,6 +2117,13 @@ remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
                id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
                g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
                save_install_queue (plugin_loader);
+
+               /* recursively remove any queued addons */
+               addons = gs_app_get_addons (app);
+               for (i = 0; i < addons->len; i++) {
+                       GsApp *addon = g_ptr_array_index (addons, i);
+                       remove_app_from_install_queue (plugin_loader, addon);
+               }
        }
 
        return ret;
diff --git a/src/gs-plugin.h b/src/gs-plugin.h
index 76d4a48..c09a9ac 100644
--- a/src/gs-plugin.h
+++ b/src/gs-plugin.h
@@ -96,6 +96,7 @@ typedef enum {
        GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN           = 1 << 10,
        GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED          = 1 << 11,
        GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH        = 1 << 12,
+       GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS           = 1 << 13,
        GS_PLUGIN_REFINE_FLAGS_LAST
 } GsPluginRefineFlags;
 
diff --git a/src/gs-shell-details.c b/src/gs-shell-details.c
index fc8f309..5edef28 100644
--- a/src/gs-shell-details.c
+++ b/src/gs-shell-details.c
@@ -29,6 +29,7 @@
 #include "gs-utils.h"
 
 #include "gs-shell-details.h"
+#include "gs-app-addon-row.h"
 #include "gs-history-dialog.h"
 #include "gs-screenshot-image.h"
 #include "gs-star-widget.h"
@@ -55,6 +56,7 @@ struct GsShellDetailsPrivate
        GtkWidget               *application_details_icon;
        GtkWidget               *application_details_summary;
        GtkWidget               *application_details_title;
+       GtkWidget               *box_addons;
        GtkWidget               *box_details;
        GtkWidget               *box_details_description;
        GtkWidget               *box_details_header;
@@ -69,6 +71,7 @@ struct GsShellDetailsPrivate
        GtkWidget               *infobar_details_package_baseos;
        GtkWidget               *infobar_details_repo;
        GtkWidget               *infobar_details_webapp;
+       GtkWidget               *label_addons_uninstalled_app;
        GtkWidget               *label_details_category_value;
        GtkWidget               *label_details_developer_title;
        GtkWidget               *label_details_developer_value;
@@ -78,6 +81,7 @@ struct GsShellDetailsPrivate
        GtkWidget               *label_details_size_value;
        GtkWidget               *label_details_updated_value;
        GtkWidget               *label_details_version_value;
+       GtkWidget               *list_box_addons;
        GtkWidget               *scrolledwindow_details;
        GtkWidget               *spinner_details;
        GtkWidget               *stack_details;
@@ -513,6 +517,7 @@ gs_shell_details_refresh_all (GsShellDetails *shell_details)
        GError *error = NULL;
        GPtrArray *history;
        GdkPixbuf *pixbuf = NULL;
+       GList *addons, *l;
        GsShellDetailsPrivate *priv = shell_details->priv;
        GtkWidget *widget2;
        const gchar *tmp;
@@ -774,6 +779,83 @@ gs_shell_details_refresh_all (GsShellDetails *shell_details)
                gtk_widget_set_visible (priv->infobar_details_webapp, FALSE);
                break;
        }
+
+       /* only show the "select addons" string if the app isn't yet installed */
+       switch (gs_app_get_state (priv->app)) {
+       case GS_APP_STATE_INSTALLED:
+       case GS_APP_STATE_UPDATABLE:
+               gtk_widget_set_visible (priv->label_addons_uninstalled_app, FALSE);
+               break;
+       default:
+               gtk_widget_set_visible (priv->label_addons_uninstalled_app, TRUE);
+               break;
+       }
+
+       addons = gtk_container_get_children (GTK_CONTAINER (priv->list_box_addons));
+       gtk_widget_set_visible (priv->box_addons, addons != NULL);
+       for (l = addons; l; l = l->next) {
+               /* show checkboxes in front of addons if the app isn't yet installed */
+               switch (gs_app_get_state (priv->app)) {
+               case GS_APP_STATE_INSTALLED:
+               case GS_APP_STATE_UPDATABLE:
+                       break;
+               default:
+                       break;
+               }
+       }
+       g_list_free (addons);
+}
+
+static void
+list_header_func (GtkListBoxRow *row,
+                  GtkListBoxRow *before,
+                  gpointer user_data)
+{
+       GtkWidget *header = NULL;
+       if (before != NULL)
+               header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+       gtk_list_box_row_set_header (row, header);
+}
+
+static gint
+list_sort_func (GtkListBoxRow *a,
+                GtkListBoxRow *b,
+                gpointer user_data)
+{
+       GsApp *a1 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (a));
+       GsApp *a2 = gs_app_addon_row_get_addon (GS_APP_ADDON_ROW (b));
+
+       return g_strcmp0 (gs_app_get_name (a1),
+                         gs_app_get_name (a2));
+}
+
+static void gs_shell_details_addon_selected_cb (GsAppAddonRow *row, GParamSpec *pspec, GsShellDetails 
*shell_details);
+
+static void
+gs_shell_details_refresh_addons (GsShellDetails *shell_details)
+{
+       GsShellDetailsPrivate *priv = shell_details->priv;
+       GPtrArray *addons;
+       guint i;
+
+       gs_container_remove_all (GTK_CONTAINER (priv->list_box_addons));
+
+       addons = gs_app_get_addons (priv->app);
+       for (i = 0; i < addons->len; i++) {
+               GsApp *addon;
+               GtkWidget *row;
+
+               addon = g_ptr_array_index (addons, i);
+               row = gs_app_addon_row_new ();
+
+               gs_app_addon_row_set_addon (GS_APP_ADDON_ROW (row), addon);
+               gtk_container_add (GTK_CONTAINER (priv->list_box_addons), row);
+               gtk_widget_show (row);
+
+               g_signal_connect (row, "notify::selected",
+                                 G_CALLBACK (gs_shell_details_addon_selected_cb),
+                                 shell_details);
+       }
 }
 
 /**
@@ -800,6 +882,7 @@ gs_shell_details_app_refine_cb (GObject *source,
                g_error_free (error);
                return;
        }
+       gs_shell_details_refresh_addons (shell_details);
        gs_shell_details_refresh_all (shell_details);
        gs_shell_details_set_state (shell_details, GS_SHELL_DETAILS_STATE_READY);
 }
@@ -848,6 +931,7 @@ gs_shell_details_filename_to_app_cb (GObject *source,
        /* change widgets */
        gs_shell_details_refresh (shell_details);
        gs_shell_details_refresh_screenshots (shell_details);
+       gs_shell_details_refresh_addons (shell_details);
        gs_shell_details_refresh_all (shell_details);
        gs_shell_details_set_state (shell_details, GS_SHELL_DETAILS_STATE_READY);
 }
@@ -894,7 +978,8 @@ gs_shell_details_set_app (GsShellDetails *shell_details, GsApp *app)
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION |
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
                                           GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH |
-                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL,
+                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL |
+                                          GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS,
                                           priv->cancellable,
                                           gs_shell_details_app_refine_cb,
                                           shell_details);
@@ -1004,10 +1089,10 @@ gs_shell_details_app_removed_cb (GObject *source,
 }
 
 /**
- * gs_shell_details_app_remove_button_cb:
+ * gs_shell_details_app_remove
  **/
 static void
-gs_shell_details_app_remove_button_cb (GtkWidget *widget, GsShellDetails *shell_details)
+gs_shell_details_app_remove (GsShellDetails *shell_details, GsApp *app)
 {
        GsShellDetailsHelper *helper;
        GsShellDetailsPrivate *priv = shell_details->priv;
@@ -1022,7 +1107,7 @@ gs_shell_details_app_remove_button_cb (GtkWidget *widget, GsShellDetails *shell_
                                /* TRANSLATORS: this is a prompt message, and
                                 * '%s' is an application summary, e.g. 'GNOME Clocks' */
                                _("Are you sure you want to remove %s?"),
-                               gs_app_get_name (priv->app));
+                               gs_app_get_name (app));
        g_string_prepend (markup, "<b>");
        g_string_append (markup, "</b>");
        dialog = gtk_message_dialog_new (window,
@@ -1034,20 +1119,20 @@ gs_shell_details_app_remove_button_cb (GtkWidget *widget, GsShellDetails *shell_
        gtk_message_dialog_format_secondary_markup (GTK_MESSAGE_DIALOG (dialog),
                                                    /* TRANSLATORS: longer dialog text */
                                                    _("%s will be removed, and you will have to install it to 
use it again."),
-                                                   gs_app_get_name (priv->app));
+                                                   gs_app_get_name (app));
        /* TRANSLATORS: this is button text to remove the application */
        gtk_dialog_add_button (GTK_DIALOG (dialog), _("Remove"), GTK_RESPONSE_OK);
-       if (gs_app_get_state (priv->app) == GS_APP_STATE_INSTALLED)
+       if (gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
                response = gtk_dialog_run (GTK_DIALOG (dialog));
        else
                response = GTK_RESPONSE_OK; /* pending install */
        if (response == GTK_RESPONSE_OK) {
-               g_debug ("remove %s", gs_app_get_id (priv->app));
+               g_debug ("remove %s", gs_app_get_id (app));
                helper = g_new0 (GsShellDetailsHelper, 1);
                helper->shell_details = g_object_ref (shell_details);
-               helper->app = g_object_ref (priv->app);
+               helper->app = g_object_ref (app);
                gs_plugin_loader_app_action_async (priv->plugin_loader,
-                                                  priv->app,
+                                                  app,
                                                   GS_PLUGIN_LOADER_ACTION_REMOVE,
                                                   priv->cancellable,
                                                   gs_shell_details_app_removed_cb,
@@ -1058,18 +1143,30 @@ gs_shell_details_app_remove_button_cb (GtkWidget *widget, GsShellDetails *shell_
 }
 
 /**
- * gs_shell_details_app_install_button_cb:
+ * gs_shell_details_app_remove_button_cb:
  **/
 static void
-gs_shell_details_app_install_button_cb (GtkWidget *widget, GsShellDetails *shell_details)
+gs_shell_details_app_remove_button_cb (GtkWidget *widget, GsShellDetails *shell_details)
+{
+       GsShellDetailsPrivate *priv = shell_details->priv;
+
+       gs_shell_details_app_remove (shell_details, priv->app);
+}
+
+/**
+ * gs_shell_details_app_install:
+ **/
+static void
+gs_shell_details_app_install (GsShellDetails *shell_details, GsApp *app)
 {
        GsShellDetailsPrivate *priv = shell_details->priv;
        GsShellDetailsHelper *helper;
+
        helper = g_new0 (GsShellDetailsHelper, 1);
        helper->shell_details = g_object_ref (shell_details);
-       helper->app = g_object_ref (priv->app);
+       helper->app = g_object_ref (app);
        gs_plugin_loader_app_action_async (priv->plugin_loader,
-                                          priv->app,
+                                          app,
                                           GS_PLUGIN_LOADER_ACTION_INSTALL,
                                           priv->cancellable,
                                           gs_shell_details_app_installed_cb,
@@ -1077,6 +1174,59 @@ gs_shell_details_app_install_button_cb (GtkWidget *widget, GsShellDetails *shell
 }
 
 /**
+ * gs_shell_details_app_install_button_cb:
+ **/
+static void
+gs_shell_details_app_install_button_cb (GtkWidget *widget, GsShellDetails *shell_details)
+{
+       GsShellDetailsPrivate *priv = shell_details->priv;
+       GList *addons, *l;
+
+       /* Mark ticked addons to be installed together with the app */
+       addons = gtk_container_get_children (GTK_CONTAINER (priv->list_box_addons));
+       for (l = addons; l; l = l->next) {
+               if (gs_app_addon_row_get_selected (l->data)) {
+                       GsApp *addon = gs_app_addon_row_get_addon (l->data);
+
+                       if (gs_app_get_state (addon) == GS_APP_STATE_AVAILABLE)
+                               gs_app_set_to_be_installed (addon, TRUE);
+               }
+       }
+       g_list_free (addons);
+
+       gs_shell_details_app_install (shell_details, priv->app);
+}
+
+/**
+ * gs_shell_details_addon_selected_cb:
+ **/
+static void
+gs_shell_details_addon_selected_cb (GsAppAddonRow *row,
+                                    GParamSpec *pspec,
+                                    GsShellDetails *shell_details)
+{
+       GsShellDetailsPrivate *priv = shell_details->priv;
+       GsApp *addon;
+
+       addon = gs_app_addon_row_get_addon (row);
+
+       /* If the main app is already installed, ticking the addon checkbox
+        * triggers an immediate install. Otherwise we'll install the addon
+        * together with the main app. */
+       switch (gs_app_get_state (priv->app)) {
+       case GS_APP_STATE_INSTALLED:
+       case GS_APP_STATE_UPDATABLE:
+               if (gs_app_addon_row_get_selected (row))
+                       gs_shell_details_app_install (shell_details, addon);
+               else
+                       gs_shell_details_app_remove (shell_details, addon);
+               break;
+       default:
+               break;
+       }
+}
+
+/**
  * gs_shell_details_app_launch_button_cb:
  **/
 static void
@@ -1247,6 +1397,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, application_details_icon);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
application_details_summary);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
application_details_title);
+       gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, box_addons);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, box_details);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, box_details_description);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, box_details_header);
@@ -1261,6 +1412,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
infobar_details_package_baseos);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, infobar_details_repo);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, infobar_details_webapp);
+       gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_addons_uninstalled_app);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_details_category_value);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_details_developer_title);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_details_developer_value);
@@ -1270,6 +1422,7 @@ gs_shell_details_class_init (GsShellDetailsClass *klass)
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, label_details_size_value);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_details_updated_value);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, 
label_details_version_value);
+       gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, list_box_addons);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, scrolledwindow_details);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, spinner_details);
        gtk_widget_class_bind_template_child_private (widget_class, GsShellDetails, stack_details);
@@ -1297,6 +1450,13 @@ gs_shell_details_init (GsShellDetails *shell_details)
                soup_session_add_feature_by_type (priv->session,
                                                  SOUP_TYPE_PROXY_RESOLVER_DEFAULT);
        }
+
+       gtk_list_box_set_header_func (GTK_LIST_BOX (priv->list_box_addons),
+                                     list_header_func,
+                                     shell_details, NULL);
+       gtk_list_box_set_sort_func (GTK_LIST_BOX (priv->list_box_addons),
+                                   list_sort_func,
+                                   shell_details, NULL);
 }
 
 /**
diff --git a/src/gs-shell-details.ui b/src/gs-shell-details.ui
index c7a070b..7cb3f99 100644
--- a/src/gs-shell-details.ui
+++ b/src/gs-shell-details.ui
@@ -602,7 +602,7 @@
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
                         <property name="margin_top">28</property>
-                        <property name="margin_bottom">24</property>
+                        <property name="margin_bottom">12</property>
                         <property name="halign">start</property>
                         <property name="valign">start</property>
                         <property name="hexpand">True</property>
@@ -888,6 +888,69 @@
                         <property name="position">10</property>
                       </packing>
                     </child>
+
+                    <child>
+                      <object class="GtkBox" id="box_addons">
+                        <property name="visible">True</property>
+                        <property name="orientation">vertical</property>
+                        <property name="margin_top">28</property>
+
+                        <child>
+                          <object class="GtkBox" id="box_addons_title">
+                            <property name="visible">True</property>
+                            <property name="orientation">vertical</property>
+                            <property name="margin_bottom">12</property>
+                            <child>
+                              <object class="GtkLabel" id="label_addons_title">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="halign">start</property>
+                                <property name="valign">start</property>
+                                <property name="hexpand">True</property>
+                                <property name="xalign">0</property>
+                                <property name="label" translatable="yes">Addons</property>
+                                <style>
+                                  <class name="application-details-title"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="label_addons_uninstalled_app">
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="xalign">0</property>
+                                <property name="label" translatable="yes">Selected addons will be installed 
with the application.</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+
+                        <child>
+                          <object class="GtkFrame" id="box_addons_frame">
+                            <property name="visible">True</property>
+                            <property name="shadow_type">in</property>
+                            <property name="halign">fill</property>
+                            <property name="valign">start</property>
+                            <style>
+                              <class name="view"/>
+                            </style>
+                            <child>
+                              <object class="GtkListBox" id="list_box_addons">
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="selection_mode">none</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">11</property>
+                      </packing>
+                    </child>
+
                   </object>
                 </child>
               </object>
diff --git a/src/plugins/gs-plugin-appstream.c b/src/plugins/gs-plugin-appstream.c
index 4d1261f..0802714 100644
--- a/src/plugins/gs-plugin-appstream.c
+++ b/src/plugins/gs-plugin-appstream.c
@@ -35,6 +35,8 @@ struct GsPluginPrivate {
        gsize                    done_init;
 };
 
+static gboolean gs_plugin_refine_item (GsPlugin *plugin, GsApp *app, AsApp *item, GError **error);
+
 /**
  * gs_plugin_get_name:
  */
@@ -276,6 +278,38 @@ out:
 }
 
 /**
+ * gs_plugin_refine_add_addons:
+ */
+static void
+gs_plugin_refine_add_addons (GsPlugin *plugin, GsApp *app, AsApp *item)
+{
+       GPtrArray *addons;
+       guint i;
+
+       addons = as_app_get_addons (item);
+       if (addons == NULL)
+               return;
+
+       for (i = 0; i < addons->len; i++) {
+               AsApp *as_addon = g_ptr_array_index (addons, i);
+               GsApp *addon;
+               GError *error = NULL;
+               gboolean ret;
+
+               addon = gs_app_new (as_app_get_id_full (as_addon));
+
+               /* add all the data we can */
+               ret = gs_plugin_refine_item (plugin, addon, as_addon, &error);
+               if (!ret) {
+                       g_warning ("failed to refine addon: %s", error->message);
+                       g_error_free (error);
+                       continue;
+               }
+
+               gs_app_add_addon (app, addon);
+       }
+}
+/**
  * gs_plugin_refine_add_screenshots:
  */
 static void
@@ -516,6 +550,9 @@ gs_plugin_refine_item (GsPlugin *plugin,
                case AS_ID_KIND_SOURCE:
                        /* handled above */
                        break;
+               case AS_ID_KIND_ADDON:
+                       gs_app_set_id_kind (app, GS_APP_ID_KIND_ADDON);
+                       break;
                default:
                        g_warning ("Unhandled AsIdKind '%s'", as_id_kind_to_string (as_app_get_id_kind 
(item)));
                        break;
@@ -527,6 +564,9 @@ gs_plugin_refine_item (GsPlugin *plugin,
        if (pkgnames->len > 0 && gs_app_get_sources(app)->len == 0)
                gs_app_set_sources (app, pkgnames);
 
+       /* set addons */
+       gs_plugin_refine_add_addons (plugin, app, item);
+
        /* set screenshots */
        gs_plugin_refine_add_screenshots (app, item);
 
diff --git a/src/plugins/gs-plugin-packagekit.c b/src/plugins/gs-plugin-packagekit.c
index 6d2d20e..cf29fe9 100644
--- a/src/plugins/gs-plugin-packagekit.c
+++ b/src/plugins/gs-plugin-packagekit.c
@@ -283,15 +283,15 @@ gs_plugin_app_install (GsPlugin *plugin,
                       GCancellable *cancellable,
                       GError **error)
 {
-       GPtrArray *array = NULL;
+       GPtrArray *addons;
+       GPtrArray *array_package_ids = NULL;
        GPtrArray *source_ids;
        PkError *error_code = NULL;
        PkResults *results = NULL;
        const gchar *package_id;
        gboolean ret = TRUE;
        gchar **package_ids = NULL;
-       guint i;
-       guint cnt = 0;
+       guint i, j;
 
        /* only process this app if was created by this plugin */
        if (g_strcmp0 (gs_app_get_management_plugin (app), "PackageKit") != 0)
@@ -310,14 +310,32 @@ gs_plugin_app_install (GsPlugin *plugin,
                                             "installing not available");
                        goto out;
                }
-               package_ids = g_new0 (gchar *, source_ids->len + 1);
+               array_package_ids = g_ptr_array_new_with_free_func (g_free);
                for (i = 0; i < source_ids->len; i++) {
                        package_id = g_ptr_array_index (source_ids, i);
                        if (g_strstr_len (package_id, -1, ";installed") != NULL)
                                continue;
-                       package_ids[cnt++] = g_strdup (package_id);
+                       g_ptr_array_add (array_package_ids, g_strdup (package_id));
                }
-               if (cnt == 0) {
+
+               addons = gs_app_get_addons (app);
+               for (i = 0; i < addons->len; i++) {
+                       GsApp *addon = g_ptr_array_index (addons, i);
+
+                       if (!gs_app_get_to_be_installed (addon))
+                               continue;
+
+                       source_ids = gs_app_get_source_ids (addon);
+                       for (j = 0; j < source_ids->len; j++) {
+                               package_id = g_ptr_array_index (source_ids, j);
+                               if (g_strstr_len (package_id, -1, ";installed") != NULL)
+                                       continue;
+                               g_ptr_array_add (array_package_ids, g_strdup (package_id));
+                       }
+               }
+               g_ptr_array_add (array_package_ids, NULL);
+
+               if (array_package_ids->len == 0) {
                        ret = FALSE;
                        g_set_error_literal (error,
                                             GS_PLUGIN_ERROR,
@@ -326,8 +344,14 @@ gs_plugin_app_install (GsPlugin *plugin,
                        goto out;
                }
                gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+               addons = gs_app_get_addons (app);
+               for (i = 0; i < addons->len; i++) {
+                       GsApp *addon = g_ptr_array_index (addons, i);
+                       if (gs_app_get_to_be_installed (addon))
+                               gs_app_set_state (addon, GS_APP_STATE_INSTALLING);
+               }
                results = pk_task_install_packages_sync (plugin->priv->task,
-                                                        package_ids,
+                                                        (gchar **) array_package_ids->pdata,
                                                         cancellable,
                                                         gs_plugin_packagekit_progress_cb, plugin,
                                                         error);
@@ -384,8 +408,8 @@ out:
        g_strfreev (package_ids);
        if (error_code != NULL)
                g_object_unref (error_code);
-       if (array != NULL)
-               g_ptr_array_unref (array);
+       if (array_package_ids != NULL)
+               g_ptr_array_unref (array_package_ids);
        if (results != NULL)
                g_object_unref (results);
        return ret;


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]