[gnome-builder/wip/chergert/shellcmd] shellcmd: add command editing to preferences



commit e9c3d247c3ecceacb3c57e58470502a84624d422
Author: Christian Hergert <chergert redhat com>
Date:   Fri Aug 9 17:39:03 2019 -0700

    shellcmd: add command editing to preferences

 src/libide/themes/themes/shared.css                |   4 +
 src/plugins/shellcmd/gbp-shellcmd-command-editor.c | 283 +++++++++++++++++++++
 src/plugins/shellcmd/gbp-shellcmd-command-editor.h |  37 +++
 .../shellcmd/gbp-shellcmd-command-editor.ui        | 179 +++++++++++++
 src/plugins/shellcmd/gbp-shellcmd-command-model.c  | 139 +++++++++-
 src/plugins/shellcmd/gbp-shellcmd-command-model.h  |   4 +
 .../shellcmd/gbp-shellcmd-command-provider.c       |   2 +-
 src/plugins/shellcmd/gbp-shellcmd-command-row.c    |  93 +++++++
 src/plugins/shellcmd/gbp-shellcmd-command-row.h    |  36 +++
 src/plugins/shellcmd/gbp-shellcmd-command-row.ui   |  31 +++
 src/plugins/shellcmd/gbp-shellcmd-command.c        | 109 +++++++-
 src/plugins/shellcmd/gbp-shellcmd-command.h        |   5 +-
 src/plugins/shellcmd/gbp-shellcmd-list.c           | 186 ++++++++++++++
 src/plugins/shellcmd/gbp-shellcmd-list.h           |  35 +++
 .../shellcmd/gbp-shellcmd-preferences-addin.c      | 151 +++++++++++
 .../shellcmd/gbp-shellcmd-preferences-addin.h      |  31 +++
 src/plugins/shellcmd/meson.build                   |   4 +
 src/plugins/shellcmd/shellcmd-plugin.c             |   4 +
 src/plugins/shellcmd/shellcmd.gresource.xml        |   2 +
 19 files changed, 1317 insertions(+), 18 deletions(-)
---
diff --git a/src/libide/themes/themes/shared.css b/src/libide/themes/themes/shared.css
index fcf9ea9c9..c28b2d76d 100644
--- a/src/libide/themes/themes/shared.css
+++ b/src/libide/themes/themes/shared.css
@@ -149,3 +149,7 @@ dzlstacklist .stack-header {
   background-color: @content_view_bg;
   background-image: none;
 }
