[gnome-software] Add a banner designer utility



commit 603ba3923b74399b5b90c2db0f33512f80f55863
Author: Richard Hughes <richard hughsie com>
Date:   Wed May 3 19:15:05 2017 +0100

    Add a banner designer utility
    
    This allows designers to use just CSS and export an AppStream file.

 contrib/gnome-software.spec.in           |    8 +-
 meson.build                              |    2 +-
 po/POTFILES.in                           |    2 +
 src/gnome-software-editor.gresource.xml  |   12 +
 src/gnome-software-editor.xml            |   66 ++
 src/gs-editor.c                          | 1232 ++++++++++++++++++++++++++++++
 src/gs-editor.ui                         |  697 +++++++++++++++++
 src/gs-feature-tile.c                    |    6 +-
 src/gs-upgrade-banner.c                  |    6 +-
 src/gs-upgrade-banner.ui                 |    4 +-
 src/meson.build                          |   61 ++-
 src/org.gnome.Software.Editor.desktop.in |   17 +
 12 files changed, 2101 insertions(+), 12 deletions(-)
---
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index 0b7600e..49467f7 100644
--- a/contrib/gnome-software.spec.in
+++ b/contrib/gnome-software.spec.in
@@ -142,7 +142,8 @@ glib-compile-schemas %{_datadir}/glib-2.0/schemas &> /dev/null || :
 %doc AUTHORS README
 %license COPYING
 %{_bindir}/gnome-software
