[gnome-panel/wip/muktupavels/launcher: 1/2] launcher: new module for launcher applet
- From: Alberts Muktupāvels <muktupavels src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-panel/wip/muktupavels/launcher: 1/2] launcher: new module for launcher applet
- Date: Sun, 12 Apr 2020 23:51:15 +0000 (UTC)
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]