[gnome-panel/wip/muktupavels/launcher: 1/2] launcher: new module for launcher applet



commit 21b2180863346268710f145892699b0e7b1a87c3
Author: Alberts Muktupāvels <alberts muktupavels gmail com>
Date:   Sun Apr 12 17:07:26 2020 +0300

    launcher: new module for launcher applet

 configure.ac                                       |    8 +
 data/theme/common.css                              |   13 +-
 modules/Makefile.am                                |    1 +
 modules/launcher/Makefile.am                       |   98 ++
 modules/launcher/custom-launcher-menu.ui           |   14 +
 modules/launcher/gp-custom-launcher-applet.c       |  228 +++
 modules/launcher/gp-custom-launcher-applet.h       |   33 +
 modules/launcher/gp-editor.c                       |  998 +++++++++++++
 modules/launcher/gp-editor.h                       |   66 +
 modules/launcher/gp-icon-name-chooser.c            |  933 ++++++++++++
 modules/launcher/gp-icon-name-chooser.h            |   36 +
 modules/launcher/gp-icon-name-chooser.ui           |  295 ++++
 modules/launcher/gp-launcher-applet.c              | 1580 ++++++++++++++++++++
 modules/launcher/gp-launcher-applet.h              |   41 +
 modules/launcher/gp-launcher-button.c              |   47 +
 modules/launcher/gp-launcher-button.h              |   33 +
 modules/launcher/gp-launcher-module.c              |   95 ++
 modules/launcher/gp-launcher-properties.c          |  695 +++++++++
 modules/launcher/gp-launcher-properties.h          |   33 +
 modules/launcher/gp-launcher-utils.c               |  313 ++++
 modules/launcher/gp-launcher-utils.h               |   54 +
 modules/launcher/launcher-menu.ui                  |   14 +
 modules/launcher/launcher.gresource.xml            |    8 +
 ...g.gnome.gnome-panel.applet.launcher.gschema.xml |    9 +
 po/POTFILES.in                                     |    9 +
 25 files changed, 5651 insertions(+), 3 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 3d0588aa7..e80525452 100644
--- a/configure.ac
+++ b/configure.ac
@@ -154,6 +154,13 @@ PKG_CHECK_MODULES(FISH, gtk+-3.0 >= $GTK_REQUIRED cairo >= $CAIRO_REQUIRED)
 AC_SUBST(FISH_CFLAGS)
 AC_SUBST(FISH_LIBS)
 