+
+row:selected label.keycap {
+  color: @theme_fg_color;
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-editor.c 
b/src/plugins/shellcmd/gbp-shellcmd-command-editor.c
new file mode 100644
index 000000000..7f75535aa
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-editor.c
@@ -0,0 +1,283 @@
+/* gbp-shellcmd-command-editor.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-shellcmd-command-editor"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-shellcmd-application-addin.h"
+#include "gbp-shellcmd-command-editor.h"
+#include "gbp-shellcmd-command-model.h"
+
+struct _GbpShellcmdCommandEditor
+{
+  GtkBin                parent_instance;
+
+  DzlBindingGroup      *bindings;
+
+  IdeEnvironmentEditor *environment;
+  DzlShortcutLabel     *shortcut;
+  GtkEntry             *title;
+  GtkEntry             *command;
+  GtkEntry             *directory;
+  GtkButton            *change;
+  GtkButton            *delete;
+  GtkRadioButton       *host;
+  GtkRadioButton       *build;
+  GtkRadioButton       *run;
+};
+
+G_DEFINE_TYPE (GbpShellcmdCommandEditor, gbp_shellcmd_command_editor, GTK_TYPE_BIN)
+
+static GbpShellcmdCommandModel *
+get_model (void)
+{
+  GbpShellcmdApplicationAddin *app_addin;
+  GbpShellcmdCommandModel *model;
+
+  app_addin = ide_application_find_addin_by_module_name (NULL, "shellcmd");
+  g_assert (GBP_IS_SHELLCMD_APPLICATION_ADDIN (app_addin));
+
+  model = gbp_shellcmd_application_addin_get_model (app_addin);
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (model));
+
+  return model;
+}
+
+static void
+on_dialog_response_cb (GbpShellcmdCommandEditor *self,
+                       gint                      response,
+                       DzlShortcutAccelDialog   *dialog)
+{
+  g_assert (GBP_IS_SHELLCMD_COMMAND_EDITOR (self));
+  g_assert (DZL_IS_SHORTCUT_ACCEL_DIALOG (dialog));
+
+
+  if (response == GTK_RESPONSE_ACCEPT)
+    {
+      GbpShellcmdCommand *command = GBP_SHELLCMD_COMMAND (dzl_binding_group_get_source (self->bindings));
+
+      if (command != NULL)
+        {
+          g_autofree gchar *accel = dzl_shortcut_accel_dialog_get_accelerator (dialog);
+          gbp_shellcmd_command_set_shortcut (command, accel);
+        }
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+on_delete_shortcut_cb (GbpShellcmdCommandEditor *self,
+                       GtkButton                *button)
+{
+  GbpShellcmdCommand *command;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_EDITOR (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  command = GBP_SHELLCMD_COMMAND (dzl_binding_group_get_source (self->bindings));
+
+  gbp_shellcmd_command_editor_set_command (self, NULL);
+
+  if (command != NULL)
+    gbp_shellcmd_command_model_remove (get_model (), command);
+}
+
+static void
+on_change_shortcut_cb (GbpShellcmdCommandEditor *self,
+                       GtkButton                *button)
+{
+  GbpShellcmdCommand *command;
+  g_autofree gchar *title = NULL;
+  GtkWidget *dialog;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_EDITOR (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  command = GBP_SHELLCMD_COMMAND (dzl_binding_group_get_source (self->bindings));
+
+  if (command == NULL)
+    return;
+
+  title = ide_command_get_title (IDE_COMMAND (command));
+
+  dialog = g_object_new (DZL_TYPE_SHORTCUT_ACCEL_DIALOG,
+                         "modal", TRUE,
+                         "shortcut-title", title,
+                         "title", _("Change Shortcut"),
+                         "transient-for", gtk_widget_get_toplevel (GTK_WIDGET (self)),
+                         "use-header-bar", TRUE,
+                         NULL);
+
+  g_signal_connect_object (dialog,
+                           "response",
+                           G_CALLBACK (on_dialog_response_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+on_radio_toggled_cb (GbpShellcmdCommandEditor *self,
+                     GtkRadioButton           *button)
+{
+  GbpShellcmdCommand *command;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_EDITOR (self));
+  g_assert (GTK_IS_RADIO_BUTTON (button));
+
+  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
+    return;
+
+  command = GBP_SHELLCMD_COMMAND (dzl_binding_group_get_source (self->bindings));
+  if (command == NULL)
+    return;
+
+  if (button == self->host)
+    gbp_shellcmd_command_set_locality (command, GBP_SHELLCMD_COMMAND_LOCALITY_HOST);
+  else if (button == self->run)
+    gbp_shellcmd_command_set_locality (command, GBP_SHELLCMD_COMMAND_LOCALITY_RUN);
+  else if (button == self->build)
+    gbp_shellcmd_command_set_locality (command, GBP_SHELLCMD_COMMAND_LOCALITY_BUILD);
+}
+
+static void
+gbp_shellcmd_command_editor_destroy (GtkWidget *widget)
+{
+  GbpShellcmdCommandEditor *self = (GbpShellcmdCommandEditor *)widget;
+
+  if (self->bindings != NULL)
+    {
+      dzl_binding_group_set_source (self->bindings, NULL);
+      g_clear_object (&self->bindings);
+    }
+
+  GTK_WIDGET_CLASS (gbp_shellcmd_command_editor_parent_class)->destroy (widget);
+}
+
+static void
+gbp_shellcmd_command_editor_class_init (GbpShellcmdCommandEditorClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = gbp_shellcmd_command_editor_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/shellcmd/gbp-shellcmd-command-editor.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, build);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, change);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, command);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, delete);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, directory);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, environment);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, host);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, run);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, shortcut);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandEditor, title);
+
+  g_type_ensure (IDE_TYPE_ENVIRONMENT_EDITOR);
+}
+
+static void
+gbp_shellcmd_command_editor_init (GbpShellcmdCommandEditor *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "title", self->title, "text", G_BINDING_BIDIRECTIONAL);
+  dzl_binding_group_bind (self->bindings, "command", self->command, "text", G_BINDING_BIDIRECTIONAL);
+  dzl_binding_group_bind (self->bindings, "shortcut", self->shortcut, "accelerator", 
G_BINDING_BIDIRECTIONAL);
+  dzl_binding_group_bind (self->bindings, "cwd", self->directory, "text", G_BINDING_BIDIRECTIONAL);
+  dzl_binding_group_bind (self->bindings, "environment", self->environment, "environment", 
G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (self->change,
+                           "clicked",
+                           G_CALLBACK (on_change_shortcut_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->delete,
+                           "clicked",
+                           G_CALLBACK (on_delete_shortcut_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->run,
+                           "toggled",
+                           G_CALLBACK (on_radio_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->build,
+                           "toggled",
+                           G_CALLBACK (on_radio_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->host,
+                           "toggled",
+                           G_CALLBACK (on_radio_toggled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+void
+gbp_shellcmd_command_editor_set_command (GbpShellcmdCommandEditor *self,
+                                         GbpShellcmdCommand       *command)
+{
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_EDITOR (self));
+  g_return_if_fail (!command || GBP_IS_SHELLCMD_COMMAND (command));
+
+  dzl_binding_group_set_source (self->bindings, command);
+
+  gtk_widget_set_visible (GTK_WIDGET (self), command != NULL);
+
+  if (command != NULL)
+    {
+      GbpShellcmdCommandLocality locality;
+      GtkRadioButton *radio = NULL;
+
+      locality = gbp_shellcmd_command_get_locality (command);
+
+      if (locality == GBP_SHELLCMD_COMMAND_LOCALITY_HOST)
+        radio = self->host;
+      else if (locality == GBP_SHELLCMD_COMMAND_LOCALITY_BUILD)
+        radio = self->build;
+      else if (locality == GBP_SHELLCMD_COMMAND_LOCALITY_RUN)
+        radio = self->run;
+
+      if (radio != NULL)
+        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (radio), TRUE);
+
+      gtk_widget_grab_focus (GTK_WIDGET (self->title));
+    }
+}
+
+GtkWidget *
+gbp_shellcmd_command_editor_new (void)
+{
+  return g_object_new (GBP_TYPE_SHELLCMD_COMMAND_EDITOR, NULL);
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-editor.h 
b/src/plugins/shellcmd/gbp-shellcmd-command-editor.h
new file mode 100644
index 000000000..4492ebf17
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-editor.h
@@ -0,0 +1,37 @@
+/* gbp-shellcmd-command-editor.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gbp-shellcmd-command.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SHELLCMD_COMMAND_EDITOR (gbp_shellcmd_command_editor_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpShellcmdCommandEditor, gbp_shellcmd_command_editor, GBP, SHELLCMD_COMMAND_EDITOR, 
GtkBin)
+
+GtkWidget *gbp_shellcmd_command_editor_new         (void);
+void       gbp_shellcmd_command_editor_set_command (GbpShellcmdCommandEditor *self,
+                                                    GbpShellcmdCommand       *command);
+
+G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-editor.ui 
b/src/plugins/shellcmd/gbp-shellcmd-command-editor.ui
new file mode 100644
index 000000000..92fd99127
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-editor.ui
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpShellcmdCommandEditor" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Title</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="title">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Shell Command</property>
+            <property name="margin-top">10</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="command">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">The command will be executed using a shell like 
“/bin/sh -c". You may use variable expansion like “$SHELL”. Both “$SRCDIR” and “$BUILDDIR” are automatically 
set for the command.</property>
+            <property name="visible">true</property>
+            <property name="wrap">true</property>
+            <property name="width-chars">10</property>
+            <property name="max-width-chars">10</property>
+            <property name="xalign">0</property>
+            <attributes>
+              <attribute name="scale" value="0.8333"/>
+            </attributes>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Command Locality</property>
+            <property name="margin-top">10</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="halign">center</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkRadioButton" id="host">
+                <property name="draw-indicator">false</property>
+                <property name="label" translatable="yes">On Host</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkRadioButton" id="build">
+                <property name="group">host</property>
+                <property name="draw-indicator">false</property>
+                <property name="label" translatable="yes">In Build Environment</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkRadioButton" id="run">
+                <property name="group">host</property>
+                <property name="draw-indicator">false</property>
+                <property name="label" translatable="yes">In Runtime Environment</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Directory</property>
+            <property name="margin-top">10</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="directory">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">If the directory is not an absolute-path, it will be 
relative to the source or build directory depending on the command locality.</property>
+            <property name="visible">true</property>
+            <property name="wrap">true</property>
+            <property name="width-chars">10</property>
+            <property name="max-width-chars">10</property>
+            <property name="xalign">0</property>
+            <attributes>
+              <attribute name="scale" value="0.8333"/>
+            </attributes>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Keyboard Shortcut</property>
+            <property name="margin-top">10</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">12</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton" id="change">
+                <property name="label" translatable="yes">Change</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="DzlShortcutLabel" id="shortcut">
+                <property name="valign">start</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Environment</property>
+            <property name="margin-top">10</property>
+            <property name="visible">true</property>
+            <property name="xalign">0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkFrame">
+            <property name="shadow-type">in</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="IdeEnvironmentEditor" id="environment">
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="delete">
+            <property name="halign">end</property>
+            <property name="label" translatable="yes">Delete Command</property>
+            <property name="visible">true</property>
+            <property name="margin-top">12</property>
+            <style>
+              <class name="destructive-action"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-model.c 
b/src/plugins/shellcmd/gbp-shellcmd-command-model.c
index 9ff5e0d0d..368b53248 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-model.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-model.c
@@ -22,6 +22,7 @@
 
 #include "config.h"
 
+#include <glib/gstdio.h>
 #include <libide-core.h>
 #include <libide-sourceview.h>
 #include <libide-threading.h>
@@ -32,8 +33,11 @@
 struct _GbpShellcmdCommandModel
 {
   GObject    parent_instance;
+
   GPtrArray *items;
   GKeyFile  *keyfile;
+
+  guint      queue_save;
 };
 
 static void list_model_iface_init (GListModelInterface *iface);
@@ -41,12 +45,59 @@ static void list_model_iface_init (GListModelInterface *iface);
 G_DEFINE_TYPE_WITH_CODE (GbpShellcmdCommandModel, gbp_shellcmd_command_model, G_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
+static gboolean
+gbp_shellcmd_command_model_queue_save_cb (gpointer data)
+{
+  GbpShellcmdCommandModel *self = data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+
+  self->queue_save = 0;
+
+  if (!gbp_shellcmd_command_model_save (self, NULL, &error))
+    g_warning ("Failed to save external-commands: %s", error->message);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_shellcmd_command_model_queue_save (GbpShellcmdCommandModel *self)
+{
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+
+  g_object_ref (self);
+
+  if (self->queue_save != 0)
+    g_source_remove (self->queue_save);
+
+  self->queue_save =
+    g_timeout_add_seconds_full (G_PRIORITY_HIGH,
+                                1,
+                                gbp_shellcmd_command_model_queue_save_cb,
+                                g_object_ref (self),
+                                g_object_unref);
+
+  g_object_unref (self);
+}
+
+static void
+on_command_changed_cb (GbpShellcmdCommandModel *self,
+                       GbpShellcmdCommand      *command)
+{
+  g_assert (GBP_SHELLCMD_COMMAND_MODEL (self));
+  g_assert (GBP_SHELLCMD_COMMAND (command));
+
+  gbp_shellcmd_command_model_queue_save (self);
+}
+
 static void
 gbp_shellcmd_command_model_finalize (GObject *object)
 {
   GbpShellcmdCommandModel *self = (GbpShellcmdCommandModel *)object;
 
   g_clear_pointer (&self->items, g_ptr_array_unref);
+  g_clear_pointer (&self->keyfile, g_key_file_free);
 
   G_OBJECT_CLASS (gbp_shellcmd_command_model_parent_class)->finalize (object);
 }
@@ -63,6 +114,7 @@ static void
 gbp_shellcmd_command_model_init (GbpShellcmdCommandModel *self)
 {
   self->items = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
+  self->keyfile = g_key_file_new ();
 }
 
 GbpShellcmdCommandModel *
@@ -128,6 +180,17 @@ set_items (GbpShellcmdCommandModel *self,
   old_items = g_steal_pointer (&self->items);
   self->items = g_ptr_array_ref (items);
 
+  for (guint i = 0; i < items->len; i++)
+    {
+      GbpShellcmdCommand *command = g_ptr_array_index (items, i);
+
+      g_signal_connect_object (command,
+                               "changed",
+                               G_CALLBACK (on_command_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
+
   if (old_items->len || self->items->len)
     g_list_model_items_changed (G_LIST_MODEL (self), 0, old_items->len, self->items->len);
 }
@@ -190,10 +253,31 @@ gbp_shellcmd_command_model_save (GbpShellcmdCommandModel  *self,
                                  GCancellable             *cancellable,
                                  GError                  **error)
 {
+  g_autofree gchar *path = NULL;
+  g_auto(GStrv) groups = NULL;
+  gsize n_groups = 0;
+
   g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self), FALSE);
   g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+  g_return_val_if_fail (self->keyfile != NULL, FALSE);
 
-  return TRUE;
+  path = get_filename ();
+
+  for (guint i = 0; i < self->items->len; i++)
+    {
+      GbpShellcmdCommand *command = g_ptr_array_index (self->items, i);
+      gbp_shellcmd_command_to_key_file (command, self->keyfile);
+    }
+
+  groups = g_key_file_get_groups (self->keyfile, &n_groups);
+
+  if (n_groups == 0)
+    {
+      g_unlink (path);
+      return TRUE;
+    }
+
+  return g_key_file_save_to_file (self->keyfile, path, error);
 }
 
 /**
@@ -249,3 +333,56 @@ gbp_shellcmd_command_model_query (GbpShellcmdCommandModel *self,
         }
     }
 }
+
+void
+gbp_shellcmd_command_model_add (GbpShellcmdCommandModel *self,
+                                GbpShellcmdCommand      *command)
+{
+  guint position;
+
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (command));
+
+  g_signal_connect_object (command,
+                           "changed",
+                           G_CALLBACK (on_command_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  position = self->items->len;
+  g_ptr_array_add (self->items, g_object_ref (command));
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  gbp_shellcmd_command_model_queue_save (self);
+}
+
+void
+gbp_shellcmd_command_model_remove (GbpShellcmdCommandModel *self,
+                                   GbpShellcmdCommand      *command)
+{
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (self));
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (command));
+
+  for (guint i = 0; i < self->items->len; i++)
+    {
+      GbpShellcmdCommand *ele = g_ptr_array_index (self->items, i);
+
+      if (ele == command)
+        {
+          const gchar *id = gbp_shellcmd_command_get_id (ele);
+
+          if (id != NULL)
+            g_key_file_remove_group (self->keyfile, id, NULL);
+
+          g_signal_handlers_disconnect_by_func (command,
+                                                G_CALLBACK (on_command_changed_cb),
+                                                self);
+
+          g_ptr_array_remove_index (self->items, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          gbp_shellcmd_command_model_queue_save (self);
+
+          break;
+        }
+    }
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-model.h 
b/src/plugins/shellcmd/gbp-shellcmd-command-model.h
index 897cfd424..6c86f655a 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-model.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-model.h
@@ -33,6 +33,10 @@ G_DECLARE_FINAL_TYPE (GbpShellcmdCommandModel, gbp_shellcmd_command_model, GBP,
 GbpShellcmdCommandModel *gbp_shellcmd_command_model_new         (void);
 GbpShellcmdCommand      *gbp_shellcmd_command_model_get_command (GbpShellcmdCommandModel  *self,
                                                                  const gchar              *command_id);
+void                     gbp_shellcmd_command_model_add         (GbpShellcmdCommandModel  *self,
+                                                                 GbpShellcmdCommand       *command);
+void                     gbp_shellcmd_command_model_remove      (GbpShellcmdCommandModel  *self,
+                                                                 GbpShellcmdCommand       *command);
 void                     gbp_shellcmd_command_model_query       (GbpShellcmdCommandModel  *self,
                                                                  GPtrArray                *items,
                                                                  const gchar              *typed_text);
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-provider.c 
b/src/plugins/shellcmd/gbp-shellcmd-command-provider.c
index 184161a45..0f0aca6d6 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command-provider.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-provider.c
@@ -167,7 +167,7 @@ gbp_shellcmd_command_provider_load_shortcuts (IdeCommandProvider *provider,
       id = gbp_shellcmd_command_get_id (command);
       shortcut = gbp_shellcmd_command_get_shortcut (command);
 
-      if (id == NULL || shortcut == NULL)
+      if (id == NULL || shortcut == NULL || shortcut[0] == 0)
         continue;
 
       g_debug ("Mapping shortcut \"%s\" to external command \"%s\"", shortcut, id);
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-row.c b/src/plugins/shellcmd/gbp-shellcmd-command-row.c
new file mode 100644
index 000000000..fd125c80e
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-row.c
@@ -0,0 +1,93 @@
+/* gbp-shellcmd-command-row.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-shellcmd-command-row"
+
+#include "config.h"
+
+#include "gbp-shellcmd-command-row.h"
+
+struct _GbpShellcmdCommandRow
+{
+  GtkListBoxRow       parent_instance;
+
+  gchar              *id;
+  GbpShellcmdCommand *command;
+
+  GtkLabel           *title;
+  DzlShortcutLabel   *chord;
+};
+
+G_DEFINE_TYPE (GbpShellcmdCommandRow, gbp_shellcmd_command_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+gbp_shellcmd_command_row_finalize (GObject *object)
+{
+  GbpShellcmdCommandRow *self = (GbpShellcmdCommandRow *)object;
+
+  g_clear_pointer (&self->id, g_free);
+  g_clear_object (&self->command);
+
+  G_OBJECT_CLASS (gbp_shellcmd_command_row_parent_class)->finalize (object);
+}
+
+static void
+gbp_shellcmd_command_row_class_init (GbpShellcmdCommandRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_shellcmd_command_row_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/shellcmd/gbp-shellcmd-command-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandRow, chord);
+  gtk_widget_class_bind_template_child (widget_class, GbpShellcmdCommandRow, title);
+}
+
+static void
+gbp_shellcmd_command_row_init (GbpShellcmdCommandRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+gbp_shellcmd_command_row_new (GbpShellcmdCommand *command)
+{
+  GbpShellcmdCommandRow *self;
+
+  self = g_object_new (GBP_TYPE_SHELLCMD_COMMAND_ROW,
+                       "visible", TRUE,
+                       NULL);
+  self->id = g_strdup (gbp_shellcmd_command_get_id (command));
+  g_set_object (&self->command, command);
+
+  g_object_bind_property (command, "title", self->title, "label", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (command, "shortcut", self->chord, "accelerator", G_BINDING_SYNC_CREATE);
+
+  return GTK_WIDGET (self);
+}
+
+GbpShellcmdCommand *
+gbp_shellcmd_command_row_get_command (GbpShellcmdCommandRow *self)
+{
+  g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_ROW (self), NULL);
+
+  return self->command;
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-row.h b/src/plugins/shellcmd/gbp-shellcmd-command-row.h
new file mode 100644
index 000000000..7120d1543
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-row.h
@@ -0,0 +1,36 @@
+/* gbp-shellcmd-command-row.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gbp-shellcmd-command.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SHELLCMD_COMMAND_ROW (gbp_shellcmd_command_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpShellcmdCommandRow, gbp_shellcmd_command_row, GBP, SHELLCMD_COMMAND_ROW, 
GtkListBoxRow)
+
+GtkWidget          *gbp_shellcmd_command_row_new         (GbpShellcmdCommand    *commad);
+GbpShellcmdCommand *gbp_shellcmd_command_row_get_command (GbpShellcmdCommandRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command-row.ui 
b/src/plugins/shellcmd/gbp-shellcmd-command-row.ui
new file mode 100644
index 000000000..3d192465d
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-command-row.ui
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpShellcmdCommandRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="ellipsize">end</property>
+            <property name="visible">true</property>
+            <property name="hexpand">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+        <child>
+          <object class="DzlShortcutLabel" id="chord">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">pan-end-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command.c b/src/plugins/shellcmd/gbp-shellcmd-command.c
index 0404ae89b..699f9d998 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command.c
+++ b/src/plugins/shellcmd/gbp-shellcmd-command.c
@@ -63,28 +63,38 @@ enum {
   N_PROPS
 };
 
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
 static void command_iface_init (IdeCommandInterface *iface);
 
 G_DEFINE_TYPE_WITH_CODE (GbpShellcmdCommand, gbp_shellcmd_command, IDE_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND, command_iface_init))
 
 static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+gbp_shellcmd_command_changed (GbpShellcmdCommand *self)
+{
+  g_assert (GBP_IS_SHELLCMD_COMMAND (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
 
 static void
 gbp_shellcmd_command_set_env (GbpShellcmdCommand  *self,
                               const gchar * const *env)
 {
-  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (self));
-
-  if (self->environment == NULL)
-    {
-      if (env == NULL || env[0] == NULL)
-        return;
+  IdeEnvironment *dest;
 
-      self->environment = ide_environment_new ();
-    }
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (self));
 
-  ide_environment_set_environ (self->environment, env);
+  dest = gbp_shellcmd_command_get_environment (self);
+  ide_environment_set_environ (dest, env);
+  gbp_shellcmd_command_changed (self);
 }
 
 static void
@@ -268,6 +278,12 @@ gbp_shellcmd_command_class_init (GbpShellcmdCommandClass *klass)
                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
   
   g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
 static void
@@ -281,7 +297,7 @@ gbp_shellcmd_command_get_cwd (GbpShellcmdCommand *self)
 {
   g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND (self), NULL);
 
-  return self->cwd;
+  return self->cwd ? self->cwd : "";
 }
 
 void
@@ -295,6 +311,7 @@ gbp_shellcmd_command_set_cwd (GbpShellcmdCommand *self,
       g_free (self->cwd);
       self->cwd = g_strdup (cwd);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CWD]);
+      gbp_shellcmd_command_changed (self);
     }
 }
 
@@ -329,13 +346,28 @@ gbp_shellcmd_command_apply (GbpShellcmdCommand    *self,
                             IdeSubprocessLauncher *launcher,
                             GFile                 *relative_to)
 {
+  g_autoptr(IdeContext) context = NULL;
   g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) workdir = NULL;
   g_autoptr(GFile) cwd = NULL;
+  const gchar *builddir = NULL;
 
   g_assert (GBP_IS_SHELLCMD_COMMAND (self));
   g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
   g_assert (G_IS_FILE (relative_to));
 
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  workdir = ide_context_ref_workdir (context);
+
+  if (ide_context_has_project (context))
+    {
+      IdeBuildManager *build_manager = ide_build_manager_from_context (context);
+      IdePipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
+
+      if (pipeline != NULL)
+        builddir = ide_pipeline_get_builddir (pipeline);
+    }
+
   if (self->cwd != NULL)
     {
       if (g_path_is_absolute (self->cwd))
@@ -350,6 +382,11 @@ gbp_shellcmd_command_apply (GbpShellcmdCommand    *self,
 
   ide_subprocess_launcher_set_cwd (launcher, g_file_peek_path (cwd));
 
+  ide_subprocess_launcher_setenv (launcher, "INSIDE_GNOME_BUILDER", PACKAGE_VERSION, TRUE);
+  ide_subprocess_launcher_setenv (launcher, "SRCDIR", g_file_peek_path (workdir), TRUE);
+  if (builddir != NULL)
+    ide_subprocess_launcher_setenv (launcher, "BUILDDIR", builddir, TRUE);
+
   if (self->environment != NULL)
     {
       g_auto(GStrv) env = ide_environment_get_environ (self->environment);
@@ -764,6 +801,7 @@ gbp_shellcmd_command_set_locality (GbpShellcmdCommand         *self,
     {
       self->locality = locality;
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCALITY]);
+      gbp_shellcmd_command_changed (self);
     }
 
 }
@@ -787,6 +825,7 @@ gbp_shellcmd_command_set_command (GbpShellcmdCommand *self,
       g_free (self->command);
       self->command = g_strdup (command);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMMAND]);
+      gbp_shellcmd_command_changed (self);
     }
 }
 
@@ -796,7 +835,15 @@ gbp_shellcmd_command_get_environment (GbpShellcmdCommand *self)
   g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND (self), NULL);
 
   if (self->environment == NULL)
-    self->environment = ide_environment_new ();
+    {
+      self->environment = ide_environment_new ();
+
+      g_signal_connect_object (self->environment,
+                               "changed",
+                               G_CALLBACK (gbp_shellcmd_command_changed),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
 
   return self->environment;
 }
@@ -820,6 +867,7 @@ gbp_shellcmd_command_set_shortcut (GbpShellcmdCommand *self,
       g_free (self->shortcut);
       self->shortcut = g_strdup (shortcut);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHORTCUT]);
+      gbp_shellcmd_command_changed (self);
     }
 }
 
@@ -834,6 +882,7 @@ gbp_shellcmd_command_set_title (GbpShellcmdCommand *self,
       g_free (self->title);
       self->title = g_strdup (title);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+      gbp_shellcmd_command_changed (self);
     }
 }
 
@@ -848,6 +897,7 @@ gbp_shellcmd_command_set_subtitle (GbpShellcmdCommand *self,
       g_free (self->subtitle);
       self->subtitle = g_strdup (subtitle);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SUBTITLE]);
+      gbp_shellcmd_command_changed (self);
     }
 }
 
@@ -943,6 +993,39 @@ gbp_shellcmd_command_from_key_file (GKeyFile     *keyfile,
   return g_steal_pointer (&self);
 }
 
+void
+gbp_shellcmd_command_to_key_file (GbpShellcmdCommand  *self,
+                                  GKeyFile            *keyfile)
+{
+  g_autoptr(GEnumClass) locality_class = NULL;
+  const GEnumValue *value;
+  g_auto(GStrv) env = NULL;
+  const gchar *localitystr = NULL;
+  const gchar *group;
+
+  g_return_if_fail (GBP_IS_SHELLCMD_COMMAND (self));
+  g_return_if_fail (keyfile != NULL);
+
+  group = self->id;
+
+  if (self->environment != NULL)
+    env = ide_environment_get_environ (self->environment);
+  else
+    env = g_new0 (gchar *, 1);
+
+  locality_class = g_type_class_ref (GBP_TYPE_SHELLCMD_COMMAND_LOCALITY);
+
+  if ((value = g_enum_get_value (locality_class, self->locality)))
+    localitystr = value->value_nick;
+
+  g_key_file_set_string (keyfile, group, "Locality", localitystr ?: "");
+  g_key_file_set_string (keyfile, group, "Shortcut", self->shortcut ?: "");
+  g_key_file_set_string (keyfile, group, "Title", self->title ?: "");
+  g_key_file_set_string (keyfile, group, "Command", self->command ?: "");
+  g_key_file_set_string (keyfile, group, "Directory", self->cwd ?: "");
+  g_key_file_set_string_list (keyfile, group, "Environment", (const gchar * const *)env, g_strv_length 
(env));
+}
+
 const gchar *
 gbp_shellcmd_command_get_id (GbpShellcmdCommand *self)
 {
@@ -970,8 +1053,8 @@ gbp_shellcmd_command_copy (GbpShellcmdCommand *self)
   if (self->environment != NULL)
     {
       g_auto(GStrv) env = ide_environment_get_environ (self->environment);
-      ret->environment = ide_environment_new ();
-      ide_environment_set_environ (ret->environment, (const gchar * const *)env);
+      IdeEnvironment *dest = gbp_shellcmd_command_get_environment (ret);
+      ide_environment_set_environ (dest, (const gchar * const *)env);
     }
 
   return g_steal_pointer (&ret);
diff --git a/src/plugins/shellcmd/gbp-shellcmd-command.h b/src/plugins/shellcmd/gbp-shellcmd-command.h
index 70e7d913d..e0bc60fdf 100644
--- a/src/plugins/shellcmd/gbp-shellcmd-command.h
+++ b/src/plugins/shellcmd/gbp-shellcmd-command.h
@@ -40,9 +40,8 @@ G_DECLARE_FINAL_TYPE (GbpShellcmdCommand, gbp_shellcmd_command, GBP, SHELLCMD_CO
 GbpShellcmdCommand         *gbp_shellcmd_command_from_key_file   (GKeyFile                   *key_file,
                                                                   const gchar                *group,
                                                                   GError                    **error);
-gboolean                    gbp_shellcmd_command_to_key_file     (GbpShellcmdCommand         *self,
-                                                                  GKeyFile                   *key_file,
-                                                                  GError                    **error);
+void                        gbp_shellcmd_command_to_key_file     (GbpShellcmdCommand         *self,
+                                                                  GKeyFile                   *key_file);
 GbpShellcmdCommand         *gbp_shellcmd_command_copy            (GbpShellcmdCommand         *self);
 const gchar                *gbp_shellcmd_command_get_id          (GbpShellcmdCommand         *self);
 GbpShellcmdCommandLocality  gbp_shellcmd_command_get_locality    (GbpShellcmdCommand         *self);
diff --git a/src/plugins/shellcmd/gbp-shellcmd-list.c b/src/plugins/shellcmd/gbp-shellcmd-list.c
new file mode 100644
index 000000000..d702eafec
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-list.c
@@ -0,0 +1,186 @@
+/* gbp-shellcmd-list.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-shellcmd-list"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-shellcmd-command.h"
+#include "gbp-shellcmd-command-row.h"
+#include "gbp-shellcmd-list.h"
+
+struct _GbpShellcmdList
+{
+  GtkFrame                 parent_instance;
+
+  GtkListBox              *list;
+  GtkBox                  *box;
+  GtkListBoxRow           *add_row;
+
+  GbpShellcmdCommandModel *model;
+};
+
+enum {
+  COMMAND_SELECTED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+G_DEFINE_TYPE (GbpShellcmdList, gbp_shellcmd_list, GTK_TYPE_FRAME)
+
+static void
+gbp_shellcmd_list_class_init (GbpShellcmdListClass *klass)
+{
+  signals [COMMAND_SELECTED] =
+    g_signal_new ("command-selected",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, GBP_TYPE_SHELLCMD_COMMAND);
+}
+
+static void
+gbp_shellcmd_list_init (GbpShellcmdList *self)
+{
+}
+
+static void
+on_row_activated_cb (GbpShellcmdList       *self,
+                     GbpShellcmdCommandRow *row,
+                     GtkListBox            *list_box)
+{
+  GbpShellcmdCommand *command = NULL;
+
+  g_assert (GBP_IS_SHELLCMD_LIST (self));
+  g_assert (!row || GBP_IS_SHELLCMD_COMMAND_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if (row != NULL)
+    command = gbp_shellcmd_command_row_get_command (row);
+
+  g_signal_emit (self, signals [COMMAND_SELECTED], 0, command);
+}
+
+static GtkWidget *
+create_row_func (gpointer item,
+                 gpointer user_data)
+{
+  return gbp_shellcmd_command_row_new (item);
+}
+
+static void
+on_add_new_row_cb (GbpShellcmdList *self,
+                   GtkListBoxRow   *row,
+                   GtkListBox      *list_box)
+{
+  g_autoptr(GbpShellcmdCommand) command = NULL;
+  g_autofree gchar *id = NULL;
+  guint nth;
+
+  g_assert (GBP_IS_SHELLCMD_LIST (self));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if (self->model == NULL)
+    return;
+
+  id = g_uuid_string_random ();
+  nth = g_list_model_get_n_items (G_LIST_MODEL (self->model));
+
+  command = g_object_new (GBP_TYPE_SHELLCMD_COMMAND,
+                          "id", id,
+                          "title", _("New command"),
+                          "command", "",
+                          NULL);
+  gbp_shellcmd_command_model_add (self->model, command);
+
+  /* Now select the new row */
+  row = gtk_list_box_get_row_at_index (self->list, nth);
+  gtk_list_box_select_row (self->list, row);
+}
+
+GtkWidget *
+gbp_shellcmd_list_new (GbpShellcmdCommandModel *model)
+{
+  GbpShellcmdList *self;
+  GtkWidget *list2;
+  GtkWidget *placeholder;
+
+  g_return_val_if_fail (GBP_IS_SHELLCMD_COMMAND_MODEL (model), NULL);
+
+  placeholder = g_object_new (GTK_TYPE_LABEL,
+                              "margin", 12,
+                              "label", _("Click + to add an external command"),
+                              "visible", TRUE,
+                              NULL);
+
+  self = g_object_new (GBP_TYPE_SHELLCMD_LIST,
+                       "shadow-type", GTK_SHADOW_IN,
+                       "visible", TRUE,
+                       NULL);
+  self->model = g_object_ref (model);
+
+  self->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->box));
+
+  self->list = g_object_new (GTK_TYPE_LIST_BOX,
+                             "selection-mode", GTK_SELECTION_NONE,
+                             "visible", TRUE,
+                             NULL);
+  gtk_list_box_set_placeholder (self->list, placeholder);
+  gtk_list_box_bind_model (self->list,
+                           G_LIST_MODEL (model),
+                           create_row_func, NULL, NULL);
+  g_signal_connect_object (self->list,
+                           "row-activated",
+                           G_CALLBACK (on_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (self->list));
+
+  list2 = g_object_new (GTK_TYPE_LIST_BOX,
+                        "selection-mode", GTK_SELECTION_NONE,
+                        "visible", TRUE,
+                        NULL);
+  g_signal_connect_object (list2,
+                           "row-activated",
+                           G_CALLBACK (on_add_new_row_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (list2));
+
+  self->add_row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                                "child", g_object_new (GTK_TYPE_IMAGE,
+                                                       "icon-name", "list-add-symbolic",
+                                                       "visible", TRUE,
+                                                       NULL),
+                                "visible", TRUE,
+                                NULL);
+  gtk_container_add (GTK_CONTAINER (list2), GTK_WIDGET (self->add_row));
+
+  return GTK_WIDGET (g_steal_pointer (&self));
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-list.h b/src/plugins/shellcmd/gbp-shellcmd-list.h
new file mode 100644
index 000000000..2ac4cf91c
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-list.h
@@ -0,0 +1,35 @@
+/* gbp-shellcmd-list.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "gbp-shellcmd-command-model.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SHELLCMD_LIST (gbp_shellcmd_list_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpShellcmdList, gbp_shellcmd_list, GBP, SHELLCMD_LIST, GtkFrame)
+
+GtkWidget *gbp_shellcmd_list_new (GbpShellcmdCommandModel *model);
+
+G_END_DECLS
diff --git a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c 
b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c
new file mode 100644
index 000000000..a8d8d9864
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.c
@@ -0,0 +1,151 @@
+/* gbp-shellcmd-preferences-addin.c
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "gbp-shellcmd-preferences-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-shellcmd-application-addin.h"
+#include "gbp-shellcmd-command-editor.h"
+#include "gbp-shellcmd-command-model.h"
+#include "gbp-shellcmd-command-row.h"
+#include "gbp-shellcmd-list.h"
+#include "gbp-shellcmd-preferences-addin.h"
+
+struct _GbpShellcmdPreferencesAddin
+{
+  GObject parent_instance;
+
+  GbpShellcmdCommandEditor *editor;
+};
+
+static GbpShellcmdCommandModel *
+get_model (void)
+{
+  GbpShellcmdApplicationAddin *app_addin;
+  GbpShellcmdCommandModel *model;
+
+  app_addin = ide_application_find_addin_by_module_name (NULL, "shellcmd");
+  g_assert (GBP_IS_SHELLCMD_APPLICATION_ADDIN (app_addin));
+
+  model = gbp_shellcmd_application_addin_get_model (app_addin);
+  g_assert (GBP_IS_SHELLCMD_COMMAND_MODEL (model));
+
+  return model;
+}
+
+static void
+on_command_selected_cb (GbpShellcmdPreferencesAddin *self,
+                        GbpShellcmdCommand          *command,
+                        GbpShellcmdList             *list)
+{
+  GtkWidget *preferences;
+
+  g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
+  g_assert (!command || GBP_IS_SHELLCMD_COMMAND (command));
+  g_assert (GBP_IS_SHELLCMD_LIST (list));
+
+  if (!(preferences = gtk_widget_get_ancestor (GTK_WIDGET (list), DZL_TYPE_PREFERENCES)))
+    return;
+
+  if (command != NULL)
+    {
+      g_autoptr(GHashTable) map = NULL;
+
+      map = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+      g_hash_table_insert (map, (gchar *)"{id}", g_strdup (gbp_shellcmd_command_get_id (command)));
+      dzl_preferences_set_page (DZL_PREFERENCES (preferences), "shellcmd.id", map);
+    }
+
+  gbp_shellcmd_command_editor_set_command (self->editor, command);
+}
+
+static void
+gbp_shellcmd_preferences_addin_load (IdePreferencesAddin *addin,
+                                     DzlPreferences      *prefs)
+{
+  GbpShellcmdPreferencesAddin *self = (GbpShellcmdPreferencesAddin *)addin;
+  GtkWidget *list;
+
+  g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (prefs));
+
+  dzl_preferences_add_page (prefs, "shellcmd", _("External Commands"), 650);
+  dzl_preferences_add_group (prefs, "shellcmd", "commands", _("External Commands"), 0);
+
+  list = gbp_shellcmd_list_new (get_model ());
+  g_signal_connect_object (list,
+                           "command-selected",
+                           G_CALLBACK (on_command_selected_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  dzl_preferences_add_custom (prefs, "shellcmd", "commands", list, NULL, 0);
+
+  dzl_preferences_add_page (prefs, "shellcmd.id", NULL, 0);
+  dzl_preferences_add_group (prefs, "shellcmd.id", "basic", _("Command"), 0);
+
+  self->editor = g_object_new (GBP_TYPE_SHELLCMD_COMMAND_EDITOR,
+                               "visible", TRUE,
+                               NULL);
+  g_signal_connect (self->editor,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->editor);
+  dzl_preferences_add_custom (prefs, "shellcmd.id", "basic", GTK_WIDGET (self->editor), NULL, 0);
+}
+
+static void
+gbp_shellcmd_preferences_addin_unload (IdePreferencesAddin *addin,
+                                       DzlPreferences      *prefs)
+{
+  GbpShellcmdPreferencesAddin *self = (GbpShellcmdPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_SHELLCMD_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (prefs));
+
+  if (self->editor != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->editor));
+
+  g_assert (self->editor == NULL);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_shellcmd_preferences_addin_load;
+  iface->unload = gbp_shellcmd_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpShellcmdPreferencesAddin, gbp_shellcmd_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN, preferences_addin_iface_init))
+
+static void
+gbp_shellcmd_preferences_addin_class_init (GbpShellcmdPreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_shellcmd_preferences_addin_init (GbpShellcmdPreferencesAddin *self)
+{
+}
diff --git a/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h 
b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h
new file mode 100644
index 000000000..6041fa0c1
--- /dev/null
+++ b/src/plugins/shellcmd/gbp-shellcmd-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-shellcmd-preferences-addin.h
+ *
+ * Copyright 2019 Christian Hergert <chergert redhat com>
+ *
+ * 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 3 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/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SHELLCMD_PREFERENCES_ADDIN (gbp_shellcmd_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpShellcmdPreferencesAddin, gbp_shellcmd_preferences_addin, GBP, 
SHELLCMD_PREFERENCES_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/shellcmd/meson.build b/src/plugins/shellcmd/meson.build
index a00a0e8ca..e6d03e030 100644
--- a/src/plugins/shellcmd/meson.build
+++ b/src/plugins/shellcmd/meson.build
@@ -4,8 +4,12 @@ plugins_sources += files([
   'shellcmd-plugin.c',
   'gbp-shellcmd-application-addin.c',
   'gbp-shellcmd-command.c',
+  'gbp-shellcmd-command-editor.c',
   'gbp-shellcmd-command-model.c',
   'gbp-shellcmd-command-provider.c',
+  'gbp-shellcmd-command-row.c',
+  'gbp-shellcmd-list.c',
+  'gbp-shellcmd-preferences-addin.c',
 ])
 
 plugin_shellcmd_enum_headers = [
diff --git a/src/plugins/shellcmd/shellcmd-plugin.c b/src/plugins/shellcmd/shellcmd-plugin.c
index 0f316a265..26b841cc8 100644
--- a/src/plugins/shellcmd/shellcmd-plugin.c
+++ b/src/plugins/shellcmd/shellcmd-plugin.c
@@ -25,6 +25,7 @@
 
 #include "gbp-shellcmd-application-addin.h"
 #include "gbp-shellcmd-command-provider.h"
+#include "gbp-shellcmd-preferences-addin.h"
 
 _IDE_EXTERN void
 _gbp_shellcmd_register_types (PeasObjectModule *module)
@@ -35,4 +36,7 @@ _gbp_shellcmd_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_COMMAND_PROVIDER,
                                               GBP_TYPE_SHELLCMD_COMMAND_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_SHELLCMD_PREFERENCES_ADDIN);
 }
diff --git a/src/plugins/shellcmd/shellcmd.gresource.xml b/src/plugins/shellcmd/shellcmd.gresource.xml
index 4af547c31..47849fb90 100644
--- a/src/plugins/shellcmd/shellcmd.gresource.xml
+++ b/src/plugins/shellcmd/shellcmd.gresource.xml
@@ -2,5 +2,7 @@
 <gresources>
   <gresource prefix="/plugins/shellcmd">
     <file>shellcmd.plugin</file>
+    <file preprocess="xml-stripblanks">gbp-shellcmd-command-editor.ui</file>
+    <file preprocess="xml-stripblanks">gbp-shellcmd-command-row.ui</file>
   </gresource>
 </gresources>


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