-%{_datadir}/applications/*.desktop
+%{_datadir}/applications/gnome-software-local-file.desktop
+%{_datadir}/applications/org.gnome.Software.desktop
 %dir %{_datadir}/gnome-software
 %{_datadir}/gnome-software/*.png
 %{_datadir}/appdata/*.appdata.xml
@@ -207,6 +208,11 @@ glib-compile-schemas %{_datadir}/glib-2.0/schemas &> /dev/null || :
 %{_libexecdir}/gnome-software-cmd
 %{_libexecdir}/gnome-software-restarter
 
+# optional editor
+%{_datadir}/applications/org.gnome.Software.Editor.desktop
+%{_bindir}/gnome-software-editor
+%{_mandir}/man1/gnome-software-editor.1.gz
+
 %files devel
 %{_libdir}/pkgconfig/gnome-software.pc
 %dir %{_includedir}/gnome-software
diff --git a/meson.build b/meson.build
index 4e27615..132b659 100644
--- a/meson.build
+++ b/meson.build
@@ -89,7 +89,7 @@ add_global_link_arguments(
   language: 'c'
 )
 
-appstream_glib = dependency('appstream-glib', version : '>= 0.6.5')
+appstream_glib = dependency('appstream-glib', version : '>= 0.6.13')
 gdk_pixbuf = dependency('gdk-pixbuf-2.0', version : '>= 2.31.5')
 gio_unix = dependency('gio-unix-2.0')
 gmodule = dependency('gmodule-2.0')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a18019f..a0726e1 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -20,6 +20,8 @@ src/gs-content-rating.c
 src/gs-dbus-helper.c
 src/gs-details-page.c
 src/gs-details-page.ui
+src/gs-editor.c
+src/gs-editor.ui
 src/gs-extras-page.c
 src/gs-extras-page.ui
 src/gs-feature-tile.c
diff --git a/src/gnome-software-editor.gresource.xml b/src/gnome-software-editor.gresource.xml
new file mode 100644
index 0000000..508aa9f
--- /dev/null
+++ b/src/gnome-software-editor.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/org/gnome/Software/Editor">
+  <file preprocess="xml-stripblanks">gs-editor.ui</file>
+ </gresource>
+ <gresource prefix="/org/gnome/Software">
+  <file preprocess="xml-stripblanks">gs-feature-tile.ui</file>
+  <file preprocess="xml-stripblanks">gs-summary-tile.ui</file>
+  <file preprocess="xml-stripblanks">gs-upgrade-banner.ui</file>
+  <file preprocess="xml-stripblanks">gs-star-widget.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/gnome-software-editor.xml b/src/gnome-software-editor.xml
new file mode 100644
index 0000000..ad7ddc4
--- /dev/null
+++ b/src/gnome-software-editor.xml
@@ -0,0 +1,66 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+        "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd";>
+
+<refentry id="gnome-software">
+
+  <refentryinfo>
+    <title>gnome-software-editor</title>
+    <productname>GNOME</productname>
+    <author>
+      <contrib>Maintainer</contrib>
+      <firstname>Richard</firstname>
+      <surname>Hughes</surname>
+      <email>richard hughsie com</email>
+    </author>
+    <copyright>
+      <year>2017</year>
+      <holder>Richard Hughes</holder>
+    </copyright>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>gnome-software-editor</refentrytitle>
+    <manvolnum>1</manvolnum>
+    <refmiscinfo class="manual">User Commands</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname>gnome-software-editor</refname>
+    <refpurpose>Design the featured banners for GNOME Software</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>gnome-software-editor</command>
+      <arg choice="opt" rep="repeat">OPTION</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+    <para>
+      This manual page documents briefly the <command>gnome-software-editor</command> command.
+    </para>
+    <para>
+      <command>gnome-software-editor</command> allows you to design featured
+      banners for the GNOME Software overview page.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+    <variablelist>
+      <varlistentry>
+        <term><option>-?</option>, <option>--help</option></term>
+        <listitem><para>Prints a short help text and exits.</para></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Author</title>
+    <para>This manual page was written by Richard Hughes <email>richard hughsie com</email>.
+    </para>
+  </refsect1>
+</refentry>
diff --git a/src/gs-editor.c b/src/gs-editor.c
new file mode 100644
index 0000000..e6dc3a2
--- /dev/null
+++ b/src/gs-editor.c
@@ -0,0 +1,1232 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2017 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.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <locale.h>
+
+#include "gs-common.h"
+#include "gs-feature-tile.h"
+#include "gs-summary-tile.h"
+#include "gs-upgrade-banner.h"
+
+typedef struct {
+       GCancellable            *cancellable;
+       GtkApplication          *application;
+       GtkBuilder              *builder;
+       GtkWidget               *featured_tile1;
+       GtkWidget               *upgrade_banner;
+       AsStore                 *store;
+       AsStore                 *store_global;
+       AsApp                   *selected_item;
+       AsApp                   *deleted_item;
+       gboolean                 is_in_refresh;
+       gboolean                 pending_changes;
+       guint                    refresh_details_delayed_id;
+} GsEditor;
+
+static gchar *
+gs_editor_css_download_resources (GsEditor *self, const gchar *css, GError **error)
+{
+       g_autoptr(GsPlugin) plugin = NULL;
+       g_autoptr(GString) css2 = NULL;
+       g_autoptr(SoupSession) soup_session = NULL;
+
+       /* replace keywords */
+       css2 = g_string_new (css);
+       as_utils_string_replace (css2, "@datadir@", DATADIR);
+
+       /* make remote URIs local */
+       plugin = gs_plugin_new ();
+       gs_plugin_set_name (plugin, "editor");
+       soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
+                                                     SOUP_SESSION_TIMEOUT, 10,
+                                                     NULL);
+       gs_plugin_set_soup_session (plugin, soup_session);
+       return gs_plugin_download_rewrite_resource (plugin, css2->str, NULL, error);
+}
+
+typedef struct {
+       GsEditor        *self;
+       GError          **error;
+} GsDesignErrorHelper;
+
+static void
+gs_design_css_parsing_error_cb (GtkCssProvider *provider,
+                               GtkCssSection *section,
+                               GError *error,
+                               gpointer user_data)
+{
+       GsDesignErrorHelper *helper = (GsDesignErrorHelper *) user_data;
+       if (*(helper->error) != NULL) {
+               g_warning ("ignoring parse error %u:%u: %s",
+                          gtk_css_section_get_start_line (section),
+                          gtk_css_section_get_start_position (section),
+                          error->message);
+               return;
+       }
+       *(helper->error) = g_error_copy (error);
+}
+
+static gboolean
+gs_design_validate_css (GsEditor *self, const gchar *css, GError **error)
+{
+       GsDesignErrorHelper helper;
+       g_autofree gchar *css_new = NULL;
+       g_autoptr(GString) str = NULL;
+       g_autoptr(GtkCssProvider) provider = NULL;
+
+       /* nothing set */
+       if (css == NULL)
+               return TRUE;
+
+       /* remove custom class if NULL */
+       str = g_string_new (NULL);
+       g_string_append (str, ".themed-widget {");
+       css_new = gs_editor_css_download_resources (self, css, error);
+       if (css_new == NULL)
+               return FALSE;
+       g_string_append (str, css_new);
+       g_string_append (str, "}");
+
+       /* set up custom provider */
+       helper.self = self;
+       helper.error = error;
+       provider = gtk_css_provider_new ();
+       g_signal_connect (provider, "parsing-error",
+                         G_CALLBACK (gs_design_css_parsing_error_cb), &helper);
+       gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                                  GTK_STYLE_PROVIDER (provider),
+                                                  GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+       gtk_css_provider_load_from_data (provider, str->str, -1, NULL);
+       return *(helper.error) == NULL;
+}
+
+static void
+gs_editor_refine_app_pixbuf (GsApp *app)
+{
+       GPtrArray *icons;
+       if (gs_app_get_pixbuf (app) != NULL)
+               return;
+       icons = gs_app_get_icons (app);
+       for (guint i = 0; i < icons->len; i++) {
+               AsIcon *ic = g_ptr_array_index (icons, i);
+               g_autoptr(GError) error = NULL;
+               if (as_icon_get_kind (ic) == AS_ICON_KIND_STOCK) {
+
+                       g_autoptr(GdkPixbuf) pb = NULL;
+                       pb = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+                                                      as_icon_get_name (ic),
+                                                      64,
+                                                      GTK_ICON_LOOKUP_FORCE_SIZE,
+                                                      &error);
+                       if (pb == NULL) {
+                               g_warning ("failed to load icon: %s", error->message);
+                               continue;
+                       }
+                       gs_app_set_pixbuf (app, pb);
+               } else {
+                       if (!as_icon_load (ic, AS_ICON_LOAD_FLAG_SEARCH_SIZE, &error)) {
+                               g_warning ("failed to load icon: %s", error->message);
+                               continue;
+                       }
+                       gs_app_set_pixbuf (app, as_icon_get_pixbuf (ic));
+               }
+               break;
+       }
+}
+
+static GsApp *
+gs_editor_convert_app (GsEditor *self, AsApp *item)
+{
+       AsApp *item_global;
+       AsAppState item_state;
+       GsApp *app;
+       const gchar *keys[] = {
+               "GnomeSoftware::AppTile-css",
+               "GnomeSoftware::FeatureTile-css",
+               "GnomeSoftware::UpgradeBanner-css",
+               NULL };
+
+       /* copy name, summary and description */
+       app = gs_app_new (as_app_get_id (item));
+       item_global = as_store_get_app_by_id (self->store_global, as_app_get_id (item));
+       if (item_global == NULL) {
+               const gchar *tmp;
+               g_autoptr(AsIcon) ic = NULL;
+               g_debug ("no app found for %s, using fallback", as_app_get_id (item));
+
+               /* copy from AsApp, falling back to something sane */
+               tmp = as_app_get_name (item, NULL);
+               if (tmp == NULL)
+                       tmp = "Application";
+               gs_app_set_name (app, GS_APP_QUALITY_NORMAL, tmp);
+               tmp = as_app_get_comment (item, NULL);
+               if (tmp == NULL)
+                       tmp = "Description";
+               gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, tmp);
+               tmp = as_app_get_description (item, NULL);
+               if (tmp == NULL)
+                       tmp = "A multiline description";
+               gs_app_set_description (app, GS_APP_QUALITY_NORMAL, tmp);
+               ic = as_icon_new ();
+               as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
+               as_icon_set_name (ic, "application-x-executable");
+               gs_app_add_icon (app, ic);
+               item_state = as_app_get_state (item);
+       } else {
+               GPtrArray *icons;
+               g_debug ("found global app for %s", as_app_get_id (item));
+               gs_app_set_name (app, GS_APP_QUALITY_NORMAL,
+                                as_app_get_name (item_global, NULL));
+               gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+                                   as_app_get_comment (item_global, NULL));
+               gs_app_set_description (app, GS_APP_QUALITY_NORMAL,
+                                       as_app_get_description (item_global, NULL));
+               icons = as_app_get_icons (item_global);
+               for (guint i = 0; i < icons->len; i++) {
+                       AsIcon *icon = g_ptr_array_index (icons, i);
+                       gs_app_add_icon (app, icon);
+               }
+               item_state = as_app_get_state (item_global);
+       }
+
+       /* copy state */
+       if (item_state == AS_APP_STATE_UNKNOWN)
+               item_state = AS_APP_STATE_AVAILABLE;
+       gs_app_set_state (app, item_state);
+
+       /* copy version */
+       gs_app_set_version (app, "3.28");
+
+       /* load pixbuf */
+       gs_editor_refine_app_pixbuf (app);
+
+       /* copy metadata */
+       for (guint i = 0; keys[i] != NULL; i++) {
+               g_autoptr(GError) error = NULL;
+               const gchar *css = as_app_get_metadata_item (item, keys[i]);
+               if (css != NULL) {
+                       g_autofree gchar *css_new = NULL;
+                       css_new = gs_editor_css_download_resources (self, css, &error);
+                       if (css_new == NULL) {
+                               g_warning ("%s", error->message);
+                               gs_app_set_metadata (app, keys[i], css);
+                       } else {
+                               gs_app_set_metadata (app, keys[i], css_new);
+                       }
+               } else {
+                       gs_app_set_metadata (app, keys[i], NULL);
+               }
+       }
+       return app;
+}
+
+static void
+gs_editor_refresh_details (GsEditor *self)
+{
+       AsAppKind app_kind = AS_APP_KIND_UNKNOWN;
+       GtkWidget *widget;
+       const gchar *css = NULL;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GsApp) app = NULL;
+
+       /* ignore changed events */
+       self->is_in_refresh = TRUE;
+
+       /* create a GsApp for the AsApp */
+       if (self->selected_item != NULL) {
+               app = gs_editor_convert_app (self, self->selected_item);
+               g_debug ("refreshing details for %s", gs_app_get_id (app));
+       }
+
+       /* get kind */
+       if (self->selected_item != NULL)
+               app_kind = as_app_get_kind (self->selected_item);
+
+       /* feature tiles */
+       if (app_kind != AS_APP_KIND_OS_UPGRADE) {
+               if (self->selected_item != NULL) {
+                       gs_app_tile_set_app (GS_APP_TILE (self->featured_tile1), app);
+                       gtk_widget_set_sensitive (self->featured_tile1, TRUE);
+               } else {
+                       gtk_widget_set_sensitive (self->featured_tile1, FALSE);
+               }
+               gtk_widget_set_visible (self->featured_tile1, TRUE);
+       } else {
+               gtk_widget_set_visible (self->featured_tile1, FALSE);
+       }
+
+       /* upgrade banner */
+       if (app_kind == AS_APP_KIND_OS_UPGRADE) {
+               if (self->selected_item != NULL) {
+                       gs_upgrade_banner_set_app (GS_UPGRADE_BANNER (self->upgrade_banner), app);
+                       gtk_widget_set_sensitive (self->upgrade_banner, TRUE);
+               } else {
+                       gtk_widget_set_sensitive (self->upgrade_banner, FALSE);
+               }
+               gtk_widget_set_visible (self->upgrade_banner, TRUE);
+       } else {
+               gtk_widget_set_visible (self->upgrade_banner, FALSE);
+       }
+
+       /* name */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "box_name"));
+       if (self->selected_item != NULL) {
+               const gchar *tmp;
+               gtk_widget_set_visible (widget, app_kind == AS_APP_KIND_OS_UPGRADE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_name"));
+               tmp = as_app_get_name (self->selected_item, NULL);
+               if (tmp != NULL)
+                       gtk_entry_set_text (GTK_ENTRY (widget), tmp);
+       } else {
+               gtk_widget_set_visible (widget, FALSE);
+       }
+
+       /* summary */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "box_summary"));
+       if (self->selected_item != NULL) {
+               const gchar *tmp;
+               gtk_widget_set_visible (widget, app_kind == AS_APP_KIND_OS_UPGRADE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_summary"));
+               tmp = as_app_get_comment (self->selected_item, NULL);
+               if (tmp != NULL)
+                       gtk_entry_set_text (GTK_ENTRY (widget), tmp);
+       } else {
+               gtk_widget_set_visible (widget, FALSE);
+       }
+
+       /* kudos */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "box_kudos"));
+       if (self->selected_item != NULL) {
+               gtk_widget_set_visible (widget, app_kind != AS_APP_KIND_OS_UPGRADE);
+       } else {
+               gtk_widget_set_visible (widget, TRUE);
+       }
+
+       /* category featured */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "checkbutton_category_featured"));
+       if (self->selected_item != NULL) {
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget),
+                                             as_app_has_category (self->selected_item,
+                                                                  "Featured"));
+               gtk_widget_set_sensitive (widget, TRUE);
+       } else {
+               gtk_widget_set_sensitive (widget, FALSE);
+       }
+
+       /* kudo popular */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "checkbutton_editors_pick"));
+       if (self->selected_item != NULL) {
+               gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (widget),
+                                             as_app_has_kudo (self->selected_item,
+                                                              "GnomeSoftware::popular"));
+               gtk_widget_set_sensitive (widget, TRUE);
+       } else {
+               gtk_widget_set_sensitive (widget, FALSE);
+       }
+
+       /* featured */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "textview_css"));
+       if (self->selected_item != NULL) {
+               GtkTextBuffer *buffer;
+               GtkTextIter iter_end;
+               GtkTextIter iter_start;
+               g_autofree gchar *css_existing = NULL;
+
+               css = as_app_get_metadata_item (self->selected_item,
+                                               "GnomeSoftware::FeatureTile-css");
+               if (css == NULL)
+                       css = "";
+               buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+               gtk_text_buffer_get_bounds (buffer, &iter_start, &iter_end);
+               css_existing = gtk_text_buffer_get_text (buffer, &iter_start, &iter_end, FALSE);
+               if (g_strcmp0 (css_existing, css) != 0)
+                       gtk_text_buffer_set_text (buffer, css, -1);
+               gtk_widget_set_sensitive (widget, TRUE);
+       } else {
+               gtk_widget_set_sensitive (widget, FALSE);
+       }
+
+       /* desktop ID */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_desktop_id"));
+       if (self->selected_item != NULL) {
+               const gchar *id = as_app_get_id (self->selected_item);
+               if (id == NULL)
+                       id = "";
+               gtk_entry_set_text (GTK_ENTRY (widget), id);
+               gtk_widget_set_sensitive (widget, TRUE);
+       } else {
+               gtk_entry_set_text (GTK_ENTRY (widget), "");
+               gtk_widget_set_sensitive (widget, FALSE);
+       }
+
+       /* validate CSS */
+       if (css == NULL) {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "label_infobar_css"));
+               gtk_label_set_label (GTK_LABEL (widget), "");
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "infobar_css"));
+               gtk_info_bar_set_message_type (GTK_INFO_BAR (widget), GTK_MESSAGE_OTHER);
+       } else if (!gs_design_validate_css (self, css, &error)) {
+               g_autofree gchar *msg = g_strdup (error->message);
+               g_strdelimit (msg, "\n\r<>", '\0');
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "label_infobar_css"));
+               gtk_label_set_label (GTK_LABEL (widget), msg);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "infobar_css"));
+               gtk_info_bar_set_message_type (GTK_INFO_BAR (widget), GTK_MESSAGE_WARNING);
+       } else {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "label_infobar_css"));
+               gtk_label_set_label (GTK_LABEL (widget), _("CSS validated OK!"));
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "infobar_css"));
+               gtk_info_bar_set_message_type (GTK_INFO_BAR (widget), GTK_MESSAGE_OTHER);
+       }
+
+       /* do not ignore changed events */
+       self->is_in_refresh = FALSE;
+}
+
+static gboolean
+gs_design_dialog_refresh_details_delayed_cb (gpointer user_data)
+{
+       GsEditor *self = (GsEditor *) user_data;
+       gs_editor_refresh_details (self);
+       self->refresh_details_delayed_id = 0;
+       return FALSE;
+}
+
+static void
+gs_design_dialog_refresh_details_delayed (GsEditor *self)
+{
+       if (self->refresh_details_delayed_id != 0)
+               g_source_remove (self->refresh_details_delayed_id);
+       self->refresh_details_delayed_id = g_timeout_add (500,
+               gs_design_dialog_refresh_details_delayed_cb, self);
+}
+
+static void
+gs_design_dialog_buffer_changed_cb (GtkTextBuffer *buffer, GsEditor *self)
+{
+       GtkTextIter iter_end;
+       GtkTextIter iter_start;
+       g_autofree gchar *css = NULL;
+
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+
+       gtk_text_buffer_get_bounds (buffer, &iter_start, &iter_end);
+       css = gtk_text_buffer_get_text (buffer, &iter_start, &iter_end, FALSE);
+       g_debug ("CSS now '%s'", css);
+       as_app_add_metadata (self->selected_item, "GnomeSoftware::FeatureTile-css", NULL);
+       as_app_add_metadata (self->selected_item, "GnomeSoftware::FeatureTile-css", css);
+       self->pending_changes = TRUE;
+       gs_design_dialog_refresh_details_delayed (self);
+}
+
+static void
+gs_editor_set_page (GsEditor *self, const gchar *name)
+{
+       GtkWidget *widget;
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "stack_main"));
+       gtk_stack_set_visible_child_name (GTK_STACK (widget), name);
+
+       if (g_strcmp0 (name, "none") == 0) {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_back"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_import"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_save"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_search"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_remove"));
+               gtk_widget_set_visible (widget, FALSE);
+
+       } else if (g_strcmp0 (name, "choice") == 0) {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_back"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_import"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_save"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_search"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_remove"));
+               gtk_widget_set_visible (widget, FALSE);
+
+       } else if (g_strcmp0 (name, "details") == 0) {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_back"));
+               gtk_widget_set_visible (widget, TRUE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_import"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_save"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_search"));
+               gtk_widget_set_visible (widget, FALSE);
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_remove"));
+               gtk_widget_set_visible (widget, TRUE);
+       }
+}
+
+static void
+gs_editor_app_tile_clicked_cb (GsAppTile *tile, GsEditor *self)
+{
+       GsApp *app = gs_app_tile_get_app (tile);
+       AsApp *item = as_store_get_app_by_id (self->store, gs_app_get_id (app));
+       if (item == NULL) {
+               g_warning ("failed to find %s", gs_app_get_id (app));
+               return;
+       }
+       g_set_object (&self->selected_item, item);
+
+       gs_editor_refresh_details (self);
+       gs_editor_set_page (self, "details");
+}
+
+static void
+gs_editor_refresh_choice (GsEditor *self)
+{
+       GPtrArray *apps;
+       GtkContainer *container;
+
+       /* add all apps */
+       container = GTK_CONTAINER (gtk_builder_get_object (self->builder,
+                                                          "flowbox_main"));
+       gs_container_remove_all (GTK_CONTAINER (container));
+       apps = as_store_get_apps (self->store);
+       for (guint i = 0; i < apps->len; i++) {
+               AsApp *item = g_ptr_array_index (apps, i);
+               GtkWidget *tile = NULL;
+               g_autoptr(GsApp) app = NULL;
+
+               app = gs_editor_convert_app (self, item);
+               tile = gs_summary_tile_new (app);
+               g_signal_connect (tile, "clicked",
+                                 G_CALLBACK (gs_editor_app_tile_clicked_cb),
+                                 self);
+               gtk_widget_set_visible (tile, TRUE);
+               gtk_widget_set_vexpand (tile, FALSE);
+               gtk_widget_set_hexpand (tile, FALSE);
+               gtk_widget_set_size_request (tile, 300, 50);
+               gtk_widget_set_valign (tile, GTK_ALIGN_START);
+               gtk_container_add (GTK_CONTAINER (container), tile);
+       }
+}
+
+static void
+gs_editor_error_message (GsEditor *self, const gchar *title, const gchar *message)
+{
+       GtkWidget *dialog;
+       GtkWindow *window;
+       window = GTK_WINDOW (gtk_builder_get_object (self->builder, "window_main"));
+       dialog = gtk_message_dialog_new (window,
+                                        GTK_DIALOG_MODAL |
+                                        GTK_DIALOG_DESTROY_WITH_PARENT,
+                                        GTK_MESSAGE_WARNING,
+                                        GTK_BUTTONS_OK,
+                                        "%s", title);
+       gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                                 "%s", message);
+       gtk_dialog_run (GTK_DIALOG (dialog));
+       gtk_widget_destroy (dialog);
+}
+
+static void
+gs_editor_button_back_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       gs_editor_set_page (self, as_store_get_size (self->store) == 0 ? "none" : "choice");
+}
+
+static void
+gs_editor_button_menu_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "popover_menu"));
+       gtk_popover_popup (GTK_POPOVER (widget));
+}
+
+static void
+gs_editor_refresh_file (GsEditor *self, GFile *file)
+{
+       GtkWidget *widget;
+
+       /* set subtitle */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "headerbar_main"));
+       if (file != NULL) {
+               g_autofree gchar *basename = g_file_get_basename (file);
+               gtk_header_bar_set_subtitle (GTK_HEADER_BAR (widget), basename);
+       } else {
+               gtk_header_bar_set_subtitle (GTK_HEADER_BAR (widget), NULL);
+       }
+}
+
+static void
+gs_editor_button_import_file (GsEditor *self, GFile *file)
+{
+       g_autoptr(GError) error = NULL;
+
+       /* load new file */
+       if (!as_store_from_file (self->store, file, NULL, NULL, &error)) {
+               /* TRANSLATORS: error dialog title */
+               gs_editor_error_message (self, _("Failed to load file"), error->message);
+               return;
+       }
+
+       /* update listview */
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_file (self, file);
+
+       /* set the appropriate page */
+       gs_editor_set_page (self, as_store_get_size (self->store) == 0 ? "none" : "choice");
+
+       /* reset */
+       self->pending_changes = FALSE;
+}
+
+static gchar *
+gs_editor_get_default_save_location (void)
+{
+       gchar *xmlfn = g_build_filename (g_get_user_data_dir (),
+                                        "app-info",
+                                        "xmls",
+                                        NULL);
+       g_mkdir_with_parents (xmlfn, 0777);
+       return xmlfn;
+}
+
+static void
+gs_editor_button_import_clicked_cb (GtkApplication *application, GsEditor *self)
+{
+       GtkFileFilter *filter;
+       GtkWindow *window;
+       GtkWidget *dialog;
+       gint res;
+       g_autoptr(GFile) file = NULL;
+       g_autofree gchar *appinfo_xml = NULL;
+
+       /* import warning */
+       window = GTK_WINDOW (gtk_builder_get_object (self->builder,
+                                                    "window_main"));
+       if (as_store_get_size (self->store) > 0) {
+               dialog = gtk_message_dialog_new (window,
+                                                GTK_DIALOG_MODAL |
+                                                GTK_DIALOG_DESTROY_WITH_PARENT,
+                                                GTK_MESSAGE_WARNING,
+                                                GTK_BUTTONS_CANCEL,
+                                                /* TRANSLATORS: window title */
+                                                _("Unsaved changes"));
+               gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                                         _("The application list is already loaded."));
+
+               gtk_dialog_add_button (GTK_DIALOG (dialog),
+                                      /* TRANSLATORS: button text */
+                                      _("Merge documents"),
+                                      GTK_RESPONSE_ACCEPT);
+               gtk_dialog_add_button (GTK_DIALOG (dialog),
+                                      /* TRANSLATORS: button text */
+                                      _("Throw away changes"),
+                                      GTK_RESPONSE_YES);
+               res = gtk_dialog_run (GTK_DIALOG (dialog));
+               gtk_widget_destroy (dialog);
+
+               if (res == GTK_RESPONSE_CANCEL)
+                       return;
+               if (res == GTK_RESPONSE_YES)
+                       as_store_remove_all (self->store);
+       }
+
+       /* import the new file */
+       dialog = gtk_file_chooser_dialog_new (_("Open AppStream File"),
+                                             window,
+                                             GTK_FILE_CHOOSER_ACTION_OPEN,
+                                             _("_Cancel"), GTK_RESPONSE_CANCEL,
+                                             _("_Open"), GTK_RESPONSE_ACCEPT,
+                                             NULL);
+       filter = gtk_file_filter_new ();
+       gtk_file_filter_add_pattern (filter, "*.xml");
+       gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter);
+       appinfo_xml = gs_editor_get_default_save_location ();
+       gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), appinfo_xml);
+       res = gtk_dialog_run (GTK_DIALOG (dialog));
+       if (res != GTK_RESPONSE_ACCEPT) {
+               gtk_widget_destroy (dialog);
+               return;
+       }
+       file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+       gs_editor_button_import_file (self, file);
+       gtk_widget_destroy (dialog);
+}
+
+static void
+gs_editor_button_save_clicked_cb (GtkApplication *application, GsEditor *self)
+{
+       GtkFileFilter *filter;
+       GtkWidget *dialog;
+       GtkWindow *window;
+       gint res;
+       g_autofree gchar *appinfo_xml = NULL;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GFile) file = NULL;
+
+       /* export a new file */
+       window = GTK_WINDOW (gtk_builder_get_object (self->builder,
+                                                    "window_main"));
+       dialog = gtk_file_chooser_dialog_new (_("Open AppStream File"),
+                                             window,
+                                             GTK_FILE_CHOOSER_ACTION_SAVE,
+                                             _("_Cancel"), GTK_RESPONSE_CANCEL,
+                                             _("_Save"), GTK_RESPONSE_ACCEPT,
+                                             NULL);
+       filter = gtk_file_filter_new ();
+       gtk_file_filter_add_pattern (filter, "*.xml");
+       gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter);
+       appinfo_xml = gs_editor_get_default_save_location ();
+       gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog), appinfo_xml);
+       res = gtk_dialog_run (GTK_DIALOG (dialog));
+       if (res != GTK_RESPONSE_ACCEPT) {
+               gtk_widget_destroy (dialog);
+               return;
+       }
+       file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+       gtk_widget_destroy (dialog);
+       if (!as_store_to_file (self->store,
+                              file,
+                              AS_NODE_TO_XML_FLAG_ADD_HEADER |
+                              AS_NODE_TO_XML_FLAG_FORMAT_MULTILINE |
+                              AS_NODE_TO_XML_FLAG_FORMAT_INDENT,
+                              self->cancellable,
+                              &error)) {
+               /* TRANSLATORS: error dialog title */
+               gs_editor_error_message (self, _("Failed to save file"), error->message);
+               return;
+       }
+       self->pending_changes = FALSE;
+       gs_editor_refresh_file (self, file);
+       gs_editor_refresh_details (self);
+}
+
+static void
+gs_editor_show_notification (GsEditor *self, const gchar *text)
+{
+       GtkWidget *widget;
+
+       /* set text */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "label_notification"));
+       gtk_label_set_markup (GTK_LABEL (widget), text);
+
+       /* show button: FIXME, use flags? */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_notification_undo_remove"));
+       gtk_widget_set_visible (widget, TRUE);
+
+       /* show revealer */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "revealer_notification"));
+       gtk_revealer_set_reveal_child (GTK_REVEALER (widget), TRUE);
+}
+
+static void
+gs_editor_button_notification_dismiss_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "revealer_notification"));
+       gtk_revealer_set_reveal_child (GTK_REVEALER (widget), FALSE);
+}
+
+static void
+gs_editor_button_undo_remove_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       if (self->deleted_item == NULL)
+               return;
+
+       /* add this back to the store and set it as current */
+       as_store_add_app (self->store, self->deleted_item);
+       g_set_object (&self->selected_item, self->deleted_item);
+       g_clear_object (&self->deleted_item);
+
+       /* hide notification */
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "revealer_notification"));
+       gtk_revealer_set_reveal_child (GTK_REVEALER (widget), FALSE);
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+       gs_editor_set_page (self, "details");
+}
+
+static void
+gs_editor_button_remove_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       const gchar *name;
+       g_autofree gchar *msg = NULL;
+
+       if (self->selected_item == NULL)
+               return;
+
+       /* send notification */
+       name = as_app_get_name (self->selected_item, NULL);
+       if (name == NULL) {
+               AsApp *item_global = as_store_get_app_by_id (self->store_global,
+                                                            as_app_get_id (self->selected_item));
+               if (item_global != NULL)
+                       name = as_app_get_name (item_global, NULL);
+       }
+       if (name != NULL) {
+               msg = g_strdup_printf ("<b>%s</b> %s", name,
+                                      /* TRANSLATORS, this is prefixed with the
+                                       * app name, e.g. 'Inkscape ' */
+                                       _("banner design deleted."));
+       } else {
+               /* TRANSLATORS, this is a notification */
+               msg = g_strdup (_("Banner design deleted."));
+       }
+       gs_editor_show_notification (self, msg);
+
+       /* save this so we can undo */
+       g_set_object (&self->deleted_item, self->selected_item);
+
+       as_store_remove_app_by_id (self->store, as_app_get_id (self->selected_item));
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+
+       /* set the appropriate page */
+       gs_editor_set_page (self, as_store_get_size (self->store) == 0 ? "none" : "choice");
+}
+
+static void
+gs_editor_checkbutton_editors_pick_cb (GtkToggleButton *widget, GsEditor *self)
+{
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+       if (self->selected_item == NULL)
+               return;
+
+       if (gtk_toggle_button_get_active (widget)) {
+               as_app_add_kudo (self->selected_item, "GnomeSoftware::popular");
+       } else {
+               as_app_remove_kudo (self->selected_item, "GnomeSoftware::popular");
+       }
+       self->pending_changes = TRUE;
+       gs_editor_refresh_details (self);
+}
+
+static void
+gs_editor_checkbutton_category_featured_cb (GtkToggleButton *widget, GsEditor *self)
+{
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+       if (self->selected_item == NULL)
+               return;
+
+       if (gtk_toggle_button_get_active (widget)) {
+               as_app_add_category (self->selected_item, "Featured");
+       } else {
+               as_app_remove_category (self->selected_item, "Featured");
+       }
+       self->pending_changes = TRUE;
+       gs_editor_refresh_details (self);
+}
+
+static void
+gs_editor_entry_desktop_id_notify_cb (GtkEntry *entry, GParamSpec *pspec, GsEditor *self)
+{
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+       if (self->selected_item == NULL)
+               return;
+
+       /* check the name does not already exist */
+       //FIXME
+
+       as_store_remove_app (self->store, self->selected_item);
+       as_app_set_id (self->selected_item, gtk_entry_get_text (entry));
+       as_store_add_app (self->store, self->selected_item);
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+}
+
+static void
+gs_editor_entry_name_notify_cb (GtkEntry *entry, GParamSpec *pspec, GsEditor *self)
+{
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+       if (self->selected_item == NULL)
+               return;
+
+       as_app_set_name (self->selected_item, NULL, gtk_entry_get_text (entry));
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+}
+
+static void
+gs_editor_entry_summary_notify_cb (GtkEntry *entry, GParamSpec *pspec, GsEditor *self)
+{
+       /* ignore, self change */
+       if (self->is_in_refresh)
+               return;
+       if (self->selected_item == NULL)
+               return;
+
+       as_app_set_comment (self->selected_item, NULL, gtk_entry_get_text (entry));
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+}
+
+static gboolean
+gs_editor_delete_event_cb (GtkWindow *window, GdkEvent *event, GsEditor *self)
+{
+       GtkWidget *dialog;
+       gint res;
+
+       if (!self->pending_changes)
+               return FALSE;
+
+       /* ask for confirmation */
+       dialog = gtk_message_dialog_new (window,
+                                        GTK_DIALOG_MODAL |
+                                        GTK_DIALOG_DESTROY_WITH_PARENT,
+                                        GTK_MESSAGE_WARNING,
+                                        GTK_BUTTONS_CANCEL,
+                                        /* TRANSLATORS: window title */
+                                        _("Unsaved changes"));
+       gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                                 _("The application list has unsaved changes."));
+       gtk_dialog_add_button (GTK_DIALOG (dialog),
+                              /* TRANSLATORS: button text */
+                              _("Throw away changes"),
+                              GTK_RESPONSE_CLOSE);
+       res = gtk_dialog_run (GTK_DIALOG (dialog));
+       gtk_widget_destroy (dialog);
+       if (res == GTK_RESPONSE_CLOSE)
+               return FALSE;
+       return TRUE;
+}
+
+static gint
+gs_editor_flow_box_sort_cb (GtkFlowBoxChild *row1, GtkFlowBoxChild *row2, gpointer user_data)
+{
+       GsAppTile *tile1 = GS_APP_TILE (gtk_bin_get_child (GTK_BIN (row1)));
+       GsAppTile *tile2 = GS_APP_TILE (gtk_bin_get_child (GTK_BIN (row2)));
+       return g_strcmp0 (gs_app_get_name (gs_app_tile_get_app (tile1)),
+                         gs_app_get_name (gs_app_tile_get_app (tile2)));
+}
+
+static void
+gs_editor_load_completion_model (GsEditor *self)
+{
+       GPtrArray *apps;
+       GtkListStore *store;
+       GtkTreeIter iter;
+
+       store = GTK_LIST_STORE (gtk_builder_get_object (self->builder, "liststore_ids"));
+       apps = as_store_get_apps (self->store_global);
+       for (guint i = 0; i < apps->len; i++) {
+               AsApp *item = g_ptr_array_index (apps, i);
+               gtk_list_store_append (store, &iter);
+               gtk_list_store_set (store, &iter, 0, as_app_get_id (item), -1);
+       }
+}
+
+static void
+gs_editor_button_new_feature_clicked_cb (GtkApplication *application, GsEditor *self)
+{
+       g_autofree gchar *id = NULL;
+       g_autoptr(AsApp) item = as_app_new ();
+       const gchar *css = "border: 1px solid #808080;\nbackground: #eee;\ncolor: #000;";
+
+       /* add new app */
+       as_app_set_kind (item, AS_APP_KIND_DESKTOP);
+       id = g_strdup_printf ("example-%04x.desktop",
+                             (guint) g_random_int_range (0x0000, 0xffff));
+       as_app_set_id (item, id);
+       as_app_add_metadata (item, "GnomeSoftware::FeatureTile-css", css);
+       as_app_add_kudo (item, "GnomeSoftware::popular");
+       as_app_add_category (item, "Featured");
+       as_store_add_app (self->store, item);
+       g_set_object (&self->selected_item, item);
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+       gs_editor_set_page (self, "details");
+}
+
+static void
+gs_editor_button_new_os_upgrade_clicked_cb (GtkApplication *application, GsEditor *self)
+{
+       g_autofree gchar *id = NULL;
+       g_autoptr(AsApp) item = as_app_new ();
+       const gchar *css = "border: 1px solid #808080;\nbackground: #fffeee;\ncolor: #000;";
+
+       /* add new app */
+       as_app_set_kind (item, AS_APP_KIND_OS_UPGRADE);
+       as_app_set_state (item, AS_APP_STATE_AVAILABLE);
+       as_app_set_id (item, "org.gnome.release");
+       as_app_set_name (item, NULL, "GNOME");
+       as_app_set_comment (item, NULL, "A major upgrade, with new features and added polish.");
+       as_app_add_metadata (item, "GnomeSoftware::UpgradeBanner-css", css);
+       as_store_add_app (self->store, item);
+       g_set_object (&self->selected_item, item);
+
+       self->pending_changes = TRUE;
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+       gs_editor_set_page (self, "details");
+}
+
+static void
+gs_editor_button_new_clicked_cb (GtkWidget *widget, GsEditor *self)
+{
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "popover_new"));
+       gtk_popover_popup (GTK_POPOVER (widget));
+}
+
+static void
+gs_editor_startup_cb (GtkApplication *application, GsEditor *self)
+{
+       GtkTextBuffer *buffer;
+       GtkWidget *main_window;
+       GtkWidget *widget;
+       gboolean ret;
+       guint retval;
+       g_autoptr(GError) error = NULL;
+
+       /* get UI */
+       retval = gtk_builder_add_from_resource (self->builder,
+                                               "/org/gnome/Software/Editor/gs-editor.ui",
+                                               &error);
+       if (retval == 0) {
+               g_warning ("failed to load ui: %s", error->message);
+               return;
+       }
+
+       /* load all system appstream */
+       as_store_set_add_flags (self->store_global, AS_STORE_ADD_FLAG_USE_MERGE_HEURISTIC);
+       ret = as_store_load (self->store_global,
+                            AS_STORE_LOAD_FLAG_IGNORE_INVALID |
+                            AS_STORE_LOAD_FLAG_APP_INFO_SYSTEM |
+                            AS_STORE_LOAD_FLAG_APPDATA |
+                            AS_STORE_LOAD_FLAG_DESKTOP,
+                            self->cancellable,
+                            &error);
+       if (!ret) {
+               g_warning ("failed to load global store: %s", error->message);
+               return;
+       }
+
+       /* load all the IDs into the completion model */
+       gs_editor_load_completion_model (self);
+
+       self->featured_tile1 = gs_feature_tile_new (NULL);
+       self->upgrade_banner = gs_upgrade_banner_new ();
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "box_featured"));
+       gtk_box_pack_start (GTK_BOX (widget), self->featured_tile1, FALSE, FALSE, 0);
+       gtk_box_pack_start (GTK_BOX (widget), self->upgrade_banner, FALSE, FALSE, 0);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "textview_css"));
+       buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
+       g_signal_connect (buffer, "changed",
+                         G_CALLBACK (gs_design_dialog_buffer_changed_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "flowbox_main"));
+       gtk_flow_box_set_sort_func (GTK_FLOW_BOX (widget),
+                                   gs_editor_flow_box_sort_cb,
+                                   self, NULL);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_save"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_save_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new_feature"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_new_feature_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new_os_upgrade"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_new_os_upgrade_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_new"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_new_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_remove"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_remove_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_import"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_import_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_back"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_back_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_menu"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_menu_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_notification_dismiss"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_notification_dismiss_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "button_notification_undo_remove"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_undo_remove_clicked_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder,
+                                                    "checkbutton_editors_pick"));
+       g_signal_connect (widget, "toggled",
+                         G_CALLBACK (gs_editor_checkbutton_editors_pick_cb), self);
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder,
+                                                    "checkbutton_category_featured"));
+       g_signal_connect (widget, "toggled",
+                         G_CALLBACK (gs_editor_checkbutton_category_featured_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_desktop_id"));
+       g_signal_connect (widget, "notify::text",
+                         G_CALLBACK (gs_editor_entry_desktop_id_notify_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_name"));
+       g_signal_connect (widget, "notify::text",
+                         G_CALLBACK (gs_editor_entry_name_notify_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "entry_summary"));
+       g_signal_connect (widget, "notify::text",
+                         G_CALLBACK (gs_editor_entry_summary_notify_cb), self);
+
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "window_main"));
+       g_signal_connect (widget, "delete_event",
+                         G_CALLBACK (gs_editor_delete_event_cb), self);
+
+       /* clear entries */
+       gs_editor_refresh_choice (self);
+       gs_editor_refresh_details (self);
+       gs_editor_refresh_file (self, NULL);
+
+       /* set the appropriate page */
+       gs_editor_set_page (self, "none");
+
+       main_window = GTK_WIDGET (gtk_builder_get_object (self->builder, "window_main"));
+       gtk_application_add_window (application, GTK_WINDOW (main_window));
+       gtk_widget_show (main_window);
+}
+
+
+static int
+gs_editor_commandline_cb (GApplication *application,
+                         GApplicationCommandLine *cmdline,
+                         GsEditor *self)
+{
+       GtkWindow *window;
+       gint argc;
+       gboolean verbose = FALSE;
+       g_auto(GStrv) argv = NULL;
+       g_autoptr(GOptionContext) context = NULL;
+       const GOptionEntry options[] = {
+               { "verbose", '\0', 0, G_OPTION_ARG_NONE, &verbose,
+                 /* TRANSLATORS: show the program version */
+                 _("Use verbose logging"), NULL },
+               { NULL}
+       };
+
+       /* get arguments */
+       argv = g_application_command_line_get_arguments (cmdline, &argc);
+       context = g_option_context_new (NULL);
+       /* TRANSLATORS: program name, an application to add and remove software repositories */
+       g_option_context_set_summary(context, _("GNOME Software Banner Designer"));
+       g_option_context_add_main_entries (context, options, NULL);
+       if (!g_option_context_parse (context, &argc, &argv, NULL))
+               return FALSE;
+
+       /* simple logging... */
+       if (verbose)
+               g_setenv ("G_MESSAGES_DEBUG", "Gs", TRUE);
+
+       /* make sure the window is raised */
+       window = GTK_WINDOW (gtk_builder_get_object (self->builder, "window_main"));
+       gtk_window_present (window);
+
+       return TRUE;
+}
+
+int
+main (int argc, char *argv[])
+{
+       gint status = 0;
+       GsEditor *self = NULL;
+
+       setlocale (LC_ALL, "");
+
+       bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+       bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+       textdomain (GETTEXT_PACKAGE);
+
+       gtk_init (&argc, &argv);
+
+       self = g_new0 (GsEditor, 1);
+       self->cancellable = g_cancellable_new ();
+       self->builder = gtk_builder_new ();
+       self->store = as_store_new ();
+       as_store_set_add_flags (self->store, AS_STORE_ADD_FLAG_USE_UNIQUE_ID);
+       self->store_global = as_store_new ();
+       as_store_set_add_flags (self->store_global, AS_STORE_ADD_FLAG_USE_UNIQUE_ID);
+
+       /* are we already activated? */
+       self->application = gtk_application_new ("org.gnome.Software.Editor",
+                                                G_APPLICATION_HANDLES_COMMAND_LINE);
+       g_signal_connect (self->application, "startup",
+                         G_CALLBACK (gs_editor_startup_cb), self);
+       g_signal_connect (self->application, "command-line",
+                         G_CALLBACK (gs_editor_commandline_cb), self);
+
+       /* run */
+       status = g_application_run (G_APPLICATION (self->application), argc, argv);
+
+       if (self != NULL) {
+               if (self->selected_item != NULL)
+                       g_object_unref (self->selected_item);
+               if (self->deleted_item != NULL)
+                       g_object_unref (self->deleted_item);
+               if (self->refresh_details_delayed_id != 0)
+                       g_source_remove (self->refresh_details_delayed_id);
+               g_object_unref (self->cancellable);
+               g_object_unref (self->store);
+               g_object_unref (self->store_global);
+               g_object_unref (self->builder);
+               g_free (self);
+       }
+       return status;
+}
diff --git a/src/gs-editor.ui b/src/gs-editor.ui
new file mode 100644
index 0000000..0e10884
--- /dev/null
+++ b/src/gs-editor.ui
@@ -0,0 +1,697 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.0 -->
+<interface>
+  <requires lib="gtk+" version="3.18"/>
+  <object class="GtkFileFilter" id="filefilter_appstream">
+    <patterns>
+      <pattern>*.xml</pattern>
+    </patterns>
+  </object>
+  <object class="GtkListStore" id="liststore_ids">
+    <columns>
+      <!-- column-name id -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkEntryCompletion" id="entrycompletion_ids">
+    <property name="model">liststore_ids</property>
+    <property name="minimum_key_length">2</property>
+    <property name="text_column">0</property>
+    <property name="inline_completion">True</property>
+  </object>
+  <object class="GtkApplicationWindow" id="window_main">
+    <property name="can_focus">False</property>
+    <property name="default_width">1400</property>
+    <property name="default_height">700</property>
+    <property name="icon_name">org.gnome.Software</property>
+    <child>
+      <object class="GtkOverlay">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkStack" id="stack_main">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="pixel_size">128</property>
+                    <property name="icon_name">input-tablet-symbolic.symbolic</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">No Designs</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">none</property>
+                <property name="title" translatable="yes">No Designs</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="border_width">24</property>
+                <property name="hscrollbar_policy">never</property>
+                <child>
+                  <object class="GtkViewport">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="shadow_type">none</property>
+                    <child>
+                      <object class="GtkFlowBox" id="flowbox_main">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="halign">start</property>
+                        <property name="valign">start</property>
+                        <property name="homogeneous">True</property>
+                        <property name="column_spacing">12</property>
+                        <property name="row_spacing">12</property>
+                        <property name="min_children_per_line">4</property>
+                        <property name="max_children_per_line">4</property>
+                        <property name="selection_mode">none</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">choice</property>
+                <property name="title" translatable="yes">page1</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="box_component">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkBox" id="box_featured">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="vexpand">True</property>
+                        <property name="border_width">12</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">12</property>
+                        <child>
+                          <object class="GtkInfoBar" id="infobar_css">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="message_type">other</property>
+                            <child internal-child="action_area">
+                              <object class="GtkButtonBox">
+                                <property name="can_focus">False</property>
+                                <property name="spacing">6</property>
+                                <property name="layout_style">end</property>
+                                <child>
+                                  <placeholder/>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">False</property>
+                                <property name="fill">False</property>
+                                <property name="pack_type">end</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child internal-child="content_area">
+                              <object class="GtkBox">
+                                <property name="can_focus">False</property>
+                                <property name="spacing">16</property>
+                                <child>
+                                  <object class="GtkLabel" id="label_infobar_css">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="halign">start</property>
+                                    <property name="hexpand">True</property>
+                                    <property name="label" translatable="yes">Error message here</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">True</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">0</property>
+                                  </packing>
+                                </child>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <placeholder/>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="pack_type">end</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkScrolledWindow">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="shadow_type">in</property>
+                            <child>
+                              <object class="GtkTextView" id="textview_css">
+                                <property name="width_request">400</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="left_margin">12</property>
+                                <property name="right_margin">12</property>
+                                <property name="top_margin">12</property>
+                                <property name="bottom_margin">12</property>
+                                <property name="monospace">True</property>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="expand">True</property>
+                            <property name="fill">True</property>
+                            <property name="pack_type">end</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <style>
+                      <class name="view"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="border_width">12</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">12</property>
+                    <child>
+                      <object class="GtkBox" id="box_desktop_id">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="valign">center</property>
+                            <property name="label" translatable="yes">App ID</property>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="entry_desktop_id">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="width_chars">25</property>
+                            <property name="completion">entrycompletion_ids</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="box_name">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="valign">center</property>
+                            <property name="label" translatable="yes">Name</property>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="entry_name">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="width_chars">25</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="box_summary">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <object class="GtkLabel">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="halign">start</property>
+                            <property name="valign">center</property>
+                            <property name="label" translatable="yes">Summary</property>
+                            <attributes>
+                              <attribute name="weight" value="bold"/>
+                            </attributes>
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkEntry" id="entry_summary">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="width_chars">25</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox" id="box_kudos">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="orientation">vertical</property>
+                        <property name="spacing">3</property>
+                        <child>
+                          <object class="GtkCheckButton" id="checkbutton_editors_pick">
+                            <property name="label" translatable="yes">Editor's Pick</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">0</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkCheckButton" id="checkbutton_category_featured">
+                            <property name="label" translatable="yes">Category Feature</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="halign">start</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">3</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="name">details</property>
+                <property name="title" translatable="yes">page2</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="index">-1</property>
+          </packing>
+        </child>
+        <child type="overlay">
+          <object class="GtkRevealer" id="revealer_notification">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="label_notification">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">start</property>
+                    <property name="margin_start">9</property>
+                    <property name="margin_end">9</property>
+                    <property name="label">Some Title</property>
+                    <property name="wrap">True</property>
+                    <property name="wrap_mode">word-char</property>
+                    <property name="max_width_chars">60</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButtonBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="layout_style">end</property>
+                    <child>
+                      <object class="GtkButton" id="button_notification_undo_remove">
+                        <property name="label" translatable="yes" comments="button in the info 
bar">Undo</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="button_notification_dismiss">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="receives_default">False</property>
+                    <property name="valign">center</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">window-close-symbolic</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="flat"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+                <style>
+                  <class name="app-notification"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="headerbar_main">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="title">Banner Designer</property>
+        <property name="subtitle">fedora.xml</property>
+        <property name="show_close_button">True</property>
+        <child>
+          <object class="GtkButton" id="button_back">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <child>
+              <object class="GtkImage" id="back_image">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">go-previous-symbolic</property>
+                <property name="icon_size">1</property>
+              </object>
+            </child>
+            <style>
+              <class name="image-button"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="button_new">
+            <property name="label" translatable="yes">New Banner</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">start</property>
+          </object>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="button_search">
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="icon_name">system-search-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="button_menu">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="halign">center</property>
+                <property name="icon_name">open-menu-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">5</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkPopover" id="popover_menu">
+    <property name="can_focus">False</property>
+    <property name="border_width">6</property>
+    <property name="relative_to">button_menu</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">3</property>
+        <child>
+          <object class="GtkModelButton" id="button_import">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="border_width">6</property>
+            <property name="text" translatable="yes">Import from file</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="button_save">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="border_width">6</property>
+            <property name="text" translatable="yes">Export to file</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="button_remove">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="border_width">6</property>
+            <property name="text" translatable="yes">Delete Design</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+  <object class="GtkPopover" id="popover_new">
+    <property name="can_focus">False</property>
+    <property name="border_width">6</property>
+    <property name="relative_to">button_new</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">3</property>
+        <child>
+          <object class="GtkModelButton" id="button_new_feature">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="border_width">6</property>
+            <property name="text" translatable="yes">Featured App</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="button_new_os_upgrade">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="border_width">6</property>
+            <property name="text" translatable="yes">OS Upgrade</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/gs-feature-tile.c b/src/gs-feature-tile.c
index 59d9773..dfda13e 100644
--- a/src/gs-feature-tile.c
+++ b/src/gs-feature-tile.c
@@ -165,11 +165,11 @@ GtkWidget *
 gs_feature_tile_new (GsApp *app)
 {
        GsFeatureTile *tile;
-
-       tile = g_object_new (GS_TYPE_FEATURE_TILE, NULL);
+       tile = g_object_new (GS_TYPE_FEATURE_TILE,
+                            "vexpand", FALSE,
+                            NULL);
        if (app != NULL)
                gs_app_tile_set_app (GS_APP_TILE (tile), app);
-
        return GTK_WIDGET (tile);
 }
 
diff --git a/src/gs-upgrade-banner.c b/src/gs-upgrade-banner.c
index cfebc63..c343013 100644
--- a/src/gs-upgrade-banner.c
+++ b/src/gs-upgrade-banner.c
@@ -332,9 +332,9 @@ GtkWidget *
 gs_upgrade_banner_new (void)
 {
        GsUpgradeBanner *self;
-
-       self = g_object_new (GS_TYPE_UPGRADE_BANNER, NULL);
-
+       self = g_object_new (GS_TYPE_UPGRADE_BANNER,
+                            "vexpand", FALSE,
+                            NULL);
        return GTK_WIDGET (self);
 }
 
diff --git a/src/gs-upgrade-banner.ui b/src/gs-upgrade-banner.ui
index 59a8b86..83727f3 100644
--- a/src/gs-upgrade-banner.ui
+++ b/src/gs-upgrade-banner.ui
@@ -8,8 +8,6 @@
         <property name="orientation">vertical</property>
         <property name="hexpand">True</property>
         <property name="vexpand">True</property>
-        <property name="margin-bottom">48</property>
-        <property name="height-request">300</property>
         <property name="valign">center</property>
         <style>
           <class name="upgrade-banner"/>
@@ -44,7 +42,7 @@
             <property name="halign">fill</property>
             <property name="valign">end</property>
             <property name="spacing">12</property>
-            <property name="margin_top">16</property>
+            <property name="margin_top">48</property>
             <style>
               <class name="osd"/>
               <class name="upgrade-buttons"/>
diff --git a/src/meson.build b/src/meson.build
index d333edd..a7c29d0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -127,6 +127,38 @@ executable(
   install_dir : get_option('libexecdir')
 )
 
+resources_editor_src = gnome.compile_resources(
+  'gs-editor-resources',
+  'gnome-software-editor.gresource.xml',
+  source_dir : '.',
+  c_name : 'gs'
+)
+
+executable(
+  'gnome-software-editor',
+  resources_editor_src,
+  sources : [
+    'gs-app-tile.c',
+    'gs-common.c',
+    'gs-editor.c',
+    'gs-summary-tile.c',
+    'gs-star-widget.c',
+    'gs-feature-tile.c',
+    'gs-upgrade-banner.c',
+  ],
+  include_directories : [
+    include_directories('..'),
+    include_directories('../lib'),
+  ],
+  dependencies : gnome_software_dependencies,
+  link_with : [
+    libgnomesoftware
+  ],
+  c_args : cargs,
+  install : true,
+  install_dir : get_option('bindir')
+)
+
 # no quoting
 cdata = configuration_data()
 cdata.set('bindir', join_paths(get_option('prefix'),
@@ -161,6 +193,15 @@ i18n.merge_file(
 )
 
 i18n.merge_file(
+  input: 'org.gnome.Software.Editor.desktop.in',
+  output: 'org.gnome.Software.Editor.desktop',
+  type: 'desktop',
+  po_dir: join_paths(meson.source_root(), 'po'),
+  install: true,
+  install_dir: join_paths(get_option('datadir'), 'applications')
+)
+
+i18n.merge_file(
   input: 'gnome-software-local-file.desktop.in',
   output: 'gnome-software-local-file.desktop',
   type: 'desktop',
@@ -174,7 +215,7 @@ install_data('org.gnome.Software-search-provider.ini',
 
 if get_option('enable-man')
   xsltproc = find_program('xsltproc')
-  custom_target('manfile',
+  custom_target('manfile-gnome-software',
     input: 'gnome-software.xml',
     output: 'gnome-software.1',
     install: true,
@@ -192,6 +233,24 @@ if get_option('enable-man')
       '@INPUT@'
     ]
   )
+  custom_target('manfile-gnome-software-editor',
+    input: 'gnome-software-editor.xml',
+    output: 'gnome-software-editor.1',
+    install: true,
+    install_dir: join_paths(get_option('mandir'), 'man1'),
+    command: [
+      xsltproc,
+      '--nonet',
+      '--stringparam', 'man.output.quietly', '1',
+      '--stringparam', 'funcsynopsis.style', 'ansi',
+      '--stringparam', 'man.th.extra1.suppress', '1',
+      '--stringparam', 'man.authors.section.enabled', '0',
+      '--stringparam', 'man.copyright.section.enabled', '0',
+      '-o', '@OUTPUT@',
+      'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl',
+      '@INPUT@'
+    ]
+  )
 endif
 
 if get_option('enable-packagekit')
diff --git a/src/org.gnome.Software.Editor.desktop.in b/src/org.gnome.Software.Editor.desktop.in
new file mode 100644
index 0000000..7cac266
--- /dev/null
+++ b/src/org.gnome.Software.Editor.desktop.in
@@ -0,0 +1,17 @@
+[Desktop Entry]
+Name=Banner Designer
+Comment=Design the featured banners for GNOME Software
+# Translators: Do NOT translate or transliterate this text (this is an icon file name)!
+Icon=org.gnome.Software
+Exec=gnome-software-editor
+NoDisplay=true
+Terminal=false
+Type=Application
+Categories=GNOME;GTK;System;PackageManager;
+# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list 
MUST also end with a semicolon!
+Keywords=AppStream;Software;App;
+StartupNotify=true
+X-GNOME-Bugzilla-Bugzilla=GNOME
+X-GNOME-Bugzilla-Product=gnome-software
+X-GNOME-Bugzilla-Component=editor
+X-GNOME-UsesNotifications=true


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