+PKG_CHECK_MODULES([LAUNCHER], [
+  gio-unix-2.0 >= $GLIB_REQUIRED
+  gtk+-3.0 >= $GTK_REQUIRED
+  libgnome-menu-3.0 >= $LIBGNOME_MENU_REQUIRED
+  libsystemd >= $LIBSYSTEMD_REQUIRED
+])
+
 PKG_CHECK_MODULES([MENU], [
   gdm
   gio-unix-2.0 >= $GLIB_REQUIRED
@@ -279,6 +286,7 @@ AC_CONFIG_FILES([
   modules/clock/Makefile
   modules/clock/pixmaps/Makefile
   modules/fish/Makefile
+  modules/launcher/Makefile
   modules/menu/Makefile
   modules/notification-area/Makefile
   modules/separator/Makefile
diff --git a/data/theme/common.css b/data/theme/common.css
index 63584cb16..07cae6d09 100644
--- a/data/theme/common.css
+++ b/data/theme/common.css
@@ -31,15 +31,18 @@ panel-toplevel.right gp-applet > menubar > .gp-image-menu-item:not(.image-only)
   margin-top: 5px;
 }
 
-gp-menu-button:hover .icon {
+gp-menu-button:hover .icon,
+gp-launcher-button:hover image {
   -gtk-icon-effect: highlight;
 }
 
-panel-toplevel.horizontal gp-menu-button {
+panel-toplevel.horizontal gp-menu-button,
+panel-toplevel.horizontal gp-launcher-button  {
   padding: 0 2px;
 }
 
-panel-toplevel.vertical gp-menu-button {
+panel-toplevel.vertical gp-menu-button,
+panel-toplevel.vertical gp-launcher-button {
   padding: 2px 0;
 }
 
@@ -87,3 +90,7 @@ gp-arrow-button {
   min-width: 0px;
   min-height: 20px;
 }
+
+.context-row {
+  padding: 4px 10px;
+}
diff --git a/modules/Makefile.am b/modules/Makefile.am
index 586caca48..ef377e340 100644
--- a/modules/Makefile.am
+++ b/modules/Makefile.am
@@ -3,6 +3,7 @@ NULL =
 SUBDIRS = \
        clock \
        fish \
+       launcher \
        menu \
        notification-area \
        separator \
diff --git a/modules/launcher/Makefile.am b/modules/launcher/Makefile.am
new file mode 100644
index 000000000..6709891df
--- /dev/null
+++ b/modules/launcher/Makefile.am
@@ -0,0 +1,98 @@
+NULL =
+
+launcher_libdir = $(libdir)/gnome-panel/modules
+launcher_lib_LTLIBRARIES = launcher.la
+
+launcher_la_CPPFLAGS = \
+       -DLOCALEDIR=\""$(localedir)"\" \
+       -DGMENU_I_KNOW_THIS_IS_UNSTABLE \
+       -DGRESOURCE_PREFIX=\""/org/gnome/gnome-panel/modules/launcher"\" \
+       -DG_LOG_DOMAIN=\""launcher"\" \
+       -DG_LOG_USE_STRUCTURED=1 \
+       -I$(top_srcdir) \
+       $(AM_CPPFLAGS) \
+       $(NULL)
+
+launcher_la_CFLAGS = \
+       $(LIBGNOME_PANEL_CFLAGS) \
+       $(LAUNCHER_CFLAGS) \
+       $(WARN_CFLAGS) \
+       $(AM_CFLAGS) \
+       $(NULL)
+
+launcher_la_SOURCES = \
+       gp-custom-launcher-applet.c \
+       gp-custom-launcher-applet.h \
+       gp-editor.c \
+       gp-editor.h \
+       gp-icon-name-chooser.c \
+       gp-icon-name-chooser.h \
+       gp-launcher-applet.c \
+       gp-launcher-applet.h \
+       gp-launcher-button.c \
+       gp-launcher-button.h \
+       gp-launcher-module.c \
+       gp-launcher-properties.c \
+       gp-launcher-properties.h \
+       gp-launcher-utils.c \
+       gp-launcher-utils.h \
+       $(BUILT_SOURCES) \
+       $(NULL)
+
+launcher_la_LIBADD = \
+       $(top_builddir)/libgnome-panel/libgnome-panel.la \
+       $(LIBGNOME_PANEL_LIBS) \
+       $(LAUNCHER_LIBS) \
+       $(NULL)
+
+launcher_la_LDFLAGS = \
+       -module -avoid-version \
+       $(WARN_LDFLAGS) \
+       $(AM_LDFLAGS) \
+       $(NULL)
+
+gsettings_SCHEMAS = \
+       org.gnome.gnome-panel.applet.launcher.gschema.xml \
+       $(NULL)
+
+@GSETTINGS_RULES@
+
+ui_FILES = \
+       custom-launcher-menu.ui \
+       gp-icon-name-chooser.ui \
+       launcher-menu.ui \
+       $(NULL)
+
+launcher_resources = \
+       $(shell $(GLIB_COMPILE_RESOURCES) \
+               --sourcedir=$(srcdir) \
+               --generate-dependencies \
+               $(srcdir)/launcher.gresource.xml)
+
+launcher-resources.c: launcher.gresource.xml $(launcher_resources)
+       $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) \
+               --target=$@ --sourcedir=$(srcdir) \
+               --generate --c-name launcher $<
+
+launcher-resources.h: launcher.gresource.xml
+       $(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) \
+               --target=$@ --sourcedir=$(srcdir) \
+               --generate --c-name launcher $<
+
+BUILT_SOURCES = \
+       launcher-resources.c \
+       launcher-resources.h \
+       $(NULL)
+
+EXTRA_DIST = \
+       launcher.gresource.xml \
+       $(gsettings_SCHEMAS) \
+       $(ui_FILES) \
+       $(NULL)
+
+CLEANFILES = \
+       *.gschema.valid \
+       $(BUILT_SOURCES) \
+       $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/modules/launcher/custom-launcher-menu.ui b/modules/launcher/custom-launcher-menu.ui
new file mode 100644
index 000000000..11fa21433
--- /dev/null
+++ b/modules/launcher/custom-launcher-menu.ui
@@ -0,0 +1,14 @@
+<interface>
+  <menu id="custom-launcher-menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Launch</attribute>
+        <attribute name="action">custom-launcher.launch</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Properties</attribute>
+        <attribute name="action">custom-launcher.properties</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/modules/launcher/gp-custom-launcher-applet.c b/modules/launcher/gp-custom-launcher-applet.c
new file mode 100644
index 000000000..8985bf721
--- /dev/null
+++ b/modules/launcher/gp-custom-launcher-applet.c
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "gp-custom-launcher-applet.h"
+
+#include "gp-editor.h"
+#include "gp-launcher-utils.h"
+
+typedef struct
+{
+  GpInitialSetupDialog *dialog;
+  GpEditor             *editor;
+} LauncherData;
+
+struct _GpCustomLauncherApplet
+{
+  GpLauncherApplet parent;
+};
+
+G_DEFINE_TYPE (GpCustomLauncherApplet,
+               gp_custom_launcher_applet,
+               GP_TYPE_LAUNCHER_APPLET)
+
+static LauncherData *
+launcher_data_new (GpInitialSetupDialog *dialog)
+{
+  LauncherData *data;
+
+  data = g_new0 (LauncherData, 1);
+  data->dialog = dialog;
+
+  return data;
+}
+
+static void
+launcher_data_free (gpointer user_data)
+{
+  LauncherData *data;
+
+  data = (LauncherData *) user_data;
+
+  g_free (data);
+}
+
+static void
+check_required_info (LauncherData *data)
+{
+  gboolean done;
+  GpEditorType type;
+  const char *type_string;
+
+  type = gp_editor_get_editor_type (data->editor);
+  type_string = NULL;
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      type_string = G_KEY_FILE_DESKTOP_TYPE_APPLICATION;
+    }
+  else if (type == GP_EDITOR_TYPE_DIRECTORY ||
+           type == GP_EDITOR_TYPE_FILE)
+    {
+      type_string = G_KEY_FILE_DESKTOP_TYPE_LINK;
+    }
+
+  done = gp_launcher_validate (gp_editor_get_name (data->editor),
+                               type_string,
+                               gp_editor_get_name (data->editor),
+                               gp_editor_get_command (data->editor),
+                               gp_editor_get_comment (data->editor),
+                               NULL);
+
+  gp_initital_setup_dialog_set_done (data->dialog, done);
+}
+
+static void
+icon_changed_cb (GpEditor     *editor,
+                 LauncherData *data)
+{
+  const char *icon;
+  GVariant *variant;
+
+  icon = gp_editor_get_icon (editor);
+  variant = icon != NULL ? g_variant_new_string (icon) : NULL;
+  gp_initital_setup_dialog_set_setting (data->dialog, "icon", variant);
+
+  check_required_info (data);
+}
+
+static void
+type_changed_cb (GpEditor     *editor,
+                 LauncherData *data)
+{
+  GpEditorType type;
+  const char *type_string;
+  GVariant *variant;
+
+  type = gp_editor_get_editor_type (editor);
+  type_string = NULL;
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      type_string = G_KEY_FILE_DESKTOP_TYPE_APPLICATION;
+    }
+  else if (type == GP_EDITOR_TYPE_DIRECTORY ||
+           type == GP_EDITOR_TYPE_FILE)
+    {
+      type_string = G_KEY_FILE_DESKTOP_TYPE_LINK;
+    }
+
+  variant = type_string != NULL ? g_variant_new_string (type_string) : NULL;
+  gp_initital_setup_dialog_set_setting (data->dialog, "type", variant);
+
+  if (type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      variant = g_variant_new_boolean (TRUE);
+      gp_initital_setup_dialog_set_setting (data->dialog, "terminal", variant);
+    }
+  else
+    {
+      gp_initital_setup_dialog_set_setting (data->dialog, "terminal", NULL);
+    }
+
+  check_required_info (data);
+}
+
+static void
+name_changed_cb (GpEditor     *editor,
+                 LauncherData *data)
+{
+  const char *name;
+  GVariant *variant;
+
+  name = gp_editor_get_name (editor);
+  variant = name != NULL ? g_variant_new_string (name) : NULL;
+  gp_initital_setup_dialog_set_setting (data->dialog, "name", variant);
+
+  check_required_info (data);
+}
+
+static void
+command_changed_cb (GpEditor     *editor,
+                    LauncherData *data)
+{
+  const char *command;
+  GVariant *variant;
+
+  command = gp_editor_get_command (editor);
+  variant = command != NULL ? g_variant_new_string (command) : NULL;
+  gp_initital_setup_dialog_set_setting (data->dialog, "command", variant);
+
+  check_required_info (data);
+}
+
+static void
+comment_changed_cb (GpEditor     *editor,
+                    LauncherData *data)
+{
+  const char *comment;
+  GVariant *variant;
+
+  comment = gp_editor_get_comment (editor);
+  variant = comment != NULL ? g_variant_new_string (comment) : NULL;
+  gp_initital_setup_dialog_set_setting (data->dialog, "comment", variant);
+
+  check_required_info (data);
+}
+
+static const char *
+gp_custom_launcher_applet_get_menu_resource (void)
+{
+  return GRESOURCE_PREFIX "/custom-launcher-menu.ui";
+}
+
+static void
+gp_custom_launcher_applet_class_init (GpCustomLauncherAppletClass *self_class)
+{
+  GpLauncherAppletClass *launcher_class;
+
+  launcher_class = GP_LAUNCHER_APPLET_CLASS (self_class);
+
+  launcher_class->get_menu_resource = gp_custom_launcher_applet_get_menu_resource;
+}
+
+static void
+gp_custom_launcher_applet_init (GpCustomLauncherApplet *self)
+{
+}
+
+void
+gp_custom_launcher_applet_initial_setup_dialog (GpInitialSetupDialog *dialog)
+{
+  GtkWidget *editor;
+  LauncherData *data;
+
+  editor = gp_editor_new (FALSE);
+
+  data = launcher_data_new (dialog);
+  data->editor = GP_EDITOR (editor);
+
+  g_signal_connect (editor, "icon-changed", G_CALLBACK (icon_changed_cb), data);
+  g_signal_connect (editor, "type-changed", G_CALLBACK (type_changed_cb), data);
+  g_signal_connect (editor, "name-changed", G_CALLBACK (name_changed_cb), data);
+  g_signal_connect (editor, "command-changed", G_CALLBACK (command_changed_cb), data);
+  g_signal_connect (editor, "comment-changed", G_CALLBACK (comment_changed_cb), data);
+
+  icon_changed_cb (data->editor, data);
+  type_changed_cb (data->editor, data);
+
+  gp_initital_setup_dialog_add_content_widget (dialog, editor, data,
+                                               launcher_data_free);
+}
diff --git a/modules/launcher/gp-custom-launcher-applet.h b/modules/launcher/gp-custom-launcher-applet.h
new file mode 100644
index 000000000..79d62528f
--- /dev/null
+++ b/modules/launcher/gp-custom-launcher-applet.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_CUSTOM_LAUNCHER_APPLET_H
+#define GP_CUSTOM_LAUNCHER_APPLET_H
+
+#include "gp-launcher-applet.h"
+
+G_BEGIN_DECLS
+
+#define GP_TYPE_CUSTOM_LAUNCHER_APPLET (gp_custom_launcher_applet_get_type ())
+G_DECLARE_FINAL_TYPE (GpCustomLauncherApplet, gp_custom_launcher_applet,
+                      GP, CUSTOM_LAUNCHER_APPLET, GpLauncherApplet)
+
+void gp_custom_launcher_applet_initial_setup_dialog (GpInitialSetupDialog *dialog);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-editor.c b/modules/launcher/gp-editor.c
new file mode 100644
index 000000000..ae41a8c83
--- /dev/null
+++ b/modules/launcher/gp-editor.c
@@ -0,0 +1,998 @@
+/*
+ * Copyright (C) 2004 - 2006 Vincent Untz
+ * Copyright (C) 2010 Novell, Inc.
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *     Alberts Muktupāvels <alberts muktupavels gmail com>
+ *     Vincent Untz <vuntz gnome org>
+ */
+
+#include "config.h"
+#include "gp-editor.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "gp-icon-name-chooser.h"
+
+#define FALLBACK_ICON "gnome-panel-launcher"
+
+typedef struct
+{
+  GpEditorType  type;
+  const char   *name;
+} GpTypeComboItem;
+
+static GpTypeComboItem type_combo_items[] =
+{
+  { GP_EDITOR_TYPE_APPLICATION, N_("Application") },
+  { GP_EDITOR_TYPE_TERMINAL_APPLICATION, N_("Application in Terminal") },
+  { GP_EDITOR_TYPE_DIRECTORY, N_("Directory") },
+  { GP_EDITOR_TYPE_FILE, N_("File") },
+  { GP_EDITOR_TYPE_NONE, NULL, }
+};
+
+struct _GpEditor
+{
+  GtkBox        parent;
+
+  gboolean      edit;
+
+  GtkIconTheme *icon_theme;
+
+  char         *icon;
+  GtkWidget    *icon_button;
+  GtkWidget    *icon_image;
+  GtkWidget    *icon_chooser;
+
+  GtkTreeModel *type_model;
+  GtkWidget    *type_label;
+  GtkWidget    *type_combo;
+
+  GtkWidget    *name_label;
+  GtkWidget    *name_entry;
+
+  GtkWidget    *command_label;
+  GtkWidget    *command_entry;
+  GtkWidget    *command_browse;
+  GtkWidget    *command_chooser;
+
+  GtkWidget    *comment_label;
+  GtkWidget    *comment_entry;
+};
+
+enum
+{
+  PROP_0,
+
+  PROP_EDIT,
+
+  LAST_PROP
+};
+
+static GParamSpec *editor_properties[LAST_PROP] = { NULL };
+
+enum
+{
+  ICON_CHANGED,
+  TYPE_CHANGED,
+  NAME_CHANGED,
+  COMMAND_CHANGED,
+  COMMENT_CHANGED,
+
+  LAST_SIGNAL
+};
+
+static guint editor_signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (GpEditor, gp_editor, GTK_TYPE_BOX)
+
+static GpEditorType
+get_editor_type (GpEditor *self)
+{
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  GpEditorType type;
+
+  if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (self->type_combo), &iter))
+    return GP_EDITOR_TYPE_NONE;
+
+  model = gtk_combo_box_get_model (GTK_COMBO_BOX (self->type_combo));
+  gtk_tree_model_get (model, &iter, 1, &type, -1);
+
+  return type;
+}
+
+static char *
+filename_to_exec_uri (const char *filename)
+{
+  GString *exec_uri;
+  const char *c;
+
+  if (filename == NULL)
+    return g_strdup ("");
+
+  if (strchr (filename, ' ') == NULL)
+    return g_strdup (filename);
+
+  exec_uri = g_string_new_len (NULL, strlen (filename));
+
+  g_string_append_c (exec_uri, '"');
+
+  for (c = filename; *c != '\0'; c++)
+    {
+      if (*c == '"')
+        g_string_append (exec_uri, "\\\"");
+      else
+        g_string_append_c (exec_uri, *c);
+    }
+
+  g_string_append_c (exec_uri, '"');
+
+  return g_string_free (exec_uri, FALSE);
+}
+
+static void
+update_icon_image (GpEditor *self)
+{
+  const char *icon;
+  GtkIconSize size;
+  int px_size;
+
+  icon = gp_editor_get_icon (self);
+  size = GTK_ICON_SIZE_DIALOG;
+  px_size = 48;
+
+  if (g_path_is_absolute (self->icon))
+    {
+      GdkPixbuf *pixbuf;
+
+      pixbuf = gdk_pixbuf_new_from_file_at_size (icon, px_size, px_size, NULL);
+      gtk_image_set_from_pixbuf (GTK_IMAGE (self->icon_image), pixbuf);
+      g_clear_object (&pixbuf);
+    }
+  else
+    {
+      gtk_image_set_from_icon_name (GTK_IMAGE (self->icon_image), icon, size);
+      gtk_image_set_pixel_size (GTK_IMAGE (self->icon_image), px_size);
+    }
+}
+
+static void
+icon_name_changed (GpEditor   *self,
+                   const char *icon_name)
+{
+  g_clear_pointer (&self->icon, g_free);
+  self->icon = g_strdup (icon_name);
+
+  g_signal_emit (self, editor_signals[ICON_CHANGED], 0);
+  update_icon_image (self);
+}
+
+static void
+icon_chooser_destroy_cb (GtkWidget *widget,
+                         GpEditor  *self)
+{
+  self->icon_chooser = NULL;
+}
+
+static void
+icon_selected_cb (GpIconNameChooser *chooser,
+                  const char        *icon_name,
+                  GpEditor          *self)
+{
+  icon_name_changed (self, icon_name);
+}
+
+static void
+choose_icon_name_activate_cb (GtkMenuItem *item,
+                              GpEditor    *self)
+{
+  if (self->icon_chooser != NULL &&
+      GP_IS_ICON_NAME_CHOOSER (self->icon_chooser))
+    {
+      gtk_window_present (GTK_WINDOW (self->icon_chooser));
+      return;
+    }
+
+  g_clear_pointer (&self->icon_chooser, gtk_widget_destroy);
+
+  self->icon_chooser = gp_icon_name_chooser_new ();
+
+  g_signal_connect (self->icon_chooser,
+                    "icon-selected",
+                    G_CALLBACK (icon_selected_cb),
+                    self);
+
+  g_signal_connect (self->icon_chooser,
+                    "destroy",
+                    G_CALLBACK (icon_chooser_destroy_cb),
+                    self);
+
+  gtk_window_set_destroy_with_parent (GTK_WINDOW (self->icon_chooser), TRUE);
+  gtk_window_present (GTK_WINDOW (self->icon_chooser));
+
+  if (self->icon != NULL && !g_path_is_absolute (self->icon))
+    gp_icon_name_chooser_set_icon_name (GP_ICON_NAME_CHOOSER (self->icon_chooser),
+                                        self->icon);
+}
+
+static void
+icon_chooser_update_preview_cb (GtkFileChooser *chooser,
+                                GtkImage       *preview)
+{
+  gchar *filename;
+  GdkPixbuf *pixbuf;
+
+  filename = gtk_file_chooser_get_preview_filename (chooser);
+  if (!filename)
+    return;
+
+  pixbuf = gdk_pixbuf_new_from_file_at_size (filename, 128, 128, NULL);
+  g_free (filename);
+
+  gtk_file_chooser_set_preview_widget_active (chooser, !!pixbuf);
+  gtk_image_set_from_pixbuf (preview, pixbuf);
+  g_clear_object (&pixbuf);
+}
+
+static void
+icon_chooser_response_cb (GtkFileChooser *chooser,
+                          gint            response_id,
+                          GpEditor       *self)
+{
+  if (response_id == GTK_RESPONSE_ACCEPT)
+    {
+      g_clear_pointer (&self->icon, g_free);
+      self->icon = gtk_file_chooser_get_filename (chooser);
+
+      g_signal_emit (self, editor_signals[ICON_CHANGED], 0);
+      update_icon_image (self);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (chooser));
+}
+
+static void
+choose_icon_file_activate_cb (GtkMenuItem *item,
+                              GpEditor    *self)
+{
+  GtkWidget *toplevel;
+  GtkFileChooser *chooser;
+  GtkFileFilter *filter;
+  GtkWidget *preview;
+
+  if (self->icon_chooser != NULL &&
+      GTK_IS_FILE_CHOOSER_DIALOG (self->icon_chooser))
+    {
+      gtk_window_present (GTK_WINDOW (self->icon_chooser));
+      return;
+    }
+
+  g_clear_pointer (&self->icon_chooser, gtk_widget_destroy);
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+  self->icon_chooser = gtk_file_chooser_dialog_new (_("Choose Icon File"),
+                                                    GTK_WINDOW (toplevel),
+                                                    GTK_FILE_CHOOSER_ACTION_OPEN,
+                                                    _("_Cancel"),
+                                                    GTK_RESPONSE_CANCEL,
+                                                    _("_Open"),
+                                                    GTK_RESPONSE_ACCEPT,
+                                                    NULL);
+
+  chooser = GTK_FILE_CHOOSER (self->icon_chooser);
+
+  filter = gtk_file_filter_new ();
+  gtk_file_filter_add_pixbuf_formats (filter);
+  gtk_file_chooser_set_filter (chooser, filter);
+
+  preview = gtk_image_new ();
+  gtk_file_chooser_set_preview_widget (chooser, preview);
+
+  if (self->icon != NULL && g_path_is_absolute (self->icon))
+    gtk_file_chooser_set_filename (chooser, self->icon);
+
+  g_signal_connect (chooser,
+                    "response",
+                    G_CALLBACK (icon_chooser_response_cb),
+                    self);
+
+  g_signal_connect (chooser,
+                    "update-preview",
+                    G_CALLBACK (icon_chooser_update_preview_cb),
+                    preview);
+
+  g_signal_connect (chooser,
+                    "destroy",
+                    G_CALLBACK (icon_chooser_destroy_cb),
+                    self);
+
+  gtk_window_set_destroy_with_parent (GTK_WINDOW (chooser), TRUE);
+  gtk_window_present (GTK_WINDOW (chooser));
+}
+
+static GtkWidget *
+create_icon_button (GpEditor *self)
+{
+  GtkWidget *button;
+  GtkWidget *menu;
+  GtkWidget *item;
+
+  button = gtk_menu_button_new ();
+
+  self->icon_image = gtk_image_new_from_icon_name (FALLBACK_ICON,
+                                                   GTK_ICON_SIZE_DIALOG);
+
+  gtk_image_set_pixel_size (GTK_IMAGE (self->icon_image), 48);
+  gtk_container_add (GTK_CONTAINER (button), self->icon_image);
+  gtk_widget_show (self->icon_image);
+
+  menu = gtk_menu_new ();
+  gtk_menu_button_set_popup (GTK_MENU_BUTTON (button), menu);
+
+  item = gtk_menu_item_new_with_label (_("Choose Icon Name"));
+  gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+  gtk_widget_show (item);
+
+  g_signal_connect (item,
+                    "activate",
+                    G_CALLBACK (choose_icon_name_activate_cb),
+                    self);
+
+  item = gtk_menu_item_new_with_label (_("Choose Icon File"));
+  gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+  gtk_widget_show (item);
+
+  g_signal_connect (item,
+                    "activate",
+                    G_CALLBACK (choose_icon_file_activate_cb),
+                    self);
+
+  return button;
+}
+
+static void
+command_chooser_destroy_cb (GtkWidget *widget,
+                            GpEditor  *self)
+{
+  self->command_chooser = NULL;
+}
+
+static void
+command_chooser_response_cb (GtkFileChooser *chooser,
+                             int             response_id,
+                             GpEditor       *self)
+{
+  if (response_id == GTK_RESPONSE_ACCEPT)
+    {
+      GpEditorType type;
+      char *text;
+      char *uri;
+
+      type = get_editor_type (self);
+      uri = NULL;
+
+      switch (type)
+        {
+          case GP_EDITOR_TYPE_APPLICATION:
+          case GP_EDITOR_TYPE_TERMINAL_APPLICATION:
+            text = gtk_file_chooser_get_filename (chooser);
+            uri = filename_to_exec_uri (text);
+            g_free (text);
+            break;
+
+          case GP_EDITOR_TYPE_DIRECTORY:
+          case GP_EDITOR_TYPE_FILE:
+            uri = gtk_file_chooser_get_uri (chooser);
+            break;
+
+          case GP_EDITOR_TYPE_NONE:
+          default:
+            break;
+        }
+
+      gtk_entry_set_text (GTK_ENTRY (self->command_entry), uri);
+      g_free (uri);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (chooser));
+}
+
+static void
+command_browse_clicked_cb (GtkButton *button,
+                           GpEditor  *self)
+{
+  GtkWidget *toplevel;
+  GtkWindow *parent;
+  GpEditorType type;
+  GtkFileChooserAction action;
+  const char *title;
+  gboolean local_only;
+  GtkFileChooser *chooser;
+
+  if (self->command_chooser != NULL)
+    {
+      gtk_window_present (GTK_WINDOW (self->command_chooser));
+      return;
+    }
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+  parent = GTK_IS_WINDOW (toplevel) ? GTK_WINDOW (toplevel) : NULL;
+
+  type = get_editor_type (self);
+  action = GTK_FILE_CHOOSER_ACTION_OPEN;
+  title = NULL;
+  local_only = TRUE;
+
+  switch (type)
+    {
+      case GP_EDITOR_TYPE_APPLICATION:
+      case GP_EDITOR_TYPE_TERMINAL_APPLICATION:
+        action = GTK_FILE_CHOOSER_ACTION_OPEN;
+        title = _("Choose an application...");
+        break;
+
+      case GP_EDITOR_TYPE_DIRECTORY:
+        action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
+        title = _("Choose a directory...");
+        break;
+
+      case GP_EDITOR_TYPE_FILE:
+        action = GTK_FILE_CHOOSER_ACTION_OPEN;
+        title = _("Choose a file...");
+        local_only = FALSE;
+        break;
+
+      case GP_EDITOR_TYPE_NONE:
+      default:
+        break;
+    }
+
+  self->command_chooser = gtk_file_chooser_dialog_new (title, parent, action,
+                                                       _("_Cancel"),
+                                                       GTK_RESPONSE_CANCEL,
+                                                       _("_Open"),
+                                                       GTK_RESPONSE_ACCEPT,
+                                                       NULL);
+
+  chooser = GTK_FILE_CHOOSER (self->command_chooser);
+  gtk_file_chooser_set_local_only (chooser, local_only);
+
+  g_signal_connect (chooser, "response",
+                    G_CALLBACK (command_chooser_response_cb),
+                    self);
+
+  g_signal_connect (chooser, "destroy",
+                    G_CALLBACK (command_chooser_destroy_cb),
+                    self);
+
+  gtk_window_set_destroy_with_parent (GTK_WINDOW (chooser), TRUE);
+  gtk_window_present (GTK_WINDOW (chooser));
+}
+
+static void
+name_changed_cb (GtkEditable *editable,
+                 GpEditor    *self)
+{
+  g_signal_emit (self, editor_signals[NAME_CHANGED], 0);
+}
+
+static void
+command_changed_cb (GtkEditable *editable,
+                    GpEditor    *self)
+{
+  GpEditorType type;
+
+  type = get_editor_type (self);
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      const char *exec;
+      char *icon_name;
+
+      exec = gp_editor_get_command (self);
+      icon_name = g_path_get_basename (exec);
+
+      if (gtk_icon_theme_has_icon (self->icon_theme, icon_name) &&
+          g_strcmp0 (icon_name, self->icon) != 0)
+        icon_name_changed (self, icon_name);
+
+      g_free (icon_name);
+    }
+
+  g_signal_emit (self, editor_signals[COMMAND_CHANGED], 0);
+}
+
+static void
+comment_changed_cb (GtkEditable *editable,
+                    GpEditor    *self)
+{
+  g_signal_emit (self, editor_signals[COMMENT_CHANGED], 0);
+}
+
+static void
+type_combo_changed_cb (GtkComboBox *combo,
+                       GpEditor    *self)
+{
+  GpEditorType type;
+  const char *text;
+  const char *title;
+  GtkFileChooserAction action;
+  gboolean local_only;
+  char *bold;
+
+  type = get_editor_type (self);
+  text = NULL;
+  title = NULL;
+  action = GTK_FILE_CHOOSER_ACTION_OPEN;
+  local_only = TRUE;
+
+  switch (type)
+    {
+      case GP_EDITOR_TYPE_APPLICATION:
+      case GP_EDITOR_TYPE_TERMINAL_APPLICATION:
+        text = _("Comm_and:");
+        title = _("Choose an application...");
+        action = GTK_FILE_CHOOSER_ACTION_OPEN;
+        break;
+
+      case GP_EDITOR_TYPE_DIRECTORY:
+        text = _("_Location:");
+        title = _("Choose a directory...");
+        action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
+        break;
+
+      case GP_EDITOR_TYPE_FILE:
+        text = _("_Location:");
+        title = _("Choose a file...");
+        action = GTK_FILE_CHOOSER_ACTION_OPEN;
+        local_only = FALSE;
+        break;
+
+      case GP_EDITOR_TYPE_NONE:
+      default:
+        break;
+    }
+
+  bold = g_strdup_printf ("<b>%s</b>", text);
+  gtk_label_set_markup_with_mnemonic (GTK_LABEL (self->command_label), bold);
+  g_free (bold);
+
+  if (self->command_chooser != NULL)
+    {
+      GtkFileChooser *chooser;
+
+      chooser = GTK_FILE_CHOOSER (self->command_chooser);
+
+      gtk_file_chooser_set_action (chooser, action);
+      gtk_file_chooser_set_local_only (chooser, local_only);
+      gtk_window_set_title (GTK_WINDOW (chooser), title);
+    }
+
+  g_signal_emit (self, editor_signals[TYPE_CHANGED], 0);
+}
+
+static gboolean
+type_visible_func (GtkTreeModel *model,
+                   GtkTreeIter  *iter,
+                   gpointer      user_data)
+{
+  GpEditor *self;
+  gboolean visible;
+  GpEditorType active_type;
+  GpEditorType type;
+
+  self = GP_EDITOR (user_data);
+
+  if (!self->edit)
+    return TRUE;
+
+  visible = FALSE;
+  active_type = get_editor_type (self);
+
+  gtk_tree_model_get (model, iter, 1, &type, -1);
+
+  if (active_type == GP_EDITOR_TYPE_APPLICATION ||
+      active_type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      visible = type == GP_EDITOR_TYPE_APPLICATION ||
+                type == GP_EDITOR_TYPE_TERMINAL_APPLICATION;
+    }
+  else if (active_type == GP_EDITOR_TYPE_DIRECTORY)
+    {
+      visible = type == GP_EDITOR_TYPE_DIRECTORY;
+    }
+  else if (active_type == GP_EDITOR_TYPE_FILE)
+    {
+      visible = type == GP_EDITOR_TYPE_FILE;
+    }
+
+  return visible;
+}
+
+static void
+setup_type_combo (GpEditor *self)
+{
+  GtkListStore *store;
+  GtkCellLayout *layout;
+  GtkCellRenderer *renderer;
+  GtkTreeIter iter;
+  guint i;
+
+  store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT);
+  self->type_model = gtk_tree_model_filter_new (GTK_TREE_MODEL (store), NULL);
+
+  gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (self->type_model),
+                                          type_visible_func,
+                                          self,
+                                          NULL);
+
+  layout = GTK_CELL_LAYOUT (self->type_combo);
+  renderer = gtk_cell_renderer_text_new ();
+
+  gtk_cell_layout_pack_start (layout, renderer, TRUE);
+  gtk_cell_layout_set_attributes (layout, renderer, "text", 0, NULL);
+
+  for (i = 0; type_combo_items[i].type != GP_EDITOR_TYPE_NONE; i++)
+    {
+      gtk_list_store_append (store, &iter);
+      gtk_list_store_set (store, &iter,
+                          0, _(type_combo_items[i].name),
+                          1, type_combo_items[i].type,
+                          -1);
+    }
+
+  g_signal_connect (self->type_combo, "changed",
+                    G_CALLBACK (type_combo_changed_cb), self);
+
+  gtk_combo_box_set_model (GTK_COMBO_BOX (self->type_combo), self->type_model);
+  gtk_combo_box_set_active (GTK_COMBO_BOX (self->type_combo), 0);
+  g_object_unref (store);
+}
+
+static GtkWidget *
+label_new_with_mnemonic (const gchar *text)
+{
+  GtkWidget *label;
+  char *bold;
+
+  bold = g_strdup_printf ("<b>%s</b>", text);
+  label = gtk_label_new_with_mnemonic (bold);
+  g_free (bold);
+
+  gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
+  gtk_label_set_xalign (GTK_LABEL (label), 1.0);
+
+  gtk_widget_show (label);
+
+  return label;
+}
+
+static void
+gp_editor_dispose (GObject *object)
+{
+  GpEditor *self;
+
+  self = GP_EDITOR (object);
+
+  g_clear_object (&self->icon_theme);
+
+  g_clear_object (&self->type_model);
+
+  g_clear_pointer (&self->icon_chooser, gtk_widget_destroy);
+  g_clear_pointer (&self->command_chooser, gtk_widget_destroy);
+
+  G_OBJECT_CLASS (gp_editor_parent_class)->dispose (object);
+}
+
+static void
+gp_editor_finalize (GObject *object)
+{
+  GpEditor *self;
+
+  self = GP_EDITOR (object);
+
+  g_clear_pointer (&self->icon, g_free);
+
+  G_OBJECT_CLASS (gp_editor_parent_class)->finalize (object);
+}
+
+static void
+gp_editor_set_property (GObject      *object,
+                        guint         property_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  GpEditor *self;
+
+  self = GP_EDITOR (object);
+
+  switch (property_id)
+    {
+      case PROP_EDIT:
+        self->edit = g_value_get_boolean (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        break;
+    }
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+  editor_properties[PROP_EDIT] =
+    g_param_spec_boolean ("edit",
+                          "edit",
+                          "edit",
+                          FALSE,
+                          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+                          G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, editor_properties);
+}
+
+static void
+install_signals (void)
+{
+  editor_signals[ICON_CHANGED] =
+    g_signal_new ("icon-changed", GP_TYPE_EDITOR, G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  editor_signals[TYPE_CHANGED] =
+    g_signal_new ("type-changed", GP_TYPE_EDITOR, G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  editor_signals[NAME_CHANGED] =
+    g_signal_new ("name-changed", GP_TYPE_EDITOR, G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  editor_signals[COMMAND_CHANGED] =
+    g_signal_new ("command-changed", GP_TYPE_EDITOR, G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  editor_signals[COMMENT_CHANGED] =
+    g_signal_new ("comment-changed", GP_TYPE_EDITOR, G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+gp_editor_class_init (GpEditorClass *self_class)
+{
+  GObjectClass *object_class;
+
+  object_class = G_OBJECT_CLASS (self_class);
+
+  object_class->dispose = gp_editor_dispose;
+  object_class->finalize = gp_editor_finalize;
+  object_class->set_property = gp_editor_set_property;
+
+  install_properties (object_class);
+  install_signals ();
+}
+
+static void
+gp_editor_init (GpEditor *self)
+{
+  GtkWidget *hbox;
+  GtkWidget *grid;
+
+  self->icon_theme = gtk_icon_theme_new ();
+
+  /* Icon */
+  self->icon = NULL;
+  self->icon_button = create_icon_button (self);
+
+  gtk_box_pack_start (GTK_BOX (self), self->icon_button, FALSE, FALSE, 0);
+  gtk_widget_set_valign (self->icon_button, GTK_ALIGN_START);
+  gtk_widget_show (self->icon_button);
+
+  /* Grid */
+  grid = gtk_grid_new ();
+  gtk_box_pack_end (GTK_BOX (self), grid, TRUE, TRUE, 0);
+  gtk_widget_show (grid);
+
+  gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
+  gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
+
+  /* Type */
+  self->type_label = label_new_with_mnemonic (_("_Type:"));
+  self->type_combo = gtk_combo_box_new ();
+  gtk_grid_attach (GTK_GRID (grid), self->type_label, 0, 0, 1, 1);
+  gtk_grid_attach (GTK_GRID (grid), self->type_combo, 1, 0, 1, 1);
+  gtk_label_set_mnemonic_widget (GTK_LABEL (self->type_label), self->type_combo);
+  gtk_widget_set_hexpand (self->type_combo, TRUE);
+  gtk_widget_show (self->type_combo);
+
+  /* Name */
+  self->name_label = label_new_with_mnemonic (_("_Name:"));
+  self->name_entry = gtk_entry_new ();
+  gtk_grid_attach (GTK_GRID (grid), self->name_label, 0, 1, 1, 1);
+  gtk_grid_attach (GTK_GRID (grid), self->name_entry, 1, 1, 1, 1);
+  gtk_label_set_mnemonic_widget (GTK_LABEL (self->name_label), self->name_entry);
+  gtk_widget_set_hexpand (self->name_entry, TRUE);
+  gtk_widget_show (self->name_entry);
+
+  g_signal_connect (self->name_entry, "changed",
+                    G_CALLBACK (name_changed_cb), self);
+
+  gtk_widget_grab_focus (self->name_entry);
+
+  /* Command */
+  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12);
+  gtk_widget_set_hexpand (hbox, TRUE);
+  gtk_widget_show (hbox);
+
+  self->command_label = label_new_with_mnemonic (_("Comm_and:"));
+
+  self->command_entry = gtk_entry_new ();
+  gtk_box_pack_start (GTK_BOX (hbox), self->command_entry, TRUE, TRUE, 0);
+  gtk_widget_show (self->command_entry);
+
+  self->command_browse = gtk_button_new_with_mnemonic (_("_Browse..."));
+  gtk_box_pack_start (GTK_BOX (hbox), self->command_browse, FALSE, FALSE, 0);
+  gtk_widget_show (self->command_browse);
+
+  gtk_grid_attach (GTK_GRID (grid), self->command_label, 0, 2, 1, 1);
+  gtk_grid_attach (GTK_GRID (grid), hbox, 1, 2, 1, 1);
+
+  g_signal_connect (self->command_browse, "clicked",
+                    G_CALLBACK (command_browse_clicked_cb), self);
+
+  g_signal_connect (self->command_entry, "changed",
+                    G_CALLBACK (command_changed_cb), self);
+
+  /* Comment */
+  self->comment_label = label_new_with_mnemonic (_("Co_mment:"));
+  self->comment_entry = gtk_entry_new ();
+  gtk_grid_attach (GTK_GRID (grid), self->comment_label, 0, 3, 1, 1);
+  gtk_grid_attach (GTK_GRID (grid), self->comment_entry, 1, 3, 1, 1);
+  gtk_label_set_mnemonic_widget (GTK_LABEL (self->comment_label), self->comment_entry);
+  gtk_widget_set_hexpand (self->comment_entry, TRUE);
+  gtk_widget_show (self->comment_entry);
+
+  g_signal_connect (self->comment_entry, "changed",
+                    G_CALLBACK (comment_changed_cb), self);
+
+  setup_type_combo (self);
+}
+
+GtkWidget *
+gp_editor_new (gboolean edit)
+{
+  return g_object_new (GP_TYPE_EDITOR,
+                       "edit", edit,
+                       "orientation", GTK_ORIENTATION_HORIZONTAL,
+                       "spacing", 12,
+                       NULL);
+}
+
+const char *
+gp_editor_get_icon (GpEditor *self)
+{
+  if (self->icon != NULL)
+    return self->icon;
+
+  return FALLBACK_ICON;
+}
+
+void
+gp_editor_set_icon (GpEditor   *self,
+                    const char *icon)
+{
+  g_clear_pointer (&self->icon_chooser, gtk_widget_destroy);
+
+  if (g_strcmp0 (self->icon, icon) == 0)
+    return;
+
+  g_clear_pointer (&self->icon, g_free);
+  self->icon = g_strdup (icon);
+
+  if (self->icon != NULL)
+    {
+      char *p;
+
+      /* Work around a common mistake in desktop files */
+      if ((p = strrchr (self->icon, '.')) != NULL &&
+          (strcmp (p, ".png") == 0 ||
+           strcmp (p, ".xpm") == 0 ||
+           strcmp (p, ".svg") == 0))
+        *p = '\0';
+    }
+
+  update_icon_image (self);
+}
+
+GpEditorType
+gp_editor_get_editor_type (GpEditor *self)
+{
+  return get_editor_type (self);
+}
+
+void
+gp_editor_set_editor_type (GpEditor     *self,
+                           GpEditorType  type)
+{
+  GtkTreeIter iter;
+
+  gtk_tree_model_get_iter_first (self->type_model, &iter);
+
+  do
+    {
+      GpEditorType tmp;
+
+      gtk_tree_model_get (self->type_model, &iter, 1, &tmp, -1);
+
+      if (type != tmp)
+        continue;
+
+      gtk_combo_box_set_active_iter (GTK_COMBO_BOX (self->type_combo), &iter);
+    }
+  while (gtk_tree_model_iter_next (self->type_model, &iter));
+
+  gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (self->type_model));
+}
+
+const char *
+gp_editor_get_name (GpEditor *self)
+{
+  return gtk_entry_get_text (GTK_ENTRY (self->name_entry));
+}
+
+void
+gp_editor_set_name (GpEditor   *self,
+                    const char *name)
+{
+  if (name == NULL)
+    name = "";
+
+  gtk_entry_set_text (GTK_ENTRY (self->name_entry), name);
+}
+
+const char *
+gp_editor_get_command (GpEditor *self)
+{
+  return gtk_entry_get_text (GTK_ENTRY (self->command_entry));
+}
+
+void
+gp_editor_set_command (GpEditor   *self,
+                       const char *command)
+{
+  if (command == NULL)
+    command = "";
+
+  gtk_entry_set_text (GTK_ENTRY (self->command_entry), command);
+}
+
+const char *
+gp_editor_get_comment (GpEditor *self)
+{
+  return gtk_entry_get_text (GTK_ENTRY (self->comment_entry));
+}
+
+void
+gp_editor_set_comment (GpEditor   *self,
+                       const char *comment)
+{
+  if (comment == NULL)
+    comment = "";
+
+  gtk_entry_set_text (GTK_ENTRY (self->comment_entry), comment);
+}
diff --git a/modules/launcher/gp-editor.h b/modules/launcher/gp-editor.h
new file mode 100644
index 000000000..d2bddf803
--- /dev/null
+++ b/modules/launcher/gp-editor.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_EDITOR_H
+#define GP_EDITOR_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  GP_EDITOR_TYPE_NONE,
+  GP_EDITOR_TYPE_APPLICATION,
+  GP_EDITOR_TYPE_TERMINAL_APPLICATION,
+  GP_EDITOR_TYPE_DIRECTORY,
+  GP_EDITOR_TYPE_FILE,
+} GpEditorType;
+
+#define GP_TYPE_EDITOR (gp_editor_get_type ())
+G_DECLARE_FINAL_TYPE (GpEditor, gp_editor, GP, EDITOR, GtkBox)
+
+GtkWidget    *gp_editor_new             (gboolean      edit);
+
+const char   *gp_editor_get_icon        (GpEditor     *self);
+
+void          gp_editor_set_icon        (GpEditor     *self,
+                                         const char   *icon);
+
+GpEditorType  gp_editor_get_editor_type (GpEditor     *self);
+
+void          gp_editor_set_editor_type (GpEditor     *self,
+                                         GpEditorType  type);
+
+const char   *gp_editor_get_name        (GpEditor     *self);
+
+void          gp_editor_set_name        (GpEditor     *self,
+                                         const char   *name);
+
+const char   *gp_editor_get_command     (GpEditor     *self);
+
+void          gp_editor_set_command     (GpEditor     *self,
+                                         const char   *command);
+
+const char   *gp_editor_get_comment     (GpEditor     *self);
+
+void          gp_editor_set_comment     (GpEditor     *self,
+                                         const char   *comment);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-icon-name-chooser.c b/modules/launcher/gp-icon-name-chooser.c
new file mode 100644
index 000000000..ba1f813be
--- /dev/null
+++ b/modules/launcher/gp-icon-name-chooser.c
@@ -0,0 +1,933 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "gp-icon-name-chooser.h"
+
+#include <glib/gi18n-lib.h>
+
+struct _GpIconNameChooser
+{
+  GtkWindow              parent;
+
+  GtkIconTheme          *icon_theme;
+
+  GtkWidget             *header_bar;
+  GtkWidget             *search_button;
+  GtkWidget             *select_button;
+
+  GtkWidget             *search_bar;
+  GtkWidget             *search_entry;
+
+  GtkWidget             *context_list;
+
+  GtkListStore          *icon_store;
+  GtkTreeModelFilter    *icon_filter;
+  GtkWidget             *icon_view;
+  GtkCellRendererPixbuf *pixbuf_cell;
+  GtkCellRendererText   *name_cell;
+
+  GtkWidget             *standard_button;
+
+  char                  *selected_context;
+  char                  *selected_icon;
+};
+
+enum
+{
+  ICON_SELECTED,
+
+  CLOSE,
+
+  LAST_SIGNAL
+};
+
+static guint chooser_signals[LAST_SIGNAL] = { 0 };
+
+G_DEFINE_TYPE (GpIconNameChooser, gp_icon_name_chooser, GTK_TYPE_WINDOW)
+
+typedef struct
+{
+  const char  *name;
+  const char  *directory;
+  const char **icons;
+} GpStandardIcons;
+
+static const char *action_icons[] =
+{
+  "address-book-new",
+  "application-exit",
+  "appointment-new",
+  "call-start",
+  "call-stop",
+  "contact-new",
+  "document-new",
+  "document-open",
+  "document-open-recent",
+  "document-page-setup",
+  "document-print",
+  "document-print-preview",
+  "document-properties",
+  "document-revert",
+  "document-save",
+  "document-save-as",
+  "document-send",
+  "edit-clear",
+  "edit-copy",
+  "edit-cut",
+  "edit-delete",
+  "edit-find",
+  "edit-find-replace",
+  "edit-paste",
+  "edit-redo",
+  "edit-select-all",
+  "edit-undo",
+  "folder-new",
+  "format-indent-less",
+  "format-indent-more",
+  "format-justify-center",
+  "format-justify-fill",
+  "format-justify-left",
+  "format-justify-right",
+  "format-text-direction-ltr",
+  "format-text-direction-rtl",
+  "format-text-bold",
+  "format-text-italic",
+  "format-text-underline",
+  "format-text-strikethrough",
+  "go-bottom",
+  "go-down",
+  "go-first",
+  "go-home",
+  "go-jump",
+  "go-last",
+  "go-next",
+  "go-previous",
+  "go-top",
+  "go-up",
+  "help-about",
+  "help-contents",
+  "help-faq",
+  "insert-image",
+  "insert-link",
+  "insert-object",
+  "insert-text",
+  "list-add",
+  "list-remove",
+  "mail-forward",
+  "mail-mark-important",
+  "mail-mark-junk",
+  "mail-mark-notjunk",
+  "mail-mark-read",
+  "mail-mark-unread",
+  "mail-message-new",
+  "mail-reply-all",
+  "mail-reply-sender",
+  "mail-send",
+  "mail-send-receive",
+  "media-eject",
+  "media-playback-pause",
+  "media-playback-start",
+  "media-playback-stop",
+  "media-record",
+  "media-seek-backward",
+  "media-seek-forward",
+  "media-skip-backward",
+  "media-skip-forward",
+  "object-flip-horizontal",
+  "object-flip-vertical",
+  "object-rotate-left",
+  "object-rotate-right",
+  "process-stop",
+  "system-lock-screen",
+  "system-log-out",
+  "system-run",
+  "system-search",
+  "system-reboot",
+  "system-shutdown",
+  "tools-check-spelling",
+  "view-fullscreen",
+  "view-refresh",
+  "view-restore",
+  "view-sort-ascending",
+  "view-sort-descending",
+  "window-close",
+  "window-new",
+  "zoom-fit-best",
+  "zoom-in",
+  "zoom-original",
+  "zoom-out",
+  NULL
+};
+
+static const char *animation_icons[] =
+{
+  "process-working",
+  NULL
+};
+
+static const char *application_icons[] =
+{
+  "accessories-calculator",
+  "accessories-character-map",
+  "accessories-dictionary",
+  "accessories-text-editor",
+  "help-browser",
+  "multimedia-volume-control",
+  "preferences-desktop-accessibility",
+  "preferences-desktop-font",
+  "preferences-desktop-keyboard",
+  "preferences-desktop-locale",
+  "preferences-desktop-multimedia",
+  "preferences-desktop-screensaver",
+  "preferences-desktop-theme",
+  "preferences-desktop-wallpaper",
+  "system-file-manager",
+  "system-software-install",
+  "system-software-update",
+  "utilities-system-monitor",
+  "utilities-terminal",
+  NULL
+};
+
+static const char *category_icons[] =
+{
+  "applications-accessories",
+  "applications-development",
+  "applications-engineering",
+  "applications-games",
+  "applications-graphics",
+  "applications-internet",
+  "applications-multimedia",
+  "applications-office",
+  "applications-other",
+  "applications-science",
+  "applications-system",
+  "applications-utilities",
+  "preferences-desktop",
+  "preferences-desktop-peripherals",
+  "preferences-desktop-personal",
+  "preferences-other",
+  "preferences-system",
+  "preferences-system-network",
+  "system-help",
+  NULL
+};
+
+static const char *device_icons[] =
+{
+  "audio-card",
+  "audio-input-microphone",
+  "battery",
+  "camera-photo",
+  "camera-video",
+  "camera-web",
+  "computer",
+  "drive-harddisk",
+  "drive-optical",
+  "drive-removable-media",
+  "input-gaming",
+  "input-keyboard",
+  "input-mouse",
+  "input-tablet",
+  "media-flash",
+  "media-floppy",
+  "media-optical",
+  "media-tape",
+  "modem",
+  "multimedia-player",
+  "network-wired",
+  "network-wireless",
+  "pda",
+  "phone",
+  "printer",
+  "scanner",
+  "video-display",
+  NULL
+};
+
+static const char *emblem_icons[] =
+{
+  "emblem-default",
+  "emblem-documents",
+  "emblem-downloads",
+  "emblem-favorite",
+  "emblem-important",
+  "emblem-mail",
+  "emblem-photos",
+  "emblem-readonly",
+  "emblem-shared",
+  "emblem-symbolic-link",
+  "emblem-synchronized",
+  "emblem-system",
+  "emblem-unreadable",
+  NULL
+};
+
+static const char *emotion_icons[] =
+{
+  "face-angel",
+  "face-angry",
+  "face-cool",
+  "face-crying",
+  "face-devilish",
+  "face-embarrassed",
+  "face-kiss",
+  "face-laugh",
+  "face-monkey",
+  "face-plain",
+  "face-raspberry",
+  "face-sad",
+  "face-sick",
+  "face-smile",
+  "face-smile-big",
+  "face-smirk",
+  "face-surprise",
+  "face-tired",
+  "face-uncertain",
+  "face-wink",
+  "face-worried",
+  NULL
+};
+
+static const char *international_icons[] =
+{
+  NULL
+};
+
+static const char *mime_type_icons[] =
+{
+  "application-x-executable",
+  "audio-x-generic",
+  "font-x-generic",
+  "image-x-generic",
+  "package-x-generic",
+  "text-html",
+  "text-x-generic",
+  "text-x-generic-template",
+  "text-x-script",
+  "video-x-generic",
+  "x-office-address-book",
+  "x-office-calendar",
+  "x-office-document",
+  "x-office-presentation",
+  "x-office-spreadsheet",
+  NULL
+};
+
+static const char *place_icons[] =
+{
+  "folder",
+  "folder-remote",
+  "network-server",
+  "network-workgroup",
+  "start-here",
+  "user-bookmarks",
+  "user-desktop",
+  "user-home",
+  "user-trash",
+  NULL
+};
+
+static const char *status_icons[] =
+{
+  "appointment-missed",
+  "appointment-soon",
+  "audio-volume-high",
+  "audio-volume-low",
+  "audio-volume-medium",
+  "audio-volume-muted",
+  "battery-caution",
+  "battery-low",
+  "dialog-error",
+  "dialog-information",
+  "dialog-password",
+  "dialog-question",
+  "dialog-warning",
+  "folder-drag-accept",
+  "folder-open",
+  "folder-visiting",
+  "image-loading",
+  "image-missing",
+  "mail-attachment",
+  "mail-unread",
+  "mail-read",
+  "mail-replied",
+  "mail-signed",
+  "mail-signed-verified",
+  "media-playlist-repeat",
+  "media-playlist-shuffle",
+  "network-error",
+  "network-idle",
+  "network-offline",
+  "network-receive",
+  "network-transmit",
+  "network-transmit-receive",
+  "printer-error",
+  "printer-printing",
+  "security-high",
+  "security-medium",
+  "security-low",
+  "software-update-available",
+  "software-update-urgent",
+  "sync-error",
+  "sync-synchronizing",
+  "task-due",
+  "task-past-due",
+  "user-available",
+  "user-away",
+  "user-idle",
+  "user-offline",
+  "user-trash-full",
+  "weather-clear",
+  "weather-clear-night",
+  "weather-few-clouds",
+  "weather-few-clouds-night",
+  "weather-fog",
+  "weather-overcast",
+  "weather-severe-alert",
+  "weather-showers",
+  "weather-showers-scattered",
+  "weather-snow",
+  "weather-storm",
+  NULL
+};
+
+static GpStandardIcons standard_icon_names[] =
+{
+  { "Actions", "actions", action_icons },
+  { "Animations", "animations", animation_icons },
+  { "Applications", "apps", application_icons },
+  { "Categories", "categories", category_icons },
+  { "Devices", "devices", device_icons },
+  { "Emblems", "emblems", emblem_icons },
+  { "Emotes", "emotes", emotion_icons },
+  { "International", "intl", international_icons },
+  { "MimeTypes", "mimetypes", mime_type_icons },
+  { "Places", "places", place_icons },
+  { "Status", "status", status_icons },
+  { NULL }
+};
+
+static gboolean
+is_standard_icon_name (const char *icon_name,
+                       const char *context)
+{
+  int i;
+
+  for (i = 0; standard_icon_names[i].name != NULL; i++)
+    {
+      int j;
+
+      if (g_strcmp0 (context, standard_icon_names[i].name) != 0)
+        continue;
+
+      for (j = 0; standard_icon_names[i].icons[j] != NULL; j++)
+        {
+          if (g_strcmp0 (icon_name, standard_icon_names[i].icons[j]) == 0)
+            return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static GtkWidget *
+create_context_row (const char *context,
+                    const char *title,
+                    gboolean    standard)
+{
+  GtkWidget *row;
+  GtkStyleContext *style;
+  GtkWidget *label;
+
+  row = gtk_list_box_row_new ();
+  gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
+  g_object_set_data_full (G_OBJECT (row), "context", g_strdup (context), g_free);
+  g_object_set_data (G_OBJECT (row), "standard", GUINT_TO_POINTER (standard));
+  gtk_widget_show (row);
+
+  style = gtk_widget_get_style_context (row);
+  gtk_style_context_add_class (style, "context-row");
+
+  label = gtk_label_new (title);
+  gtk_label_set_xalign (GTK_LABEL (label), .0);
+  gtk_container_add (GTK_CONTAINER (row), label);
+  gtk_widget_show (label);
+
+  return row;
+}
+
+static void
+load_icon_names (GpIconNameChooser *self)
+{
+  GtkWidget *row;
+  GList *contexts;
+  GList *l1;
+
+  row = create_context_row ("All", _("All"), TRUE);
+  gtk_list_box_prepend (GTK_LIST_BOX (self->context_list), row);
+  gtk_list_box_select_row (GTK_LIST_BOX (self->context_list),
+                           GTK_LIST_BOX_ROW (row));
+
+  contexts = gtk_icon_theme_list_contexts (self->icon_theme);
+
+  for (l1 = contexts; l1 != NULL; l1 = l1->next)
+    {
+      const char *context;
+      gboolean standard;
+      int i;
+      GList *icons;
+      GList *l2;
+
+      context = l1->data;
+
+      standard = FALSE;
+      for (i = 0; standard_icon_names[i].name != NULL; i++)
+        {
+          if (g_strcmp0 (context, standard_icon_names[i].name) == 0)
+            {
+              standard = TRUE;
+              break;
+            }
+        }
+
+      row = create_context_row (context, _(context), standard);
+      gtk_list_box_prepend (GTK_LIST_BOX (self->context_list), row);
+
+      icons = gtk_icon_theme_list_icons (self->icon_theme, context);
+
+      for (l2 = icons; l2 != NULL; l2 = l2->next)
+        {
+          const char *icon_name;
+
+          icon_name = l2->data;
+
+          standard = is_standard_icon_name (icon_name, context);
+
+          gtk_list_store_insert_with_values (self->icon_store,
+                                             NULL,
+                                             -1,
+                                             0, context,
+                                             1, icon_name,
+                                             2, standard,
+                                             -1);
+        }
+
+      g_list_free_full (icons, g_free);
+    }
+
+  g_list_free_full (contexts, g_free);
+}
+
+static void
+cancel_button_clicked_cb (GtkButton         *button,
+                          GpIconNameChooser *self)
+{
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+select_button_clicked_cb (GtkButton         *button,
+                          GpIconNameChooser *self)
+{
+  g_signal_emit (self, chooser_signals[ICON_SELECTED], 0, self->selected_icon);
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+search_entry_search_changed_cb (GtkSearchEntry    *entry,
+                                GpIconNameChooser *self)
+{
+  gtk_icon_view_unselect_all (GTK_ICON_VIEW (self->icon_view));
+  gtk_tree_model_filter_refilter (self->icon_filter);
+}
+
+static void
+context_list_row_selected_cb (GtkListBox        *box,
+                              GtkListBoxRow     *row,
+                              GpIconNameChooser *self)
+{
+  const char *context;
+
+  if (row != NULL)
+    context = g_object_get_data (G_OBJECT (row), "context");
+  else
+    context = "All";
+
+  if (g_strcmp0 (self->selected_context, context) == 0)
+    return;
+
+  g_clear_pointer (&self->selected_context, g_free);
+  self->selected_context = g_strdup (context);
+
+  gtk_icon_view_unselect_all (GTK_ICON_VIEW (self->icon_view));
+  gtk_tree_model_filter_refilter (self->icon_filter);
+}
+
+static void
+icon_view_item_activated_cb (GtkIconView       *iconview,
+                             GtkTreePath       *path,
+                             GpIconNameChooser *self)
+{
+  select_button_clicked_cb (GTK_BUTTON (self->select_button), self);
+}
+
+static void
+icon_view_selection_changed_cb (GtkIconView       *icon_view,
+                                GpIconNameChooser *self)
+{
+  GList *selected_items;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  char *icon_name;
+
+  selected_items = gtk_icon_view_get_selected_items (icon_view);
+
+  if (selected_items == NULL)
+    {
+      gtk_header_bar_set_subtitle (GTK_HEADER_BAR (self->header_bar), NULL);
+      gtk_widget_set_sensitive (self->select_button, FALSE);
+
+      g_clear_pointer (&self->selected_icon, g_free);
+      return;
+    }
+
+  model = GTK_TREE_MODEL (self->icon_filter);
+
+  gtk_tree_model_get_iter (model, &iter, selected_items->data);
+  gtk_tree_model_get (model, &iter, 1, &icon_name, -1);
+
+  g_list_free_full (selected_items, (GDestroyNotify) gtk_tree_path_free);
+
+  gtk_header_bar_set_subtitle (GTK_HEADER_BAR (self->header_bar), icon_name);
+  gtk_widget_set_sensitive (self->select_button, icon_name != NULL);
+
+  g_clear_pointer (&self->selected_icon, g_free);
+  self->selected_icon = icon_name;
+}
+
+static void
+standard_check_button_toggled_cb (GtkToggleButton   *toggle_button,
+                                  GpIconNameChooser *self)
+{
+  gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->context_list));
+  gtk_tree_model_filter_refilter (self->icon_filter);
+}
+
+static void
+close_cb (GpIconNameChooser *self,
+          gpointer           user_data)
+{
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static gboolean
+key_press_event_cb (GtkWidget    *window,
+                    GdkEvent     *event,
+                    GtkSearchBar *search_bar)
+{
+  return gtk_search_bar_handle_event (search_bar, event);
+}
+
+static gboolean
+filter_contexts_func (GtkListBoxRow *row,
+                      gpointer       user_data)
+{
+  GpIconNameChooser *self;
+
+  self = GP_ICON_NAME_CHOOSER (user_data);
+
+  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->standard_button)))
+    return TRUE;
+
+  return GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "standard"));
+}
+
+static int
+sort_contexts_func (GtkListBoxRow *row1,
+                    GtkListBoxRow *row2,
+                    gpointer       user_data)
+{
+  const char *context1;
+  const char *context2;
+
+  context1 = g_object_get_data (G_OBJECT (row1), "context");
+  context2 = g_object_get_data (G_OBJECT (row2), "context");
+
+  if (g_strcmp0 (context1, "All") == 0)
+    return -1;
+  else if (g_strcmp0 (context2, "All") == 0)
+    return 1;
+
+  return g_strcmp0 (context1, context2);
+}
+
+static gboolean
+icon_visible_func (GtkTreeModel *model,
+                   GtkTreeIter  *iter,
+                   gpointer      user_data)
+
+{
+  GpIconNameChooser *self;
+  char *context;
+  char *icon_name;
+  gboolean standard;
+  gboolean visible;
+
+  self = GP_ICON_NAME_CHOOSER (user_data);
+
+  gtk_tree_model_get (model, iter,
+                      0, &context,
+                      1, &icon_name,
+                      2, &standard,
+                      -1);
+
+  if (icon_name == NULL)
+    {
+      visible = FALSE;
+    }
+  else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->standard_button)) &&
+           !standard)
+    {
+      visible = FALSE;
+    }
+  else if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->search_button)))
+    {
+      const char *search_text;
+
+      search_text = gtk_entry_get_text (GTK_ENTRY (self->search_entry));
+
+      visible = (g_strcmp0 (self->selected_context, "All") == 0 ||
+                 g_strcmp0 (self->selected_context, context) == 0) &&
+                strstr (icon_name, search_text) != NULL;
+    }
+  else
+    {
+      visible = g_strcmp0 (self->selected_context, "All") == 0 ||
+                g_strcmp0 (self->selected_context, context) == 0;
+    }
+
+  g_free (context);
+  g_free (icon_name);
+
+  return visible;
+}
+
+static void
+gp_icon_name_chooser_dispose (GObject *object)
+{
+  GpIconNameChooser *self;
+
+  self = GP_ICON_NAME_CHOOSER (object);
+
+  g_clear_object (&self->icon_theme);
+
+  G_OBJECT_CLASS (gp_icon_name_chooser_parent_class)->dispose (object);
+}
+
+static void
+gp_icon_name_chooser_finalize (GObject *object)
+{
+  GpIconNameChooser *self;
+
+  self = GP_ICON_NAME_CHOOSER (object);
+
+  g_clear_pointer (&self->selected_context, g_free);
+  g_clear_pointer (&self->selected_icon, g_free);
+
+  G_OBJECT_CLASS (gp_icon_name_chooser_parent_class)->finalize (object);
+}
+
+static void
+install_signals (void)
+{
+  chooser_signals[ICON_SELECTED] =
+    g_signal_new ("icon-selected",
+                  GP_TYPE_ICON_NAME_CHOOSER,
+                  0,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING);
+
+  chooser_signals[CLOSE] =
+    g_signal_new ("close",
+                  GP_TYPE_ICON_NAME_CHOOSER,
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  0);
+}
+
+static void
+gp_icon_name_chooser_class_init (GpIconNameChooserClass *self_class)
+{
+  GObjectClass *object_class;
+  GtkWidgetClass *widget_class;
+  GtkBindingSet *binding_set;
+  const char *resource_name;
+
+  object_class = G_OBJECT_CLASS (self_class);
+  widget_class = GTK_WIDGET_CLASS (self_class);
+
+  object_class->dispose = gp_icon_name_chooser_dispose;
+  object_class->finalize = gp_icon_name_chooser_finalize;
+
+  install_signals ();
+
+  binding_set = gtk_binding_set_by_class (widget_class);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0);
+
+  resource_name = GRESOURCE_PREFIX "/gp-icon-name-chooser.ui";
+  gtk_widget_class_set_template_from_resource (widget_class, resource_name);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, header_bar);
+
+  gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, search_button);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, select_button);
+  gtk_widget_class_bind_template_callback (widget_class, select_button_clicked_cb);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, search_bar);
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, search_entry);
+  gtk_widget_class_bind_template_callback (widget_class, search_entry_search_changed_cb);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, context_list);
+  gtk_widget_class_bind_template_callback (widget_class, context_list_row_selected_cb);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, icon_store);
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, icon_filter);
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, icon_view);
+  gtk_widget_class_bind_template_callback (widget_class, icon_view_item_activated_cb);
+  gtk_widget_class_bind_template_callback (widget_class, icon_view_selection_changed_cb);
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, pixbuf_cell);
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, name_cell);
+
+  gtk_widget_class_bind_template_child (widget_class, GpIconNameChooser, standard_button);
+  gtk_widget_class_bind_template_callback (widget_class, standard_check_button_toggled_cb);
+}
+
+static void
+gp_icon_name_chooser_init (GpIconNameChooser *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->icon_theme = gtk_icon_theme_new ();
+
+  g_object_bind_property (self->search_button,
+                          "active",
+                          self->search_bar,
+                          "search-mode-enabled",
+                          G_BINDING_BIDIRECTIONAL);
+
+  g_signal_connect (self, "close", G_CALLBACK (close_cb), NULL);
+
+  g_signal_connect (self,
+                    "key-press-event",
+                    G_CALLBACK (key_press_event_cb),
+                    self->search_bar);
+
+  gtk_list_box_set_filter_func (GTK_LIST_BOX (self->context_list),
+                                filter_contexts_func,
+                                self,
+                                NULL);
+
+  gtk_list_box_set_sort_func (GTK_LIST_BOX (self->context_list),
+                              sort_contexts_func,
+                              self,
+                              NULL);
+
+  gtk_tree_model_filter_set_visible_func (self->icon_filter,
+                                          icon_visible_func,
+                                          self,
+                                          NULL);
+
+  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (self->icon_store),
+                                        1,
+                                        GTK_SORT_ASCENDING);
+
+  g_object_set (self->name_cell, "xalign", 0.5, NULL);
+
+  load_icon_names (self);
+}
+
+GtkWidget *
+gp_icon_name_chooser_new (void)
+{
+  return g_object_new (GP_TYPE_ICON_NAME_CHOOSER, NULL);
+}
+
+void
+gp_icon_name_chooser_set_icon_name (GpIconNameChooser *self,
+                                    const char        *icon_name)
+{
+  GtkTreeModel *model;
+  gboolean valid;
+  GtkTreeIter iter;
+  GtkTreePath *path;
+
+  if (!gtk_icon_theme_has_icon (self->icon_theme, icon_name))
+    return;
+
+  g_clear_pointer (&self->selected_icon, g_free);
+  self->selected_icon = g_strdup (icon_name);
+
+  gtk_header_bar_set_subtitle (GTK_HEADER_BAR (self->header_bar), self->selected_icon);
+  gtk_widget_set_sensitive (self->select_button, self->selected_icon != NULL);
+
+  model = GTK_TREE_MODEL (self->icon_filter);
+  valid = gtk_tree_model_get_iter_first (model, &iter);
+  path = NULL;
+
+  while (valid)
+    {
+      char *tmp;
+
+      gtk_tree_model_get (model, &iter, 1, &tmp, -1);
+
+      if (g_strcmp0 (self->selected_icon, tmp) == 0)
+        {
+          path = gtk_tree_model_get_path (model, &iter);
+          g_free (tmp);
+          break;
+        }
+
+      valid = gtk_tree_model_iter_next (model, &iter);
+      g_free (tmp);
+    }
+
+  if (path == NULL)
+    return;
+
+  gtk_icon_view_select_path (GTK_ICON_VIEW (self->icon_view), path);
+  gtk_icon_view_scroll_to_path (GTK_ICON_VIEW (self->icon_view),
+                                path,
+                                TRUE,
+                                0.5,
+                                0.5);
+
+  gtk_tree_path_free (path);
+}
diff --git a/modules/launcher/gp-icon-name-chooser.h b/modules/launcher/gp-icon-name-chooser.h
new file mode 100644
index 000000000..df33797a2
--- /dev/null
+++ b/modules/launcher/gp-icon-name-chooser.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_ICON_NAME_CHOOSER_H
+#define GP_ICON_NAME_CHOOSER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GP_TYPE_ICON_NAME_CHOOSER (gp_icon_name_chooser_get_type ())
+G_DECLARE_FINAL_TYPE (GpIconNameChooser, gp_icon_name_chooser,
+                      GP, ICON_NAME_CHOOSER, GtkWindow)
+
+GtkWidget *gp_icon_name_chooser_new           (void);
+
+void       gp_icon_name_chooser_set_icon_name (GpIconNameChooser *self,
+                                               const char        *icon_name);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-icon-name-chooser.ui b/modules/launcher/gp-icon-name-chooser.ui
new file mode 100644
index 000000000..915ef6f4a
--- /dev/null
+++ b/modules/launcher/gp-icon-name-chooser.ui
@@ -0,0 +1,295 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.2 -->
+<interface>
+  <requires lib="gtk+" version="3.20"/>
+  <object class="GtkListStore" id="icon_store">
+    <columns>
+      <!-- column-name context -->
+      <column type="gchararray"/>
+      <!-- column-name icon-name -->
+      <column type="gchararray"/>
+      <!-- column-name standard -->
+      <column type="gboolean"/>
+    </columns>
+  </object>
+  <object class="GtkTreeModelFilter" id="icon_filter">
+    <property name="child_model">icon_store</property>
+  </object>
+  <template class="GpIconNameChooser" parent="GtkWindow">
+    <property name="can_focus">False</property>
+    <property name="default_width">600</property>
+    <property name="default_height">400</property>
+    <property name="type_hint">dialog</property>
+    <child type="titlebar">
+      <object class="GtkHeaderBar" id="header_bar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="title" translatable="yes">Select Icon Name</property>
+        <child>
+          <object class="GtkButton" id="cancel_button">
+            <property name="label" translatable="yes">Cancel</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <signal name="clicked" handler="cancel_button_clicked_cb" object="GpIconNameChooser" 
swapped="no"/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="select_button">
+            <property name="label" translatable="yes">Select</property>
+            <property name="visible">True</property>
+            <property name="sensitive">False</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <signal name="clicked" handler="select_button_clicked_cb" object="GpIconNameChooser" 
swapped="no"/>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkToggleButton" id="search_button">
+            <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="icon_name">edit-find-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack_type">end</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkSearchBar" id="search_bar">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkSearchEntry" id="search_entry">
+                <property name="width_request">400</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="primary_icon_name">edit-find-symbolic</property>
+                <property name="primary_icon_activatable">False</property>
+                <property name="primary_icon_sensitive">False</property>
+                <signal name="search-changed" handler="search_entry_search_changed_cb" 
object="GpIconNameChooser" swapped="no"/>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</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="margin_left">12</property>
+            <property name="margin_right">12</property>
+            <property name="margin_top">12</property>
+            <property name="margin_bottom">12</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Contexts:</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</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="hscrollbar_policy">never</property>
+                        <property name="shadow_type">in</property>
+                        <child>
+                          <object class="GtkViewport">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <child>
+                              <object class="GtkListBox" id="context_list">
+                                <property name="width_request">130</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">False</property>
+                                <property name="activate_on_single_click">False</property>
+                                <signal name="row-selected" handler="context_list_row_selected_cb" 
object="GpIconNameChooser" swapped="no"/>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="view"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">True</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="GtkSeparator">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="label" translatable="yes">Icon Names:</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</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="hscrollbar_policy">never</property>
+                        <property name="shadow_type">in</property>
+                        <child>
+                          <object class="GtkIconView" id="icon_view">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="margin">6</property>
+                            <property name="model">icon_filter</property>
+                            <property name="columns">4</property>
+                            <property name="item_width">128</property>
+                            <signal name="item-activated" handler="icon_view_item_activated_cb" 
object="GpIconNameChooser" swapped="no"/>
+                            <signal name="selection-changed" handler="icon_view_selection_changed_cb" 
object="GpIconNameChooser" swapped="no"/>
+                            <child>
+                              <object class="GtkCellRendererPixbuf" id="pixbuf_cell">
+                                <property name="xpad">6</property>
+                                <property name="ypad">6</property>
+                                <property name="stock_size">6</property>
+                              </object>
+                              <attributes>
+                                <attribute name="icon-name">1</attribute>
+                              </attributes>
+                            </child>
+                            <child>
+                              <object class="GtkCellRendererText" id="name_cell">
+                                <property name="xpad">6</property>
+                                <property name="ypad">6</property>
+                                <property name="yalign">0</property>
+                                <property name="alignment">center</property>
+                                <property name="wrap_mode">word-char</property>
+                                <property name="wrap_width">12</property>
+                              </object>
+                              <attributes>
+                                <attribute name="text">1</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="view"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">True</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">True</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="standard_button">
+                <property name="label" translatable="yes">Show standard icons only</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+                <signal name="toggled" handler="standard_check_button_toggled_cb" object="GpIconNameChooser" 
swapped="no"/>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/modules/launcher/gp-launcher-applet.c b/modules/launcher/gp-launcher-applet.c
new file mode 100644
index 000000000..2e2165035
--- /dev/null
+++ b/modules/launcher/gp-launcher-applet.c
@@ -0,0 +1,1580 @@
+/*
+ * Copyright (C) 1997, 1998, 1999, 2000 The Free Software Foundation
+ * Copyright (C) 2000, 2001 Eazel, Inc.
+ * Copyright (C) 2002 Sun Microsystems Inc
+ * Copyright (C) 2004 Vincent Untz
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *     Alberts Muktupāvels <alberts muktupavels gmail com>
+ *     Federico Mena
+ *     George Lebl <jirka 5z com>
+ *     Mark McLoughlin <mark skynet ie>
+ *     Miguel de Icaza
+ *     Vincent Untz <vincent vuntz net>
+ */
+
+#include "config.h"
+#include "gp-launcher-applet.h"
+
+#include <glib/gi18n-lib.h>
+#include <gmenu-tree.h>
+#include <systemd/sd-journal.h>
+
+#include "gp-launcher-button.h"
+#include "gp-launcher-properties.h"
+#include "gp-launcher-utils.h"
+
+#define LAUNCHER_SCHEMA "org.gnome.gnome-panel.applet.launcher"
+#define LOCKDOWN_SCHEMA "org.gnome.desktop.lockdown"
+
+typedef struct
+{
+  GIcon *icon;
+  gchar *text;
+  gchar *path;
+} ApplicationData;
+
+typedef struct
+{
+  GpInitialSetupDialog *dialog;
+  GtkTreeStore         *store;
+  GSList               *applications;
+} LauncherData;
+
+typedef struct
+{
+  GSettings    *applet_settings;
+  GSettings    *lockdown_settings;
+
+  GtkWidget    *button;
+  GtkWidget    *image;
+
+  char         *location;
+  GKeyFile     *key_file;
+  GFileMonitor *monitor;
+
+  GtkWidget    *properties;
+} GpLauncherAppletPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GpLauncherApplet, gp_launcher_applet, GP_TYPE_APPLET)
+
+static ApplicationData *
+application_data_new (GIcon       *icon,
+                      const gchar *text,
+                      const gchar *path)
+{
+  ApplicationData *data;
+
+  data = g_new0 (ApplicationData, 1);
+
+  data->icon = icon ? g_object_ref (icon) : NULL;
+  data->text = g_strdup (text);
+  data->path = g_strdup (path);
+
+  return data;
+}
+
+static void
+application_data_free (gpointer user_data)
+{
+  ApplicationData *data;
+
+  data = (ApplicationData *) user_data;
+
+  g_clear_object (&data->icon);
+  g_free (data->text);
+  g_free (data->path);
+  g_free (data);
+}
+
+static LauncherData *
+launcher_data_new (GpInitialSetupDialog *dialog)
+{
+  LauncherData *data;
+
+  data = g_new0 (LauncherData, 1);
+  data->dialog = dialog;
+
+  return data;
+}
+
+static void
+launcher_data_free (gpointer user_data)
+{
+  LauncherData *data;
+
+  data = (LauncherData *) user_data;
+
+  g_clear_object (&data->store);
+  g_slist_free_full (data->applications, application_data_free);
+  g_free (data);
+}
+
+static gchar *
+make_text (const gchar *name,
+           const gchar *desc)
+{
+  const gchar *real_name;
+  gchar *result;
+
+  real_name = name ? name : _("(empty)");
+
+  if (desc != NULL && *desc != '\0')
+    result = g_markup_printf_escaped ("<span weight=\"bold\">%s</span>\n%s",
+                                      real_name, desc);
+  else
+    result = g_markup_printf_escaped ("<span weight=\"bold\">%s</span>",
+                                      real_name);
+
+  return result;
+}
+
+static void
+populate_from_root (GtkTreeStore       *store,
+                    GtkTreeIter        *parent,
+                    GMenuTreeDirectory *directory,
+                    LauncherData       *data);
+
+static void
+append_directory (GtkTreeStore       *store,
+                  GtkTreeIter        *parent,
+                  GMenuTreeDirectory *directory,
+                  LauncherData       *data)
+{
+  GIcon *icon;
+  gchar *text;
+  ApplicationData *app_data;
+  GtkTreeIter iter;
+
+  icon = gmenu_tree_directory_get_icon (directory);
+  text = make_text (gmenu_tree_directory_get_name (directory),
+                    gmenu_tree_directory_get_comment (directory));
+
+  app_data = application_data_new (icon, text, NULL);
+  data->applications = g_slist_prepend (data->applications, app_data);
+  g_free (text);
+
+  gtk_tree_store_append (store, &iter, parent);
+  gtk_tree_store_set (store, &iter,
+                      0, app_data->icon,
+                      1, app_data->text,
+                      2, NULL,
+                      -1);
+
+  populate_from_root (store, &iter, directory, data);
+}
+
+static void
+append_entry (GtkTreeStore   *store,
+              GtkTreeIter    *parent,
+              GMenuTreeEntry *entry,
+              LauncherData   *data)
+{
+  GAppInfo *app_info;
+  GIcon *icon;
+  gchar *text;
+  const gchar *path;
+  ApplicationData *app_data;
+  GtkTreeIter iter;
+
+  app_info = G_APP_INFO (gmenu_tree_entry_get_app_info (entry));
+
+  icon = g_app_info_get_icon (app_info);
+  text = make_text (g_app_info_get_display_name (app_info),
+                    g_app_info_get_description (app_info));
+  path = gmenu_tree_entry_get_desktop_file_path (entry);
+
+  app_data = application_data_new (icon, text, path);
+  data->applications = g_slist_prepend (data->applications, app_data);
+  g_free (text);
+
+  gtk_tree_store_append (store, &iter, parent);
+  gtk_tree_store_set (store, &iter,
+                      0, app_data->icon,
+                      1, app_data->text,
+                      2, app_data,
+                      -1);
+}
+
+static void
+append_alias (GtkTreeStore   *store,
+              GtkTreeIter    *parent,
+              GMenuTreeAlias *alias,
+              LauncherData   *data)
+{
+  GMenuTreeItemType type;
+
+  type = gmenu_tree_alias_get_aliased_item_type (alias);
+
+  if (type == GMENU_TREE_ITEM_DIRECTORY)
+    {
+      GMenuTreeDirectory *dir;
+
+      dir = gmenu_tree_alias_get_aliased_directory (alias);
+      append_directory (store, parent, dir, data);
+      gmenu_tree_item_unref (dir);
+    }
+  else if (type == GMENU_TREE_ITEM_ENTRY)
+    {
+      GMenuTreeEntry *entry;
+
+      entry = gmenu_tree_alias_get_aliased_entry (alias);
+      append_entry (store, parent, entry, data);
+      gmenu_tree_item_unref (entry);
+    }
+}
+
+static void
+populate_from_root (GtkTreeStore       *store,
+                    GtkTreeIter        *parent,
+                    GMenuTreeDirectory *directory,
+                    LauncherData       *data)
+{
+  GMenuTreeIter *iter;
+  GMenuTreeItemType next_type;
+
+  iter = gmenu_tree_directory_iter (directory);
+
+  next_type = gmenu_tree_iter_next (iter);
+  while (next_type != GMENU_TREE_ITEM_INVALID)
+    {
+      if (next_type == GMENU_TREE_ITEM_DIRECTORY)
+        {
+          GMenuTreeDirectory *dir;
+
+          dir = gmenu_tree_iter_get_directory (iter);
+          append_directory (store, parent, dir, data);
+          gmenu_tree_item_unref (dir);
+        }
+      else if (next_type == GMENU_TREE_ITEM_ENTRY)
+        {
+          GMenuTreeEntry *entry;
+
+          entry = gmenu_tree_iter_get_entry (iter);
+          append_entry (store, parent, entry, data);
+          gmenu_tree_item_unref (entry);
+        }
+      else if (next_type == GMENU_TREE_ITEM_ALIAS)
+        {
+          GMenuTreeAlias *alias;
+
+          alias = gmenu_tree_iter_get_alias (iter);
+          append_alias (store, parent, alias, data);
+          gmenu_tree_item_unref (alias);
+        }
+
+      next_type = gmenu_tree_iter_next (iter);
+    }
+
+  gmenu_tree_iter_unref (iter);
+}
+
+static void
+populate_model_from_menu (GtkTreeStore *store,
+                          const gchar  *menu,
+                          gboolean      separator,
+                          LauncherData *data)
+{
+  GMenuTree *tree;
+  GMenuTreeDirectory *root;
+
+  tree = gmenu_tree_new (menu, GMENU_TREE_FLAGS_SORT_DISPLAY_NAME);
+
+  if (!gmenu_tree_load_sync (tree, NULL))
+    {
+      g_object_unref (tree);
+      return;
+    }
+
+  root = gmenu_tree_get_root_directory (tree);
+
+  if (root == NULL)
+    {
+      g_object_unref (tree);
+      return;
+    }
+
+  if (separator)
+    {
+      GtkTreeIter iter;
+
+      gtk_tree_store_append (store, &iter, NULL);
+      gtk_tree_store_set (store, &iter, 0, NULL, 1, NULL, 2, NULL, -1);
+    }
+
+  populate_from_root (store, NULL, root, data);
+  gmenu_tree_item_unref (root);
+  g_object_unref (tree);
+}
+
+static gchar *
+get_applications_menu (void)
+{
+  const gchar *xdg_menu_prefx;
+
+  xdg_menu_prefx = g_getenv ("XDG_MENU_PREFIX");
+  if (!xdg_menu_prefx || *xdg_menu_prefx == '\0')
+    return g_strdup ("gnome-applications.menu");
+
+  return g_strdup_printf ("%sapplications.menu", xdg_menu_prefx);
+}
+
+static void
+populate_model (GtkTreeStore *store,
+                LauncherData *data)
+{
+  gchar *menu;
+
+  menu = get_applications_menu ();
+  populate_model_from_menu (store, menu, FALSE, data);
+  g_free (menu);
+
+  menu = g_strdup ("gnomecc.menu");
+  populate_model_from_menu (store, menu, TRUE, data);
+  g_free (menu);
+}
+
+static void
+selection_changed_cb (GtkTreeSelection *selection,
+                      LauncherData     *data)
+{
+  gboolean done;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  done = FALSE;
+  if (gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      ApplicationData *app_data;
+
+      app_data = NULL;
+      gtk_tree_model_get (model, &iter, 2, &app_data, -1);
+
+      if (app_data != NULL)
+        {
+          GVariant *variant;
+
+          variant = g_variant_new_string (app_data->path);
+
+          gp_initital_setup_dialog_set_setting (data->dialog, "location", variant);
+          done = TRUE;
+        }
+    }
+
+  gp_initital_setup_dialog_set_done (data->dialog, done);
+}
+
+static gboolean
+is_this_drop_ok (GtkWidget      *widget,
+                 GdkDragContext *context)
+{
+  GtkWidget *source;
+  GdkAtom text_uri_list;
+  GList *list_targets;
+  GList *l;
+
+  source = gtk_drag_get_source_widget (context);
+
+  if (source == widget)
+    return FALSE;
+
+  if (!(gdk_drag_context_get_actions (context) & GDK_ACTION_COPY))
+    return FALSE;
+
+  text_uri_list = gdk_atom_intern_static_string ("text/uri-list");
+
+  list_targets = gdk_drag_context_list_targets (context);
+
+  for (l = list_targets; l != NULL; l = l->next)
+    {
+      GdkAtom atom;
+
+      atom = GDK_POINTER_TO_ATOM (l->data);
+
+      if (atom == text_uri_list)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void launch (GpLauncherApplet *self,
+                    GList            *uris);
+
+static void
+drag_data_received_cb (GtkWidget        *widget,
+                       GdkDragContext   *context,
+                       gint              x,
+                       gint              y,
+                       GtkSelectionData *data,
+                       guint             info,
+                       guint             time,
+                       GpLauncherApplet *self)
+{
+  const guchar *selection_data;
+  char **uris;
+  GList *uri_list;
+  int i;
+
+  selection_data = gtk_selection_data_get_data (data);
+  uris = g_uri_list_extract_uris ((const char *) selection_data);
+
+  uri_list = NULL;
+  for (i = 0; uris[i] != NULL; i++)
+    uri_list = g_list_prepend (uri_list, uris[i]);
+
+  uri_list = g_list_reverse (uri_list);
+  launch (self, uri_list);
+
+  g_list_free (uri_list);
+  g_strfreev (uris);
+
+  gtk_drag_finish (context, TRUE, FALSE, time);
+}
+
+static gboolean
+drag_drop_cb (GtkWidget        *widget,
+              GdkDragContext   *context,
+              gint              x,
+              gint              y,
+              guint             time,
+              GpLauncherApplet *self)
+{
+  GdkAtom text_uri_list;
+
+  if (!is_this_drop_ok (widget, context))
+    return FALSE;
+
+  text_uri_list = gdk_atom_intern_static_string ("text/uri-list");
+
+  gtk_drag_get_data (widget, context, text_uri_list, time);
+
+  return TRUE;
+}
+
+static void
+drag_leave_cb (GtkWidget        *widget,
+               GdkDragContext   *context,
+               guint             time,
+               GpLauncherApplet *self)
+{
+  gtk_drag_unhighlight (widget);
+}
+
+static gboolean
+drag_motion_cb (GtkWidget        *widget,
+                GdkDragContext   *context,
+                gint              x,
+                gint              y,
+                guint             time,
+                GpLauncherApplet *self)
+{
+  if (!is_this_drop_ok (widget, context))
+    return FALSE;
+
+  gdk_drag_status (context, GDK_ACTION_COPY, time);
+  gtk_drag_highlight (widget);
+
+  return TRUE;
+}
+
+static void
+setup_drop_destination (GpLauncherApplet *self)
+{
+  GtkTargetList *target_list;
+  GdkAtom target;
+
+  gtk_drag_dest_set (GTK_WIDGET (self), 0, NULL, 0, 0);
+
+  target_list = gtk_target_list_new (NULL, 0);
+
+  target = gdk_atom_intern_static_string ("text/uri-list");
+  gtk_target_list_add (target_list, target, 0, 0);
+
+  gtk_drag_dest_set_target_list (GTK_WIDGET (self), target_list);
+  gtk_target_list_unref (target_list);
+
+  g_signal_connect (self, "drag-data-received",
+                    G_CALLBACK (drag_data_received_cb), self);
+
+  g_signal_connect (self, "drag-drop",
+                    G_CALLBACK (drag_drop_cb), self);
+
+  g_signal_connect (self, "drag-leave",
+                    G_CALLBACK (drag_leave_cb), self);
+
+  g_signal_connect (self, "drag-motion",
+                    G_CALLBACK (drag_motion_cb), self);
+}
+
+/* zoom factor, steps and delay if composited (factor must be odd) */
+#define ZOOM_FACTOR 5
+#define ZOOM_STEPS  14
+#define ZOOM_DELAY 10
+
+typedef struct
+{
+  int              size;
+  int              size_start;
+  int              size_end;
+  GtkPositionType  position;
+  double           opacity;
+  GIcon           *icon;
+  guint            timeout_id;
+  GtkWidget       *win;
+} ZoomData;
+
+static gboolean
+zoom_draw_cb (GtkWidget *widget,
+              cairo_t   *cr,
+              ZoomData  *zoom)
+{
+  GtkIconInfo *icon_info;
+  GdkPixbuf *pixbuf;
+  int width;
+  int height;
+  int x;
+  int y;
+
+  icon_info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (),
+                                              zoom->icon,
+                                              zoom->size,
+                                              GTK_ICON_LOOKUP_FORCE_SIZE);
+
+  if (icon_info == NULL)
+    return FALSE;
+
+  pixbuf = gtk_icon_info_load_icon (icon_info, NULL);
+  g_object_unref (icon_info);
+
+  if (pixbuf == NULL)
+    return FALSE;
+
+  gtk_window_get_size (GTK_WINDOW (zoom->win), &width, &height);
+
+  switch (zoom->position)
+    {
+      case GTK_POS_TOP:
+        x = (width - gdk_pixbuf_get_width (pixbuf)) / 2;
+        y = 0;
+        break;
+
+      case GTK_POS_BOTTOM:
+        x = (width - gdk_pixbuf_get_width (pixbuf)) / 2;
+        y = height - gdk_pixbuf_get_height (pixbuf);
+        break;
+
+      case GTK_POS_LEFT:
+        x = 0;
+        y = (height - gdk_pixbuf_get_height (pixbuf)) / 2;
+        break;
+
+      case GTK_POS_RIGHT:
+        x = width - gdk_pixbuf_get_width (pixbuf);
+        y = (height - gdk_pixbuf_get_height (pixbuf)) / 2;
+        break;
+
+      default:
+        g_assert_not_reached ();
+        break;
+    }
+
+  cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE);
+  cairo_set_source_rgba (cr, 0, 0, 0, 0.0);
+  cairo_rectangle (cr, 0, 0, width, height);
+  cairo_fill (cr);
+
+  gdk_cairo_set_source_pixbuf (cr, pixbuf, x, y);
+  g_object_unref (pixbuf);
+
+  cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+  cairo_paint_with_alpha (cr, MAX (zoom->opacity, 0));
+
+  return FALSE;
+}
+
+static gboolean
+zoom_timeout_cb (gpointer user_data)
+{
+  ZoomData *zoom;
+
+  zoom = user_data;
+
+  if (zoom->size >= zoom->size_end)
+    {
+      gtk_widget_destroy (zoom->win);
+      g_object_unref (zoom->icon);
+
+      g_slice_free (ZoomData, zoom);
+
+      return G_SOURCE_REMOVE;
+    }
+
+  zoom->size += MAX ((zoom->size_end - zoom->size_start) / ZOOM_STEPS, 1);
+  zoom->opacity -= 1.0 / (ZOOM_STEPS + 1.0);
+
+  gtk_widget_queue_draw (zoom->win);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+draw_zoom_animation (GpLauncherApplet *self,
+                     int               x,
+                     int               y,
+                     int               width,
+                     int               height,
+                     GIcon            *icon,
+                     GtkPositionType   position)
+{
+  ZoomData *zoom;
+  GdkScreen *screen;
+  GdkVisual *visual;
+  int wx;
+  int wy;
+
+  width += 2;
+  height += 2;
+
+  zoom = g_slice_new (ZoomData);
+
+  zoom->size = MIN (width, height);
+  zoom->size_start = zoom->size;
+  zoom->size_end = zoom->size * ZOOM_FACTOR;
+  zoom->position = position;
+  zoom->opacity = 1.0;
+  zoom->icon = g_object_ref (icon);
+  zoom->timeout_id = 0;
+
+  zoom->win = gtk_window_new (GTK_WINDOW_POPUP);
+
+  gtk_window_set_keep_above (GTK_WINDOW (zoom->win), TRUE);
+  gtk_window_set_decorated (GTK_WINDOW (zoom->win), FALSE);
+  gtk_widget_set_app_paintable (zoom->win, TRUE);
+
+  screen = gtk_widget_get_screen (GTK_WIDGET (self));
+  visual = gdk_screen_get_rgba_visual (screen);
+  gtk_widget_set_visual (zoom->win, visual);
+
+  gtk_window_set_gravity (GTK_WINDOW (zoom->win), GDK_GRAVITY_STATIC);
+  gtk_window_set_default_size (GTK_WINDOW (zoom->win),
+                               width * ZOOM_FACTOR,
+                               height * ZOOM_FACTOR);
+
+  switch (position)
+    {
+      case GTK_POS_TOP:
+        wx = x - width * (ZOOM_FACTOR / 2);
+        wy = y;
+        break;
+
+      case GTK_POS_BOTTOM:
+        wx = x - width * (ZOOM_FACTOR / 2);
+        wy = y - height * (ZOOM_FACTOR - 1);
+        break;
+
+      case GTK_POS_LEFT:
+        wx = x;
+        wy = y - height * (ZOOM_FACTOR / 2);
+        break;
+
+      case GTK_POS_RIGHT:
+        wx = x - width * (ZOOM_FACTOR - 1);
+        wy = y - height * (ZOOM_FACTOR / 2);
+        break;
+
+      default:
+        g_assert_not_reached ();
+        break;
+    }
+
+  g_signal_connect (zoom->win, "draw", G_CALLBACK (zoom_draw_cb), zoom);
+
+  gtk_window_move (GTK_WINDOW (zoom->win), wx, wy);
+  gtk_widget_realize (zoom->win);
+  gtk_widget_show (zoom->win);
+
+  zoom->timeout_id = g_timeout_add (ZOOM_DELAY, zoom_timeout_cb, zoom);
+  g_source_set_name_by_id (zoom->timeout_id, "[gnome-panel] zoom_timeout_cb");
+}
+
+static void
+launch_animation (GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+  GdkScreen *screen;
+  GtkSettings *settings;
+  gboolean enable_animations;
+  GIcon *icon;
+  int x;
+  int y;
+  GtkAllocation allocation;
+  GtkPositionType position;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  screen = gtk_widget_get_screen (GTK_WIDGET (self));
+  settings = gtk_widget_get_settings (GTK_WIDGET (self));
+
+  enable_animations = TRUE;
+  g_object_get (settings, "gtk-enable-animations", &enable_animations, NULL);
+
+  if (!enable_animations || !gdk_screen_is_composited (screen))
+    return;
+
+  icon = NULL;
+  gtk_image_get_gicon (GTK_IMAGE (priv->image), &icon, NULL);
+
+  if (icon == NULL)
+    return;
+
+  gdk_window_get_origin (gtk_widget_get_window (GTK_WIDGET (self)), &x, &y);
+  gtk_widget_get_allocation (GTK_WIDGET (self), &allocation);
+
+  position = gp_applet_get_position (GP_APPLET (self));
+
+  draw_zoom_animation (self,
+                       x,
+                       y,
+                       allocation.width,
+                       allocation.height,
+                       icon,
+                       position);
+}
+
+static void
+child_setup (gpointer user_data)
+{
+  GAppInfo *info;
+  const gchar *id;
+  gint stdout_fd;
+  gint stderr_fd;
+
+  info = G_APP_INFO (user_data);
+  id = g_app_info_get_id (info);
+
+  stdout_fd = sd_journal_stream_fd (id, LOG_INFO, FALSE);
+  if (stdout_fd >= 0)
+    {
+      dup2 (stdout_fd, STDOUT_FILENO);
+      close (stdout_fd);
+    }
+
+  stderr_fd = sd_journal_stream_fd (id, LOG_WARNING, FALSE);
+  if (stderr_fd >= 0)
+    {
+      dup2 (stderr_fd, STDERR_FILENO);
+      close (stderr_fd);
+    }
+}
+
+static void
+close_pid (GPid     pid,
+           gint     status,
+           gpointer user_data)
+{
+  g_spawn_close_pid (pid);
+}
+
+static void
+pid_cb (GDesktopAppInfo *info,
+        GPid             pid,
+        gpointer         user_data)
+{
+  g_child_watch_add (pid, close_pid, NULL);
+}
+
+static void
+launch (GpLauncherApplet *self,
+        GList            *uris)
+{
+  GpLauncherAppletPrivate *priv;
+  char *type;
+  char *command;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  type = NULL;
+  command = NULL;
+
+  if (!gp_launcher_read_from_key_file (priv->key_file,
+                                       NULL,
+                                       &type,
+                                       NULL,
+                                       &command,
+                                       NULL,
+                                       NULL))
+    return;
+
+  launch_animation (self);
+
+  if (g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) == 0)
+    {
+      GDesktopAppInfo *app_info;
+
+      app_info = g_desktop_app_info_new_from_keyfile (priv->key_file);
+
+      if (app_info != NULL)
+        {
+          GSpawnFlags flags;
+          GError *error;
+
+          flags = G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD;
+
+          error = NULL;
+          g_desktop_app_info_launch_uris_as_manager (app_info,
+                                                     uris,
+                                                     NULL,
+                                                     flags,
+                                                     child_setup,
+                                                     app_info,
+                                                     pid_cb,
+                                                     NULL,
+                                                     &error);
+
+          if (error != NULL)
+            {
+              gp_launcher_show_error_message (NULL,
+                                              _("Could not launch application"),
+                                              error->message);
+
+              g_error_free (error);
+            }
+
+          g_object_unref (app_info);
+        }
+      else
+        {
+          char *error_message;
+
+          error_message = g_strdup_printf (_("Can not execute “%s” command line."),
+                                           command);
+
+          gp_launcher_show_error_message (NULL,
+                                          _("Could not launch application"),
+                                          error_message);
+
+          g_free (error_message);
+        }
+    }
+  else if (g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_LINK) == 0)
+    {
+      GError *error;
+
+      error = NULL;
+      gtk_show_uri_on_window (NULL,
+                              command,
+                              gtk_get_current_event_time (),
+                              &error);
+
+      if (error != NULL)
+        {
+          gp_launcher_show_error_message (NULL,
+                                          _("Could not open location"),
+                                          error->message);
+
+          g_error_free (error);
+        }
+    }
+
+  g_free (type);
+  g_free (command);
+}
+
+static void
+launcher_error (GpLauncherApplet *self,
+                const char       *error)
+{
+  GpLauncherAppletPrivate *priv;
+  guint icon_size;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  gtk_widget_set_tooltip_text (GTK_WIDGET (self), error);
+
+  gtk_image_set_from_icon_name (GTK_IMAGE (priv->image),
+                                "gnome-panel-launcher",
+                                GTK_ICON_SIZE_MENU);
+
+  icon_size = gp_applet_get_panel_icon_size (GP_APPLET (self));
+  gtk_image_set_pixel_size (GTK_IMAGE (priv->image), icon_size);
+}
+
+static void
+update_icon (GpLauncherApplet *self,
+             const char       *icon_name)
+{
+  GpLauncherAppletPrivate *priv;
+  GIcon *icon;
+  guint icon_size;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+  icon = NULL;
+
+  if (icon_name != NULL && *icon_name != '\0')
+    {
+      if (g_path_is_absolute (icon_name))
+        {
+          GFile *file;
+
+          file = g_file_new_for_path (icon_name);
+          icon = g_file_icon_new (file);
+          g_object_unref (file);
+        }
+      else
+        {
+          char *p;
+
+          /* Work around a common mistake in desktop files */
+          if ((p = strrchr (icon_name, '.')) != NULL &&
+              (strcmp (p, ".png") == 0 ||
+               strcmp (p, ".xpm") == 0 ||
+               strcmp (p, ".svg") == 0))
+            *p = '\0';
+
+          icon = g_themed_icon_new (icon_name);
+        }
+    }
+
+  if (icon == NULL)
+    icon = g_themed_icon_new ("gnome-panel-launcher");
+
+  gtk_image_set_from_gicon (GTK_IMAGE (priv->image), icon, GTK_ICON_SIZE_MENU);
+  g_object_unref (icon);
+
+  icon_size = gp_applet_get_panel_icon_size (GP_APPLET (self));
+  gtk_image_set_pixel_size (GTK_IMAGE (priv->image), icon_size);
+}
+
+static void
+update_tooltip (GpLauncherApplet *self,
+                const char       *name,
+                const char       *comment)
+{
+  char *tooltip;
+
+  if (name != NULL && *name != '\0' && comment != NULL && *comment != '\0')
+    tooltip = g_strdup_printf ("%s\n%s", name, comment);
+  else if (name != NULL && *name != '\0')
+    tooltip = g_strdup (name);
+  else if (comment != NULL && *comment != '\0')
+    tooltip = g_strdup (comment);
+  else
+    tooltip = NULL;
+
+  gtk_widget_set_tooltip_text (GTK_WIDGET (self), tooltip);
+  g_free (tooltip);
+
+  g_object_bind_property (self,
+                          "enable-tooltips",
+                          self,
+                          "has-tooltip",
+                          G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+}
+
+static void
+update_launcher (GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+  GError *error;
+  char *error_message;
+  char *icon;
+  char *name;
+  char *comment;
+  AtkObject *atk;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  error = NULL;
+  g_key_file_load_from_file (priv->key_file,
+                             priv->location,
+                             G_KEY_FILE_NONE,
+                             &error);
+
+  error_message = NULL;
+
+  if (error != NULL)
+    {
+      error_message = g_strdup_printf (_("Failed to load key file “%s”: %s"),
+                                       priv->location,
+                                       error->message);
+
+      g_error_free (error);
+
+      launcher_error (self, error_message);
+      g_free (error_message);
+
+      return;
+    }
+
+  icon = NULL;
+  name = NULL;
+  comment = NULL;
+
+  if (!gp_launcher_read_from_key_file (priv->key_file,
+                                       &icon,
+                                       NULL,
+                                       &name,
+                                       NULL,
+                                       &comment,
+                                       &error_message))
+    {
+      launcher_error (self, error_message);
+      g_free (error_message);
+
+      return;
+    }
+
+  update_icon (self, icon);
+  update_tooltip (self, name, comment);
+
+  atk = gtk_widget_get_accessible (GTK_WIDGET (self));
+  atk_object_set_name (atk, name != NULL ? name : "");
+  atk_object_set_description (atk, comment != NULL ? comment : "");
+
+  g_free (icon);
+  g_free (name);
+  g_free (comment);
+}
+
+static void
+file_changed_cb (GFileMonitor     *monitor,
+                 GFile            *file,
+                 GFile            *other_file,
+                 GFileMonitorEvent event_type,
+                 GpLauncherApplet *self)
+{
+  update_launcher (self);
+}
+
+static void
+location_changed (GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+  GFile *file;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  g_clear_pointer (&priv->location, g_free);
+  g_clear_pointer (&priv->key_file, g_key_file_unref);
+  g_clear_object (&priv->monitor);
+
+  priv->location = g_settings_get_string (priv->applet_settings, "location");
+
+  if (!g_path_is_absolute (priv->location))
+    {
+      char *launchers_dir;
+      char *filename;
+
+      launchers_dir = gp_launcher_get_launchers_dir ();
+
+      filename = g_build_filename (launchers_dir, priv->location, NULL);
+      g_free (launchers_dir);
+
+      g_free (priv->location);
+      priv->location = filename;
+    }
+
+  priv->key_file = g_key_file_new ();
+
+  file = g_file_new_for_path (priv->location);
+  priv->monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, NULL);
+  g_file_monitor_set_rate_limit (priv->monitor, 200);
+  g_object_unref (file);
+
+  g_signal_connect (priv->monitor,
+                    "changed",
+                    G_CALLBACK (file_changed_cb),
+                    self);
+
+  update_launcher (self);
+}
+
+static void
+update_properties_action (GpLauncherApplet *self)
+{
+  GpApplet *applet;
+  GpLauncherAppletPrivate *priv;
+  gboolean locked_down;
+  gboolean disable_command_line;
+  GAction *action;
+  gboolean enabled;
+
+  applet = GP_APPLET (self);
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  locked_down = gp_applet_get_locked_down (applet);
+  disable_command_line = g_settings_get_boolean (priv->lockdown_settings,
+                                                 "disable-command-line");
+
+  action = gp_applet_menu_lookup_action (applet, "properties");
+  enabled = !locked_down && !disable_command_line;
+
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enabled);
+}
+
+static void
+applet_settings_changed_cb (GSettings        *settings,
+                            const gchar      *key,
+                            GpLauncherApplet *self)
+{
+  if (g_strcmp0 (key, "location") != 0)
+    return;
+
+  location_changed (self);
+}
+
+static void
+lockdown_settings_changed_cb (GSettings        *settings,
+                              const gchar      *key,
+                              GpLauncherApplet *self)
+{
+  if (g_strcmp0 (key, "disable-command-line") != 0)
+    return;
+
+  update_properties_action (self);
+}
+
+static void
+locked_down_cb (GpApplet         *applet,
+                GParamSpec       *pspec,
+                GpLauncherApplet *self)
+{
+  update_properties_action (self);
+}
+
+static void
+panel_icon_size_cb (GpApplet         *applet,
+                    GParamSpec       *pspec,
+                    GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+  guint icon_size;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  icon_size = gp_applet_get_panel_icon_size (applet);
+  gtk_image_set_pixel_size (GTK_IMAGE (priv->image), icon_size);
+}
+
+static void
+launch_cb (GSimpleAction *action,
+           GVariant      *parameter,
+           gpointer       user_data)
+{
+  launch (GP_LAUNCHER_APPLET (user_data), NULL);
+}
+
+static void
+properties_cb (GSimpleAction *action,
+               GVariant      *parameter,
+               gpointer       user_data)
+{
+  GpLauncherApplet *self;
+  GpLauncherAppletPrivate *priv;
+
+  self = GP_LAUNCHER_APPLET (user_data);
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  if (priv->properties != NULL)
+    {
+      gtk_window_present (GTK_WINDOW (priv->properties));
+      return;
+    }
+
+  priv->properties = gp_launcher_properties_new (priv->applet_settings);
+  g_object_add_weak_pointer (G_OBJECT (priv->properties),
+                             (gpointer *) &priv->properties);
+
+  gtk_window_present (GTK_WINDOW (priv->properties));
+}
+
+static const GActionEntry launcher_menu_actions[] =
+  {
+    { "launch", launch_cb, NULL, NULL, NULL },
+    { "properties", properties_cb, NULL, NULL, NULL },
+    { NULL }
+  };
+
+static void
+setup_menu (GpLauncherApplet *self)
+{
+  GpApplet *applet;
+  const gchar *resource;
+
+  applet = GP_APPLET (self);
+
+  resource = GP_LAUNCHER_APPLET_GET_CLASS (self)->get_menu_resource ();
+  gp_applet_setup_menu_from_resource (applet, resource, launcher_menu_actions);
+
+  update_properties_action (self);
+}
+
+static void
+clicked_cb (GtkWidget        *widget,
+            GpLauncherApplet *self)
+{
+  launch (self, NULL);
+}
+
+static void
+setup_button (GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+  guint icon_size;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  priv->button = gp_launcher_button_new ();
+  gtk_container_add (GTK_CONTAINER (self), priv->button);
+  gtk_widget_show (priv->button);
+
+  g_signal_connect (priv->button, "clicked",
+                    G_CALLBACK (clicked_cb), self);
+
+  priv->image = gtk_image_new ();
+  gtk_container_add (GTK_CONTAINER (priv->button), priv->image);
+  gtk_widget_show (priv->image);
+
+  icon_size = gp_applet_get_panel_icon_size (GP_APPLET (self));
+  gtk_image_set_pixel_size (GTK_IMAGE (priv->image), icon_size);
+}
+
+static void
+gp_launcher_applet_setup (GpLauncherApplet *self)
+{
+  GpLauncherAppletPrivate *priv;
+
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  priv->applet_settings = gp_applet_settings_new (GP_APPLET (self),
+                                                  LAUNCHER_SCHEMA);
+
+  priv->lockdown_settings = g_settings_new (LOCKDOWN_SCHEMA);
+
+  g_signal_connect (priv->applet_settings, "changed",
+                    G_CALLBACK (applet_settings_changed_cb), self);
+
+  g_signal_connect (priv->lockdown_settings, "changed::disable-command-line",
+                    G_CALLBACK (lockdown_settings_changed_cb), self);
+
+  g_signal_connect (self, "notify::locked-down",
+                    G_CALLBACK (locked_down_cb), self);
+
+  g_signal_connect (self, "notify::panel-icon-size",
+                    G_CALLBACK (panel_icon_size_cb), self);
+
+  setup_menu (self);
+  setup_button (self);
+
+  setup_drop_destination (self);
+
+  location_changed (self);
+}
+
+static void
+gp_launcher_applet_constructed (GObject *object)
+{
+  G_OBJECT_CLASS (gp_launcher_applet_parent_class)->constructed (object);
+  gp_launcher_applet_setup (GP_LAUNCHER_APPLET (object));
+}
+
+static void
+gp_launcher_applet_dispose (GObject *object)
+{
+  GpLauncherApplet *self;
+  GpLauncherAppletPrivate *priv;
+
+  self = GP_LAUNCHER_APPLET (object);
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  g_clear_object (&priv->applet_settings);
+  g_clear_object (&priv->lockdown_settings);
+
+  g_clear_pointer (&priv->key_file, g_key_file_unref);
+  g_clear_object (&priv->monitor);
+
+  g_clear_pointer (&priv->properties, gtk_widget_destroy);
+
+  G_OBJECT_CLASS (gp_launcher_applet_parent_class)->dispose (object);
+}
+
+static void
+gp_launcher_applet_finalize (GObject *object)
+{
+  GpLauncherApplet *self;
+  GpLauncherAppletPrivate *priv;
+
+  self = GP_LAUNCHER_APPLET (object);
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  g_clear_pointer (&priv->location, g_free);
+
+  G_OBJECT_CLASS (gp_launcher_applet_parent_class)->finalize (object);
+}
+
+static void
+gp_launcher_applet_initial_setup (GpApplet *applet,
+                                  GVariant *initial_settings)
+{
+  GSettings *settings;
+  const char *location;
+
+  settings = gp_applet_settings_new (applet, LAUNCHER_SCHEMA);
+
+  location = NULL;
+  if (g_variant_lookup (initial_settings, "location", "&s", &location))
+    {
+      g_settings_set_string (settings, "location", location);
+    }
+  else
+    {
+      const char *type;
+      const char *icon;
+      const char *name;
+      const char *command;
+      const char *comment;
+      GKeyFile *file;
+      char *filename;
+      GError *error;
+
+      g_variant_lookup (initial_settings, "type", "&s", &type);
+      g_variant_lookup (initial_settings, "icon", "&s", &icon);
+      g_variant_lookup (initial_settings, "name", "&s", &name);
+      g_variant_lookup (initial_settings, "command", "&s", &command);
+      g_variant_lookup (initial_settings, "comment", "&s", &comment);
+
+      file = g_key_file_new ();
+
+      g_key_file_set_string (file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_VERSION, "1.0");
+
+      g_key_file_set_string (file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_TYPE,
+                             type);
+
+      g_key_file_set_string (file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_ICON,
+                             icon);
+
+      g_key_file_set_string (file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_NAME,
+                             name);
+
+      g_key_file_set_string (file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_COMMENT,
+                             comment);
+
+      if (g_strcmp0 (type, "Application") == 0)
+        {
+          gboolean terminal;
+
+          g_key_file_set_string (file,
+                                 G_KEY_FILE_DESKTOP_GROUP,
+                                 G_KEY_FILE_DESKTOP_KEY_EXEC,
+                                 command);
+
+          if (g_variant_lookup (initial_settings, "terminal", "b", &terminal))
+            {
+              g_key_file_set_boolean (file,
+                                      G_KEY_FILE_DESKTOP_GROUP,
+                                      G_KEY_FILE_DESKTOP_KEY_TERMINAL,
+                                      terminal);
+            }
+        }
+      else if (g_strcmp0 (type, "Link") == 0)
+        {
+          g_key_file_set_string (file,
+                                 G_KEY_FILE_DESKTOP_GROUP,
+                                 G_KEY_FILE_DESKTOP_KEY_URL,
+                                 command);
+        }
+      else
+        {
+          g_assert_not_reached ();
+        }
+
+      filename = gp_launcher_get_unique_filename ();
+
+      error = NULL;
+      g_key_file_save_to_file (file, filename, &error);
+      g_key_file_unref (file);
+
+      if (error != NULL)
+        {
+          g_warning ("%s", error->message);
+          g_error_free (error);
+        }
+      else
+        {
+          char *basename;
+
+          basename = g_path_get_basename (filename);
+          g_settings_set_string (settings, "location", basename);
+          g_free (basename);
+        }
+
+      g_free (filename);
+    }
+
+  g_object_unref (settings);
+}
+
+static void
+delete_cb (GObject      *source_object,
+           GAsyncResult *res,
+           gpointer      user_data)
+{
+  GError *error;
+
+  error = NULL;
+  g_file_delete_finish (G_FILE (source_object), res, &error);
+
+  if (error != NULL)
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        g_warning ("Failed to delete launcher file: %s", error->message);
+
+      g_error_free (error);
+      return;
+    }
+}
+
+static void
+gp_launcher_applet_remove_from_panel (GpApplet *applet)
+{
+  GpLauncherApplet *self;
+  GpLauncherAppletPrivate *priv;
+  char *launchers_dir;
+
+  self = GP_LAUNCHER_APPLET (applet);
+  priv = gp_launcher_applet_get_instance_private (self);
+
+  launchers_dir = gp_launcher_get_launchers_dir ();
+
+  if (g_str_has_prefix (priv->location, launchers_dir))
+    {
+      GFile *file;
+
+      file = g_file_new_for_path (priv->location);
+
+      g_file_delete_async (file, G_PRIORITY_DEFAULT, NULL, delete_cb, NULL);
+      g_object_unref (file);
+    }
+
+  g_free (launchers_dir);
+}
+
+static const char *
+gp_launcher_applet_get_menu_resource (void)
+{
+  return GRESOURCE_PREFIX "/launcher-menu.ui";
+}
+
+static void
+gp_launcher_applet_class_init (GpLauncherAppletClass *self_class)
+{
+  GObjectClass *object_class;
+  GpAppletClass *applet_class;
+
+  object_class = G_OBJECT_CLASS (self_class);
+  applet_class = GP_APPLET_CLASS (self_class);
+
+  object_class->constructed = gp_launcher_applet_constructed;
+  object_class->dispose = gp_launcher_applet_dispose;
+  object_class->finalize = gp_launcher_applet_finalize;
+
+  applet_class->initial_setup = gp_launcher_applet_initial_setup;
+  applet_class->remove_from_panel = gp_launcher_applet_remove_from_panel;
+
+  self_class->get_menu_resource = gp_launcher_applet_get_menu_resource;
+}
+
+static void
+gp_launcher_applet_init (GpLauncherApplet *self)
+{
+  GpApplet *applet;
+
+  applet = GP_APPLET (self);
+
+  gp_applet_set_flags (applet, GP_APPLET_FLAGS_EXPAND_MINOR);
+}
+
+void
+gp_launcher_applet_initial_setup_dialog (GpInitialSetupDialog *dialog)
+{
+  LauncherData *data;
+  GtkWidget *scrolled;
+  GtkWidget *tree_view;
+  GtkTreeSelection *selection;
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *renderer;
+
+  data = launcher_data_new (dialog);
+
+  scrolled = gtk_scrolled_window_new (NULL, NULL);
+  gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_IN);
+  gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW (scrolled), 460);
+  gtk_scrolled_window_set_min_content_width (GTK_SCROLLED_WINDOW (scrolled), 480);
+  gtk_widget_show (scrolled);
+
+  tree_view = gtk_tree_view_new ();
+  gtk_container_add (GTK_CONTAINER (scrolled), tree_view);
+  gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree_view), FALSE);
+  gtk_widget_show (tree_view);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree_view));
+  gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
+
+  g_signal_connect (selection, "changed",
+                    G_CALLBACK (selection_changed_cb), data);
+
+  column = gtk_tree_view_column_new ();
+  gtk_tree_view_append_column (GTK_TREE_VIEW (tree_view), column);
+
+  renderer = gtk_cell_renderer_pixbuf_new ();
+  gtk_tree_view_column_pack_start (column, renderer, FALSE);
+  gtk_tree_view_column_add_attribute (column, renderer, "gicon", 0);
+
+  g_object_set (renderer,
+                "stock-size", GTK_ICON_SIZE_DND,
+                "xpad", 4, "ypad", 4,
+                NULL);
+
+  renderer = gtk_cell_renderer_text_new ();
+  gtk_tree_view_column_pack_start (column, renderer, TRUE);
+  gtk_tree_view_column_add_attribute (column, renderer, "markup", 1);
+
+  g_object_set (renderer,
+                "ellipsize", PANGO_ELLIPSIZE_END,
+                "xpad", 4, "ypad", 4,
+                NULL);
+
+  data->store = gtk_tree_store_new (3, G_TYPE_ICON, G_TYPE_STRING, G_TYPE_POINTER);
+  populate_model (data->store, data);
+
+  gtk_tree_view_set_model (GTK_TREE_VIEW (tree_view),
+                           GTK_TREE_MODEL (data->store));
+
+  gp_initital_setup_dialog_add_content_widget (dialog, scrolled, data,
+                                               launcher_data_free);
+}
diff --git a/modules/launcher/gp-launcher-applet.h b/modules/launcher/gp-launcher-applet.h
new file mode 100644
index 000000000..43e708b8d
--- /dev/null
+++ b/modules/launcher/gp-launcher-applet.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_LAUNCHER_APPLET_H
+#define GP_LAUNCHER_APPLET_H
+
+#include <libgnome-panel/gp-applet.h>
+#include <libgnome-panel/gp-module.h>
+
+G_BEGIN_DECLS
+
+#define GP_TYPE_LAUNCHER_APPLET (gp_launcher_applet_get_type ())
+G_DECLARE_DERIVABLE_TYPE (GpLauncherApplet, gp_launcher_applet,
+                          GP, LAUNCHER_APPLET, GpApplet)
+
+struct _GpLauncherAppletClass
+{
+  GpAppletClass parent;
+
+  const char * (* get_menu_resource) (void);
+};
+
+void gp_launcher_applet_initial_setup_dialog (GpInitialSetupDialog *dialog);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-launcher-button.c b/modules/launcher/gp-launcher-button.c
new file mode 100644
index 000000000..d7b4b236a
--- /dev/null
+++ b/modules/launcher/gp-launcher-button.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "gp-launcher-button.h"
+
+struct _GpLauncherButton
+{
+  GtkButton parent;
+};
+
+G_DEFINE_TYPE (GpLauncherButton, gp_launcher_button, GTK_TYPE_BUTTON)
+
+static void
+gp_launcher_button_class_init (GpLauncherButtonClass *button_class)
+{
+  GtkWidgetClass *widget_class;
+
+  widget_class = GTK_WIDGET_CLASS (button_class);
+
+  gtk_widget_class_set_css_name (widget_class, "gp-launcher-button");
+}
+
+static void
+gp_launcher_button_init (GpLauncherButton *button)
+{
+}
+
+GtkWidget *
+gp_launcher_button_new (void)
+{
+  return g_object_new (GP_TYPE_LAUNCHER_BUTTON, NULL);
+}
diff --git a/modules/launcher/gp-launcher-button.h b/modules/launcher/gp-launcher-button.h
new file mode 100644
index 000000000..7c4d9cccd
--- /dev/null
+++ b/modules/launcher/gp-launcher-button.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_LAUNCHER_BUTTON_H
+#define GP_LAUNCHER_BUTTON_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GP_TYPE_LAUNCHER_BUTTON (gp_launcher_button_get_type ())
+G_DECLARE_FINAL_TYPE (GpLauncherButton, gp_launcher_button,
+                      GP, LAUNCHER_BUTTON, GtkButton)
+
+GtkWidget *gp_launcher_button_new (void);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-launcher-module.c b/modules/launcher/gp-launcher-module.c
new file mode 100644
index 000000000..28709e7df
--- /dev/null
+++ b/modules/launcher/gp-launcher-module.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n-lib.h>
+#include <libgnome-panel/gp-module.h>
+
+#include "gp-custom-launcher-applet.h"
+#include "gp-launcher-applet.h"
+
+static GpAppletInfo *
+launcher_get_applet_info (const gchar *id)
+{
+  GpGetAppletTypeFunc type_func;
+  const gchar *name;
+  const gchar *description;
+  const gchar *icon;
+  GpInitialSetupDialogFunc initial_setup_func;
+  GpAppletInfo *info;
+
+  initial_setup_func = NULL;
+
+  if (g_strcmp0 (id, "custom-launcher") == 0)
+    {
+      type_func = gp_custom_launcher_applet_get_type;
+      name = _("Custom Application Launcher");
+      description = _("Create a new launcher");
+      icon = "gnome-panel-launcher";
+
+      initial_setup_func = gp_custom_launcher_applet_initial_setup_dialog;
+    }
+  else if (g_strcmp0 (id, "launcher") == 0)
+    {
+      type_func = gp_launcher_applet_get_type;
+      name = _("Application Launcher...");
+      description = _("Copy a launcher from the applications menu");
+      icon = "gnome-panel-launcher";
+
+      initial_setup_func = gp_launcher_applet_initial_setup_dialog;
+    }
+  else
+    {
+      g_assert_not_reached ();
+      return NULL;
+    }
+
+  info = gp_applet_info_new (type_func, name, description, icon);
+
+  if (initial_setup_func != NULL)
+    gp_applet_info_set_initial_setup_dialog (info, initial_setup_func);
+
+  return info;
+}
+
+static const gchar *
+launcher_get_applet_id_from_iid (const gchar *iid)
+{
+  if (g_strcmp0 (iid, "PanelInternalFactory::Launcher") == 0)
+    return "custom-launcher";
+
+  return NULL;
+}
+
+void
+gp_module_load (GpModule *module)
+{
+  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+  gp_module_set_gettext_domain (module, GETTEXT_PACKAGE);
+
+  gp_module_set_abi_version (module, GP_MODULE_ABI_VERSION);
+
+  gp_module_set_id (module, "org.gnome.gnome-panel.launcher");
+  gp_module_set_version (module, PACKAGE_VERSION);
+
+  gp_module_set_applet_ids (module, "custom-launcher", "launcher", NULL);
+
+  gp_module_set_get_applet_info (module, launcher_get_applet_info);
+  gp_module_set_compatibility (module, launcher_get_applet_id_from_iid);
+}
diff --git a/modules/launcher/gp-launcher-properties.c b/modules/launcher/gp-launcher-properties.c
new file mode 100644
index 000000000..fbf08cf53
--- /dev/null
+++ b/modules/launcher/gp-launcher-properties.c
@@ -0,0 +1,695 @@
+/*
+ * Copyright (C) 2008 Novell, Inc.
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *     Alberts Muktupāvels <alberts muktupavels gmail com>
+ *     Vincent Untz <vincent vuntz net>
+ */
+
+#include "config.h"
+#include "gp-launcher-properties.h"
+
+#include <glib/gi18n-lib.h>
+
+#include "gp-editor.h"
+#include "gp-launcher-utils.h"
+
+enum
+{
+  GP_RESPONSE_REVERT
+};
+
+struct _GpLauncherProperties
+{
+  GtkDialog  parent;
+
+  GpEditor  *editor;
+  GtkWidget *revert_button;
+
+  GSettings *settings;
+
+  GKeyFile  *file;
+  GKeyFile  *revert_file;
+
+  gboolean   dirty;
+
+  guint      save_id;
+};
+
+enum
+{
+  PROP_0,
+
+  PROP_SETTINGS,
+
+  LAST_PROP
+};
+
+static GParamSpec *properties_properties[LAST_PROP] = { NULL };
+
+G_DEFINE_TYPE (GpLauncherProperties, gp_launcher_properties, GTK_TYPE_DIALOG)
+
+static void
+show_error_message (GpLauncherProperties *self,
+                    const char           *error_message)
+{
+  gp_launcher_show_error_message (GTK_WINDOW (self),
+                                  _("Could not save launcher"),
+                                  error_message);
+}
+
+static gboolean
+get_launcher_filename (GpLauncherProperties  *self,
+                       char                 **filename)
+{
+  char *location;
+  char *launchers_dir;
+
+  g_assert (*filename == NULL);
+
+  location = g_settings_get_string (self->settings, "location");
+  launchers_dir = gp_launcher_get_launchers_dir ();
+
+  if (g_path_is_absolute (location) &&
+      !g_str_has_prefix (location, launchers_dir))
+    {
+      *filename = gp_launcher_get_unique_filename ();
+
+      g_free (location);
+      g_free (launchers_dir);
+
+      return TRUE;
+    }
+
+  *filename = location;
+
+  g_free (launchers_dir);
+
+  return FALSE;
+}
+
+static gboolean
+launcher_save (GpLauncherProperties *self,
+               gboolean              interactive)
+{
+  char *error_message;
+  gboolean location_changed;
+  char *filename;
+  GError *error;
+
+  if (self->save_id != 0)
+    {
+      g_source_remove (self->save_id);
+      self->save_id = 0;
+    }
+
+  if (!self->dirty)
+    return TRUE;
+
+  error_message = NULL;
+  if (!gp_launcher_validate_key_file (self->file, &error_message))
+    {
+      if (interactive)
+        show_error_message (self, error_message);
+      g_free (error_message);
+
+      return FALSE;
+    }
+
+  filename = NULL;
+  location_changed = get_launcher_filename (self, &filename);
+
+  error = NULL;
+  if (!g_key_file_save_to_file (self->file, filename, &error))
+    {
+      if (interactive)
+        show_error_message (self, error->message);
+
+      g_error_free (error);
+      g_free (filename);
+
+      return FALSE;
+    }
+
+  if (location_changed)
+    {
+      char *basename;
+
+      basename = g_path_get_basename (filename);
+      g_settings_set_string (self->settings, "location", basename);
+      g_free (basename);
+    }
+
+  g_free (filename);
+
+  self->dirty = FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+save_cb (gpointer user_data)
+{
+  GpLauncherProperties *self;
+
+  self = GP_LAUNCHER_PROPERTIES (user_data);
+  self->save_id = 0;
+
+  launcher_save (self, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+launcher_changed (GpLauncherProperties *self)
+{
+  self->dirty = TRUE;
+
+  gtk_dialog_set_response_sensitive (GTK_DIALOG (self),
+                                     GP_RESPONSE_REVERT,
+                                     TRUE);
+
+  if (self->save_id != 0)
+    g_source_remove (self->save_id);
+
+  self->save_id = g_timeout_add_seconds (2, save_cb, self);
+  g_source_set_name_by_id (self->save_id, "[gnome-panel] save_cb");
+}
+
+static void
+remove_locale_key (GKeyFile   *key_file,
+                   const char *key)
+{
+  char **keys;
+  size_t key_len;
+  int i;
+
+  keys = g_key_file_get_keys (key_file, G_KEY_FILE_DESKTOP_GROUP, NULL, NULL);
+  if (keys == NULL)
+    return;
+
+  key_len = strlen (key);
+
+  for (i = 0; keys[i] != NULL; i++)
+    {
+      size_t len;
+
+      if (strncmp (keys[i], key, key_len) != 0)
+        continue;
+
+      len = strlen (keys[i]);
+
+      if (len == key_len || keys[i][key_len] == '[')
+        {
+          g_key_file_remove_key (key_file,
+                                 G_KEY_FILE_DESKTOP_GROUP,
+                                 keys[i],
+                                 NULL);
+        }
+    }
+
+  g_strfreev (keys);
+}
+
+static void
+icon_changed_cb (GpEditor             *editor,
+                 GpLauncherProperties *self)
+{
+  const char *icon;
+
+  icon = gp_editor_get_icon (editor);
+
+  remove_locale_key (self->file, G_KEY_FILE_DESKTOP_KEY_ICON);
+
+  if (icon != NULL && *icon != '\0')
+    {
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_ICON,
+                             icon);
+    }
+
+  launcher_changed (self);
+}
+
+static void
+type_changed_cb (GpEditor             *editor,
+                 GpLauncherProperties *self)
+{
+  GpEditorType type;
+  const char *type_key;
+  const char *command;
+
+  type = gp_editor_get_editor_type (editor);
+  command = gp_editor_get_command (editor);
+  type_key = NULL;
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      type_key = G_KEY_FILE_DESKTOP_TYPE_APPLICATION;
+    }
+  else if (type == GP_EDITOR_TYPE_DIRECTORY ||
+           type == GP_EDITOR_TYPE_FILE)
+    {
+      type_key = G_KEY_FILE_DESKTOP_TYPE_LINK;
+    }
+
+  g_key_file_set_string (self->file,
+                         G_KEY_FILE_DESKTOP_GROUP,
+                         G_KEY_FILE_DESKTOP_KEY_TYPE,
+                         type_key);
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      g_key_file_remove_key (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_URL,
+                             NULL);
+
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_EXEC,
+                             command);
+
+      if (type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+        {
+          g_key_file_set_boolean (self->file,
+                                 G_KEY_FILE_DESKTOP_GROUP,
+                                 G_KEY_FILE_DESKTOP_KEY_TERMINAL,
+                                 TRUE);
+        }
+      else
+        {
+          g_key_file_remove_key (self->file,
+                                 G_KEY_FILE_DESKTOP_GROUP,
+                                 G_KEY_FILE_DESKTOP_KEY_TERMINAL,
+                                 NULL);
+        }
+    }
+  else if (type == GP_EDITOR_TYPE_DIRECTORY ||
+           type == GP_EDITOR_TYPE_FILE)
+    {
+      g_key_file_remove_key (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_TERMINAL,
+                             NULL);
+
+      g_key_file_remove_key (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_EXEC,
+                             NULL);
+
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_URL,
+                             command);
+    }
+  else
+    {
+      g_assert_not_reached ();
+    }
+
+  launcher_changed (self);
+}
+
+static void
+name_changed_cb (GpEditor             *editor,
+                 GpLauncherProperties *self)
+{
+  const char *name;
+
+  name = gp_editor_get_name (editor);
+
+  remove_locale_key (self->file, "X-GNOME-FullName");
+  remove_locale_key (self->file, G_KEY_FILE_DESKTOP_KEY_NAME);
+
+  if (name != NULL && *name != '\0')
+    {
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_NAME,
+                             name);
+    }
+
+  launcher_changed (self);
+}
+
+static void
+command_changed_cb (GpEditor             *editor,
+                    GpLauncherProperties *self)
+{
+  const char *command;
+  GpEditorType type;
+
+  command = gp_editor_get_command (editor);
+  type = gp_editor_get_editor_type (editor);
+
+  if (type == GP_EDITOR_TYPE_APPLICATION ||
+      type == GP_EDITOR_TYPE_TERMINAL_APPLICATION)
+    {
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_EXEC,
+                             command);
+    }
+  else if (type == GP_EDITOR_TYPE_DIRECTORY ||
+           type == GP_EDITOR_TYPE_FILE)
+    {
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_URL,
+                             command);
+    }
+  else
+    {
+      g_assert_not_reached ();
+    }
+
+  launcher_changed (self);
+}
+
+static void
+comment_changed_cb (GpEditor             *editor,
+                    GpLauncherProperties *self)
+{
+  const char *comment;
+
+  comment = gp_editor_get_comment (editor);
+
+  remove_locale_key (self->file, G_KEY_FILE_DESKTOP_KEY_COMMENT);
+
+  if (comment != NULL && *comment != '\0')
+    {
+      g_key_file_set_string (self->file,
+                             G_KEY_FILE_DESKTOP_GROUP,
+                             G_KEY_FILE_DESKTOP_KEY_COMMENT,
+                             comment);
+    }
+
+  launcher_changed (self);
+}
+
+static void
+fill_editor_from_file (GpLauncherProperties *self,
+                       GKeyFile             *key_file)
+{
+  char *icon;
+  char *type_string;
+  char *name;
+  char *command;
+  char *comment;
+  gboolean terminal;
+  GpEditorType type;
+
+  icon = NULL;
+  type_string = NULL;
+  name = NULL;
+  command = NULL;
+  comment = NULL;
+
+  if (!gp_launcher_read_from_key_file (key_file,
+                                       &icon,
+                                       &type_string,
+                                       &name,
+                                       &command,
+                                       &comment,
+                                       NULL))
+    return;
+
+  terminal = g_key_file_get_boolean (key_file,
+                                     G_KEY_FILE_DESKTOP_GROUP,
+                                     G_KEY_FILE_DESKTOP_KEY_TERMINAL,
+                                     NULL);
+
+  if (g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) == 0)
+    {
+      if (terminal)
+        type = GP_EDITOR_TYPE_TERMINAL_APPLICATION;
+      else
+        type = GP_EDITOR_TYPE_APPLICATION;
+    }
+  else if (g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_LINK) == 0)
+    {
+      GFile *file;
+      char *path;
+
+      file = g_file_new_for_uri (command);
+      path = g_file_get_path (file);
+      g_object_unref (file);
+
+      if (file != NULL && g_file_test (path, G_FILE_TEST_IS_DIR))
+        type = GP_EDITOR_TYPE_DIRECTORY;
+      else
+        type = GP_EDITOR_TYPE_FILE;
+
+      g_free (path);
+    }
+  else
+    {
+      type = GP_EDITOR_TYPE_NONE;
+    }
+
+  gp_editor_set_icon (self->editor, icon);
+  gp_editor_set_editor_type (self->editor, type);
+  gp_editor_set_name (self->editor, name);
+  gp_editor_set_command (self->editor, command);
+  gp_editor_set_comment (self->editor, comment);
+
+  g_free (icon);
+  g_free (type_string);
+  g_free (name);
+  g_free (command);
+  g_free (comment);
+}
+
+static void
+response_cb (GtkWidget            *widget,
+             int                   response_id,
+             GpLauncherProperties *self)
+{
+  if (response_id == GTK_RESPONSE_CLOSE)
+    {
+      if (launcher_save (self, TRUE))
+        gtk_widget_destroy (widget);
+    }
+  else if (response_id == GP_RESPONSE_REVERT)
+    {
+      fill_editor_from_file (self, self->revert_file);
+      gtk_dialog_set_response_sensitive (GTK_DIALOG (self),
+                                         GP_RESPONSE_REVERT,
+                                         FALSE);
+    }
+  else if (response_id == GTK_RESPONSE_DELETE_EVENT)
+    {
+      fill_editor_from_file (self, self->revert_file);
+      launcher_save (self, FALSE);
+    }
+}
+
+static void
+gp_launcher_properties_constructed (GObject *object)
+{
+  GpLauncherProperties *self;
+  char *location;
+  GError *error;
+  GKeyFileFlags flags;
+
+  self = GP_LAUNCHER_PROPERTIES (object);
+
+  G_OBJECT_CLASS (gp_launcher_properties_parent_class)->constructed (object);
+
+  location = g_settings_get_string (self->settings, "location");
+
+  if (!g_path_is_absolute (location))
+    {
+      char *launchers_dir;
+      char *filename;
+
+      launchers_dir = gp_launcher_get_launchers_dir ();
+
+      filename = g_build_filename (launchers_dir, location, NULL);
+      g_free (launchers_dir);
+
+      g_free (location);
+      location = filename;
+    }
+
+  flags = G_KEY_FILE_KEEP_COMMENTS | G_KEY_FILE_KEEP_TRANSLATIONS;
+
+  self->file = g_key_file_new ();
+  self->revert_file = g_key_file_new ();
+
+  error = NULL;
+  g_key_file_load_from_file (self->file, location, flags, &error);
+
+  if (error != NULL)
+    {
+      g_warning ("Failed to load key file “%s”: %s", location, error->message);
+      g_error_free (error);
+      g_free (location);
+
+      return;
+    }
+
+  g_key_file_load_from_file (self->revert_file, location, flags, NULL);
+  g_free (location);
+
+  fill_editor_from_file (self, self->file);
+
+  g_signal_connect (self->editor,
+                    "icon-changed",
+                    G_CALLBACK (icon_changed_cb),
+                    self);
+
+  g_signal_connect (self->editor,
+                    "type-changed",
+                    G_CALLBACK (type_changed_cb),
+                    self);
+
+  g_signal_connect (self->editor,
+                    "name-changed",
+                    G_CALLBACK (name_changed_cb),
+                    self);
+
+  g_signal_connect (self->editor,
+                    "command-changed",
+                    G_CALLBACK (command_changed_cb),
+                    self);
+
+  g_signal_connect (self->editor,
+                    "comment-changed",
+                    G_CALLBACK (comment_changed_cb),
+                    self);
+}
+
+static void
+gp_launcher_properties_dispose (GObject *object)
+{
+  GpLauncherProperties *self;
+
+  self = GP_LAUNCHER_PROPERTIES (object);
+
+  if (self->save_id != 0)
+    {
+      g_source_remove (self->save_id);
+      self->save_id = 0;
+    }
+
+  g_clear_object (&self->settings);
+
+  g_clear_pointer (&self->file, g_key_file_unref);
+  g_clear_pointer (&self->revert_file, g_key_file_unref);
+
+  G_OBJECT_CLASS (gp_launcher_properties_parent_class)->dispose (object);
+}
+
+static void
+gp_launcher_properties_set_property (GObject      *object,
+                                     guint         property_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  GpLauncherProperties *self;
+
+  self = GP_LAUNCHER_PROPERTIES (object);
+
+  switch (property_id)
+    {
+      case PROP_SETTINGS:
+        g_assert (self->settings == NULL);
+        self->settings = g_value_dup_object (value);
+        break;
+
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+        break;
+    }
+}
+
+static void
+install_properties (GObjectClass *object_class)
+{
+  properties_properties[PROP_SETTINGS] =
+    g_param_spec_object ("settings",
+                         "settings",
+                         "settings",
+                         G_TYPE_SETTINGS,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
+                         G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class,
+                                     LAST_PROP,
+                                     properties_properties);
+}
+
+static void
+gp_launcher_properties_class_init (GpLauncherPropertiesClass *self_class)
+{
+  GObjectClass *object_class;
+
+  object_class = G_OBJECT_CLASS (self_class);
+
+  object_class->constructed = gp_launcher_properties_constructed;
+  object_class->dispose = gp_launcher_properties_dispose;
+  object_class->set_property = gp_launcher_properties_set_property;
+
+  install_properties (object_class);
+}
+
+static void
+gp_launcher_properties_init (GpLauncherProperties *self)
+{
+  GtkWidget *content_area;
+  GtkWidget *editor;
+
+  content_area = gtk_dialog_get_content_area (GTK_DIALOG (self));
+  gtk_container_set_border_width (GTK_CONTAINER (content_area), 12);
+  gtk_box_set_spacing (GTK_BOX (content_area), 6);
+
+  editor = gp_editor_new (TRUE);
+  self->editor = GP_EDITOR (editor);
+
+  gtk_container_add (GTK_CONTAINER (content_area), editor);
+  gtk_widget_show (editor);
+
+  self->revert_button = gtk_dialog_add_button (GTK_DIALOG (self),
+                                               _("_Revert"),
+                                               GP_RESPONSE_REVERT);
+
+  gtk_dialog_set_response_sensitive (GTK_DIALOG (self),
+                                     GP_RESPONSE_REVERT,
+                                     FALSE);
+
+  gtk_dialog_add_button (GTK_DIALOG (self),
+                         _("_Close"),
+                         GTK_RESPONSE_CLOSE);
+
+  gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_CLOSE);
+
+  g_signal_connect (self, "response", G_CALLBACK (response_cb), self);
+}
+
+GtkWidget *
+gp_launcher_properties_new (GSettings *settings)
+{
+  return g_object_new (GP_TYPE_LAUNCHER_PROPERTIES,
+                       "title", _("Launcher Properties"),
+                       "settings", settings,
+                       NULL);
+}
diff --git a/modules/launcher/gp-launcher-properties.h b/modules/launcher/gp-launcher-properties.h
new file mode 100644
index 000000000..f95d3c02b
--- /dev/null
+++ b/modules/launcher/gp-launcher-properties.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_LAUNCHER_PROPERTIES_H
+#define GP_LAUNCHER_PROPERTIES_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GP_TYPE_LAUNCHER_PROPERTIES (gp_launcher_properties_get_type ())
+G_DECLARE_FINAL_TYPE (GpLauncherProperties, gp_launcher_properties,
+                      GP, LAUNCHER_PROPERTIES, GtkDialog)
+
+GtkWidget *gp_launcher_properties_new (GSettings *settings);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/gp-launcher-utils.c b/modules/launcher/gp-launcher-utils.c
new file mode 100644
index 000000000..484580332
--- /dev/null
+++ b/modules/launcher/gp-launcher-utils.c
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "gp-launcher-utils.h"
+
+#include <glib/gi18n-lib.h>
+
+static void
+error_response_cb (GtkWidget *widget,
+                   int        response_id,
+                   gpointer   user_data)
+{
+  gtk_widget_destroy (widget);
+}
+
+gboolean
+gp_launcher_read_from_key_file (GKeyFile  *key_file,
+                                char     **icon,
+                                char     **type,
+                                char     **name,
+                                char     **command,
+                                char     **comment,
+                                char     **error)
+{
+  char *start_group;
+  char *type_string;
+
+  g_return_val_if_fail (key_file != NULL, FALSE);
+  g_return_val_if_fail (icon == NULL || *icon == NULL, FALSE);
+  g_return_val_if_fail (type == NULL || *type == NULL, FALSE);
+  g_return_val_if_fail (name == NULL || *name == NULL, FALSE);
+  g_return_val_if_fail (command == NULL || *command == NULL, FALSE);
+  g_return_val_if_fail (comment == NULL || *comment == NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  start_group = g_key_file_get_start_group (key_file);
+  if (start_group == NULL ||
+      g_strcmp0 (start_group, G_KEY_FILE_DESKTOP_GROUP) != 0)
+    {
+      if (error != NULL)
+        *error = g_strdup_printf (_("Launcher does not start with required “%s” group."),
+                                  G_KEY_FILE_DESKTOP_GROUP);
+
+      g_free (start_group);
+      return FALSE;
+    }
+
+  g_free (start_group);
+  type_string = g_key_file_get_string (key_file,
+                                       G_KEY_FILE_DESKTOP_GROUP,
+                                       G_KEY_FILE_DESKTOP_KEY_TYPE,
+                                       NULL);
+
+  if (type_string == NULL ||
+      (g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) != 0 &&
+       g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_LINK) != 0))
+    {
+      if (error != NULL)
+        *error = g_strdup_printf (_("Launcher has invalid Type key value “%s”."),
+                                  type_string != NULL ? type_string : "(null)");
+
+      g_free (type_string);
+      return FALSE;
+    }
+
+  if (icon != NULL)
+    {
+      *icon = g_key_file_get_locale_string (key_file,
+                                            G_KEY_FILE_DESKTOP_GROUP,
+                                            G_KEY_FILE_DESKTOP_KEY_ICON,
+                                            NULL,
+                                            NULL);
+    }
+
+  if (type != NULL)
+    *type = g_strdup (type_string);
+
+  if (name != NULL)
+    {
+      *name = g_key_file_get_locale_string (key_file,
+                                            G_KEY_FILE_DESKTOP_GROUP,
+                                            "X-GNOME-FullName",
+                                            NULL,
+                                            NULL);
+
+      if (*name == NULL)
+        {
+          *name = g_key_file_get_locale_string (key_file,
+                                                G_KEY_FILE_DESKTOP_GROUP,
+                                                G_KEY_FILE_DESKTOP_KEY_NAME,
+                                                NULL,
+                                                NULL);
+        }
+    }
+
+  if (command != NULL)
+    {
+      if (g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) == 0)
+        {
+          *command = g_key_file_get_string (key_file,
+                                            G_KEY_FILE_DESKTOP_GROUP,
+                                            G_KEY_FILE_DESKTOP_KEY_EXEC,
+                                            NULL);
+        }
+      else if (g_strcmp0 (type_string, G_KEY_FILE_DESKTOP_TYPE_LINK) == 0)
+        {
+          *command = g_key_file_get_string (key_file,
+                                            G_KEY_FILE_DESKTOP_GROUP,
+                                            G_KEY_FILE_DESKTOP_KEY_URL,
+                                            NULL);
+        }
+    }
+
+  if (comment != NULL)
+    {
+      *comment = g_key_file_get_locale_string (key_file,
+                                               G_KEY_FILE_DESKTOP_GROUP,
+                                               G_KEY_FILE_DESKTOP_KEY_COMMENT,
+                                               NULL,
+                                               NULL);
+    }
+
+  g_free (type_string);
+
+  return TRUE;
+}
+
+gboolean
+gp_launcher_validate (const char  *icon,
+                      const char  *type,
+                      const char  *name,
+                      const char  *command,
+                      const char  *comment,
+                      char       **error)
+{
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (icon == NULL || *icon == '\0')
+    {
+      if (error != NULL)
+        *error = g_strdup (_("The icon of the launcher is not set."));
+
+      return FALSE;
+    }
+
+  if (type == NULL || *type == '\0')
+    {
+      if (error != NULL)
+        *error = g_strdup (_("The type of the launcher is not set."));
+
+      return FALSE;
+    }
+
+  if (g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) != 0 &&
+      g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_LINK) != 0)
+    {
+      if (error != NULL)
+        *error = g_strdup_printf (_("The type of the launcher must be “%s” or “%s”."),
+                                  G_KEY_FILE_DESKTOP_TYPE_APPLICATION,
+                                  G_KEY_FILE_DESKTOP_TYPE_LINK);
+
+      return FALSE;
+    }
+
+  if (name == NULL || *name == '\0')
+    {
+      if (error != NULL)
+        *error = g_strdup (_("The name of the launcher is not set."));
+
+      return FALSE;
+    }
+
+  if (command == NULL || *command == '\0')
+    {
+      if (g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_APPLICATION) == 0)
+        {
+          if (error != NULL)
+            *error = g_strdup (_("The command of the launcher is not set."));
+        }
+      else if (g_strcmp0 (type, G_KEY_FILE_DESKTOP_TYPE_LINK) == 0)
+        {
+          if (error != NULL)
+            *error = g_strdup (_("The location of the launcher is not set."));
+        }
+
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+gboolean
+gp_launcher_validate_key_file (GKeyFile  *key_file,
+                               char     **error)
+{
+  char *icon;
+  char *type;
+  char *name;
+  char *command;
+  char *comment;
+  gboolean valid;
+
+  g_return_val_if_fail (key_file != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  icon = NULL;
+  type = NULL;
+  name = NULL;
+  command = NULL;
+  comment = NULL;
+
+  if (!gp_launcher_read_from_key_file (key_file,
+                                       &icon,
+                                       &type,
+                                       &name,
+                                       &command,
+                                       &comment,
+                                       error))
+    return FALSE;
+
+  valid = gp_launcher_validate (icon, type, name, command, comment, error);
+
+  g_free (icon);
+  g_free (type);
+  g_free (name);
+  g_free (command);
+  g_free (comment);
+
+  return valid;
+}
+
+char *
+gp_launcher_get_launchers_dir (void)
+{
+  char *dir;
+
+  dir = g_build_filename (g_get_user_config_dir (),
+                          "gnome-panel",
+                          "launchers",
+                          NULL);
+
+  g_mkdir_with_parents (dir, 0700);
+
+  return dir;
+}
+
+char *
+gp_launcher_get_unique_filename (void)
+{
+  char *launchers_dir;
+  char *filename;
+
+  launchers_dir = gp_launcher_get_launchers_dir ();
+  filename = NULL;
+
+  do
+    {
+      char *uuid;
+      char *desktop;
+
+      g_free (filename);
+
+      uuid = g_uuid_string_random ();
+      desktop = g_strdup_printf ("%s.desktop", uuid);
+      g_free (uuid);
+
+      filename = g_build_filename (launchers_dir, desktop, NULL);
+      g_free (desktop);
+    }
+  while (g_file_test (filename, G_FILE_TEST_EXISTS));
+
+  g_free (launchers_dir);
+
+  return filename;
+}
+
+void
+gp_launcher_show_error_message (GtkWindow  *parent,
+                                const char *primary_text,
+                                const char *secondary_text)
+{
+  GtkWidget *dialog;
+
+  dialog = gtk_message_dialog_new (parent,
+                                   GTK_DIALOG_DESTROY_WITH_PARENT,
+                                   GTK_MESSAGE_ERROR,
+                                   GTK_BUTTONS_CLOSE,
+                                   "%s",
+                                   primary_text);
+
+  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
+                                            "%s",
+                                            secondary_text);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (error_response_cb), NULL);
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
diff --git a/modules/launcher/gp-launcher-utils.h b/modules/launcher/gp-launcher-utils.h
new file mode 100644
index 000000000..702e3d61e
--- /dev/null
+++ b/modules/launcher/gp-launcher-utils.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 Alberts Muktupāvels
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GP_LAUNCHER_UTILS_H
+#define GP_LAUNCHER_UTILS_H
+
+#include <glib.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+gboolean  gp_launcher_read_from_key_file  (GKeyFile    *key_file,
+                                           char       **icon,
+                                           char       **type,
+                                           char       **name,
+                                           char       **command,
+                                           char       **comment,
+                                           char       **error);
+
+gboolean  gp_launcher_validate            (const char  *icon,
+                                           const char  *type,
+                                           const char  *name,
+                                           const char  *command,
+                                           const char  *comment,
+                                           char       **error);
+
+gboolean  gp_launcher_validate_key_file   (GKeyFile    *key_file,
+                                           char       **error);
+
+char     *gp_launcher_get_launchers_dir   (void);
+
+char     *gp_launcher_get_unique_filename (void);
+
+void      gp_launcher_show_error_message  (GtkWindow   *parent,
+                                           const char  *primary_text,
+                                           const char  *secondary_text);
+
+G_END_DECLS
+
+#endif
diff --git a/modules/launcher/launcher-menu.ui b/modules/launcher/launcher-menu.ui
new file mode 100644
index 000000000..d84bf05aa
--- /dev/null
+++ b/modules/launcher/launcher-menu.ui
@@ -0,0 +1,14 @@
+<interface>
+  <menu id="launcher-menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Launch</attribute>
+        <attribute name="action">launcher.launch</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Properties</attribute>
+        <attribute name="action">launcher.properties</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/modules/launcher/launcher.gresource.xml b/modules/launcher/launcher.gresource.xml
new file mode 100644
index 000000000..f5180a526
--- /dev/null
+++ b/modules/launcher/launcher.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/gnome-panel/modules/launcher">
+    <file compressed="true">custom-launcher-menu.ui</file>
+    <file compressed="true">gp-icon-name-chooser.ui</file>
+    <file compressed="true">launcher-menu.ui</file>
+  </gresource>
+</gresources>
diff --git a/modules/launcher/org.gnome.gnome-panel.applet.launcher.gschema.xml 
b/modules/launcher/org.gnome.gnome-panel.applet.launcher.gschema.xml
new file mode 100644
index 000000000..1222d00da
--- /dev/null
+++ b/modules/launcher/org.gnome.gnome-panel.applet.launcher.gschema.xml
@@ -0,0 +1,9 @@
+<schemalist gettext-domain="gnome-panel">
+  <schema id="org.gnome.gnome-panel.applet.launcher">
+    <key name="location" type="s">
+      <default>''</default>
+      <summary>Launcher location</summary>
+      <description>The location of the .desktop file describing the launcher.</description>
+    </key>
+  </schema>
+</schemalist>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 20f72e907..1daae1bd2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -51,6 +51,15 @@ modules/fish/fish-applet.c
 modules/fish/fish-module.c
 modules/fish/fish-menu.ui
 modules/fish/fish.ui
+modules/launcher/custom-launcher-menu.ui
+modules/launcher/gp-editor.c
+modules/launcher/gp-icon-name-chooser.ui
+modules/launcher/gp-launcher-applet.c
+modules/launcher/gp-launcher-module.c
+modules/launcher/gp-launcher-properties.c
+modules/launcher/gp-launcher-utils.c
+modules/launcher/launcher-menu.ui
+modules/launcher/org.gnome.gnome-panel.applet.launcher.gschema.xml
 modules/menu/gp-bookmarks.c
 modules/menu/gp-lock-logout.c
 modules/menu/gp-menu-bar-applet.c


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