[gnome-software/wip/hughsie/appstream-editor: 12/12] Add a banner designer utility



commit 2e6ab5abf2bb8a6cdf25a12c906be0e60b96dc57
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           |    7 +-
 meson.build                              |    2 +-
 po/POTFILES.in                           |    2 +
 src/gnome-software-editor.gresource.xml  |   11 +
 src/gs-editor.c                          |  952 ++++++++++++++++++++++++++++++
 src/gs-editor.ui                         |  434 ++++++++++++++
 src/meson.build                          |   40 ++
 src/org.gnome.Software.Editor.desktop.in |   17 +
 8 files changed, 1463 insertions(+), 2 deletions(-)
---
diff --git a/contrib/gnome-software.spec.in b/contrib/gnome-software.spec.in
index 0b7600e..d5a59f2 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,10 @@ 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
+%{_libexecdir}/gnome-software-editor
+
 %files devel
 %{_libdir}/pkgconfig/gnome-software.pc
 %dir %{_includedir}/gnome-software
diff --git a/meson.build b/meson.build
index 1075150..5d61280 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..d6dbd5f
--- /dev/null
+++ b/src/gnome-software-editor.gresource.xml
@@ -0,0 +1,11 @@
+<?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-star-widget.ui</file>
+ </gresource>
+</gresources>
diff --git a/src/gs-editor.c b/src/gs-editor.c
new file mode 100644
index 0000000..7c288a1
--- /dev/null
+++ b/src/gs-editor.c
@@ -0,0 +1,952 @@
+/* -*- 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-feature-tile.h"
+#include "gs-summary-tile.h"
+#include "gs-common.h"
+
+typedef struct {
+       GCancellable            *cancellable;
+       GtkApplication          *application;
+       GtkBuilder              *builder;
+       GtkWidget               *featured_tile1;
+       AsStore                 *store;
+       AsStore                 *store_global;
+       AsApp                   *selected_item;
+       gboolean                 is_in_refresh;
+       gboolean                 pending_changes;
+} 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;
+       GsApp *app;
+       const gchar *css = NULL;
+       g_autofree gchar *css_new = NULL;
+       g_autoptr(GError) error = NULL;
+
+       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) {
+               g_autoptr(AsIcon) ic = NULL;
+               g_debug ("no app found for %s, using fallback", as_app_get_id (item));
+               gs_app_set_name (app, GS_APP_QUALITY_NORMAL,
+                                "Application");
+               gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+                                   "Description");
+               gs_app_set_description (app, GS_APP_QUALITY_NORMAL,
+                                       "A multiline description");
+               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);
+       } 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);
+               }
+       }
+       gs_editor_refine_app_pixbuf (app);
+       css = as_app_get_metadata_item (item, "GnomeSoftware::FeatureTile-css");
+       if (css != 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, "GnomeSoftware::FeatureTile-css", css);
+               } else {
+                       gs_app_set_metadata (app, "GnomeSoftware::FeatureTile-css", css_new);
+               }
+       } else {
+               gs_app_set_metadata (app, "GnomeSoftware::FeatureTile-css", NULL);
+       }
+       return app;
+}
+
+static void
+gs_editor_refresh_details (GsEditor *self)
+{
+       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);
+
+       /* update */
+       if (app != NULL) {
+               g_debug ("refreshing details for %s", gs_app_get_id (app));
+               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);
+       }
+
+       /* 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) {
+                       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);
+               }
+               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)) {
+               widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "label_infobar_css"));
+               gtk_label_set_label (GTK_LABEL (widget), error->message);
+               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 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_editor_refresh_details (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_add"));
+               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);
+
+       } 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_add"));
+               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);
+
+       } 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_add"));
+               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);
+       }
+}
+
+static void
+gs_editor_app_tile_clicked_cb (GsAppTile *tile, GsEditor *self)
+{
+       GsApp *app;
+       AsApp *item = NULL;
+       GPtrArray *apps;
+
+       app = gs_app_tile_get_app (tile);
+       apps = as_store_get_apps (self->store);
+       for (guint i = 0; i < apps->len; i++) {
+               AsApp *item_tmp = g_ptr_array_index (apps, i);
+               if (g_strcmp0 (as_app_get_id (item_tmp),
+                              gs_app_get_id (app)) == 0) {
+                       item = item_tmp;
+                       break;
+               }
+       }
+//     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_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_button_add_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_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_remove_clicked_cb (GtkApplication *application, GsEditor *self)
+{
+       if (self->selected_item == NULL)
+               return;
+       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_text_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_app_set_id (self->selected_item, 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_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);
+       gtk_widget_show (self->featured_tile1);
+       widget = GTK_WIDGET (gtk_builder_get_object (self->builder, "box_featured"));
+       gtk_box_pack_start (GTK_BOX (widget), self->featured_tile1, 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_add"));
+       g_signal_connect (widget, "clicked",
+                         G_CALLBACK (gs_editor_button_add_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,
+                                                    "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_text_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);
+               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..f72ee10
--- /dev/null
+++ b/src/gs-editor.ui
@@ -0,0 +1,434 @@
+<?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="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">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="GtkGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_left">18</property>
+                <property name="margin_right">18</property>
+                <property name="margin_top">18</property>
+                <property name="margin_bottom">18</property>
+                <property name="row_spacing">12</property>
+                <property name="column_spacing">12</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="halign">end</property>
+                    <property name="valign">center</property>
+                    <property name="label" translatable="yes">App ID</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">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="text" translatable="yes">gimp.desktop</property>
+                    <property name="completion">entrycompletion_ids</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <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="left_attach">1</property>
+                    <property name="top_attach">1</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="left_attach">1</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                    <property name="height">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="button_remove">
+                    <property name="label" translatable="yes">Delete</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="halign">start</property>
+                    <property name="valign">end</property>
+                    <style>
+                      <class name="destructive-action"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">3</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">details</property>
+            <property name="title">page2</property>
+            <property name="position">2</property>
+          </packing>
+        </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_add">
+            <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>
+        </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_save">
+            <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">document-save-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="button_import">
+            <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">document-open-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/meson.build b/src/meson.build
index d333edd..264161a 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -127,6 +127,37 @@ 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',
+  ],
+  include_directories : [
+    include_directories('..'),
+    include_directories('../lib'),
+  ],
+  dependencies : gnome_software_dependencies,
+  link_with : [
+    libgnomesoftware
+  ],
+  c_args : cargs,
+  install : true,
+  install_dir : 'libexec'
+)
+
 # no quoting
 cdata = configuration_data()
 cdata.set('bindir', join_paths(get_option('prefix'),
@@ -161,6 +192,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',
diff --git a/src/org.gnome.Software.Editor.desktop.in b/src/org.gnome.Software.Editor.desktop.in
new file mode 100644
index 0000000..4be01cc
--- /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=/usr/libexec/gnome-software-editor %U
+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]