[gnome-builder] recent: move recent projects into hidden plugin



commit 7024c6aff70a6ba520eff0dbe8c2eda99fad7b93
Author: Christian Hergert <chergert redhat com>
Date:   Fri Nov 17 19:47:01 2017 -0800

    recent: move recent projects into hidden plugin
    
    This allows things to be implemented outside of libide directly
    and use the same API as the newcomers section.

 src/libide/greeter/ide-greeter-perspective.c  |  516 +++----------------------
 src/libide/greeter/ide-greeter-perspective.ui |   93 ++---
 src/plugins/meson.build                       |    1 +
 src/plugins/recent/gbp-recent-project-row.c   |  387 ++++++++++++++++++
 src/plugins/recent/gbp-recent-project-row.h   |   34 ++
 src/plugins/recent/gbp-recent-project-row.ui  |   89 +++++
 src/plugins/recent/gbp-recent-section.c       |  212 ++++++++++
 src/plugins/recent/gbp-recent-section.h       |   29 ++
 src/plugins/recent/gbp-recent-section.ui      |   72 ++++
 src/plugins/recent/meson.build                |   16 +
 src/plugins/recent/recent-plugin.c            |   30 ++
 src/plugins/recent/recent.gresource.xml       |   10 +
 src/plugins/recent/recent.plugin              |    9 +
 13 files changed, 989 insertions(+), 509 deletions(-)
---
diff --git a/src/libide/greeter/ide-greeter-perspective.c b/src/libide/greeter/ide-greeter-perspective.c
index f102f6b..e2336b2 100644
--- a/src/libide/greeter/ide-greeter-perspective.c
+++ b/src/libide/greeter/ide-greeter-perspective.c
@@ -39,8 +39,6 @@ struct _IdeGreeterPerspective
 {
   GtkBin                parent_instance;
 
-  DzlSignalGroup       *signal_group;
-  IdeRecentProjects    *recent_projects;
   DzlPatternSpec       *pattern_spec;
   PeasExtensionSet     *genesis_set;
 
@@ -60,8 +58,6 @@ struct _IdeGreeterPerspective
   GtkRevealer          *info_bar_revealer;
   GtkViewport          *viewport;
   GtkWidget            *titlebar;
-  GtkBox               *my_projects_container;
-  GtkListBox           *my_projects_list_box;
   GtkButton            *open_button;
   GtkButton            *cancel_button;
   GtkButton            *remove_button;
@@ -90,14 +86,6 @@ G_DEFINE_TYPE_EXTENDED (IdeGreeterPerspective, ide_greeter_perspective, GTK_TYPE
                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PERSPECTIVE,
                                                ide_perspective_iface_init))
 
-enum {
-  PROP_0,
-  PROP_RECENT_PROJECTS,
-  LAST_PROP
-};
-
-static GParamSpec *properties [LAST_PROP];
-
 static GtkWidget *
 ide_greeter_perspective_get_titlebar (IdePerspective *perspective)
 {
@@ -125,66 +113,37 @@ ide_perspective_iface_init (IdePerspectiveInterface *iface)
 }
 
 static void
-ide_greeter_perspective_first_visible_cb (GtkWidget *widget,
-                                          gpointer   user_data)
+ide_greeter_perspective_activate_cb (PeasExtensionSet *set,
+                                     PeasPluginInfo   *plugin_info,
+                                     PeasExtension    *exten,
+                                     gpointer          user_data)
 {
-  GtkWidget **row = user_data;
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  gboolean *handled = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
 
-  if ((*row == NULL) && gtk_widget_get_child_visible (widget))
-    *row = widget;
+  if (!*handled)
+    *handled = ide_greeter_section_activate_first (section);
 }
 
 static void
 ide_greeter_perspective__search_entry_activate (IdeGreeterPerspective *self,
                                                 GtkSearchEntry        *search_entry)
 {
-  GtkWidget *row = NULL;
+  gboolean handled = FALSE;
 
   g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
   g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
 
-  gtk_container_foreach (GTK_CONTAINER (self->my_projects_list_box),
-                         ide_greeter_perspective_first_visible_cb,
-                         &row);
-
-  if (row != NULL)
-    g_signal_emit_by_name (row, "activate");
-}
-
-IdeRecentProjects *
-ide_greeter_perspective_get_recent_projects (IdeGreeterPerspective *self)
-{
-  g_return_val_if_fail (IDE_IS_GREETER_PERSPECTIVE (self), NULL);
-
-  return self->recent_projects;
-}
-
-static void
-ide_greeter_perspective_apply_filter_cb (GtkWidget *widget,
-                                   gpointer   user_data)
-{
-  gboolean *visible = user_data;
-
-  g_assert (IDE_IS_GREETER_PROJECT_ROW (widget));
-
-  if (gtk_widget_get_child_visible (widget))
-    *visible = TRUE;
-}
-
-static void
-ide_greeter_perspective_apply_filter (IdeGreeterPerspective *self,
-                                      GtkListBox            *list_box,
-                                      GtkWidget             *container)
-{
-  gboolean visible = FALSE;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (GTK_IS_LIST_BOX (list_box));
-  g_assert (GTK_IS_CONTAINER (container));
+  peas_extension_set_foreach (self->sections,
+                              ide_greeter_perspective_activate_cb,
+                              &handled);
 
-  gtk_list_box_invalidate_filter (list_box);
-  gtk_container_foreach (GTK_CONTAINER (list_box), ide_greeter_perspective_apply_filter_cb, &visible);
-  gtk_widget_set_visible (GTK_WIDGET (container), visible);
+  if (!handled)
+    gdk_window_beep (gtk_widget_get_window (GTK_WIDGET (search_entry)));
 }
 
 static void
@@ -195,13 +154,16 @@ ide_greeter_perspective_filter_sections (PeasExtensionSet *set,
 {
   IdeGreeterPerspective *self = user_data;
   IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  gboolean has_child;
 
   g_assert (PEAS_IS_EXTENSION_SET (set));
   g_assert (plugin_info != NULL);
   g_assert (IDE_IS_GREETER_SECTION (section));
   g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
 
-  ide_greeter_section_filter (section, self->pattern_spec);
+  has_child = ide_greeter_section_filter (section, self->pattern_spec);
+
+  gtk_widget_set_visible (GTK_WIDGET (section), has_child);
 }
 
 static void
@@ -212,12 +174,9 @@ ide_greeter_perspective_apply_filter_all (IdeGreeterPerspective *self)
   g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
 
   g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
-  if ((text = gtk_entry_get_text (GTK_ENTRY (self->search_entry))))
-    self->pattern_spec = dzl_pattern_spec_new (text);
 
-  ide_greeter_perspective_apply_filter (self,
-                                        self->my_projects_list_box,
-                                        GTK_WIDGET (self->my_projects_container));
+  if (NULL != (text = gtk_entry_get_text (GTK_ENTRY (self->search_entry))))
+    self->pattern_spec = dzl_pattern_spec_new (text);
 
   if (self->sections != NULL)
     peas_extension_set_foreach (self->sections,
@@ -235,215 +194,6 @@ ide_greeter_perspective__search_entry_changed (IdeGreeterPerspective *self,
   ide_greeter_perspective_apply_filter_all (self);
 }
 
-static gboolean
-row_focus_in_event (IdeGreeterPerspective *self,
-                    GdkEventFocus         *focus,
-                    IdeGreeterProjectRow  *row)
-{
-  GtkAllocation alloc;
-  GtkAllocation row_alloc;
-  gint dest_x;
-  gint dest_y;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-
-  gtk_widget_get_allocation (GTK_WIDGET (self->viewport), &alloc);
-  gtk_widget_get_allocation (GTK_WIDGET (row), &row_alloc);
-
-  /*
-   * If we are smaller than the visible area, don't do anything for now.
-   * This can happen during creation of the window and resize process.
-   */
-  if (row_alloc.height > alloc.height)
-    return GDK_EVENT_PROPAGATE;
-
-  if (gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (self->viewport), 0, 0, &dest_x, 
&dest_y))
-    {
-      gint distance = 0;
-
-      if (dest_y < 0)
-        {
-          distance = dest_y;
-        }
-      else if ((dest_y + row_alloc.height) > alloc.height)
-        {
-          distance = dest_y + row_alloc.height - alloc.height;
-        }
-
-      if (distance != 0)
-        {
-          GtkAdjustment *vadj;
-          gdouble value;
-
-          vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self->viewport));
-          value = gtk_adjustment_get_value (vadj);
-          gtk_adjustment_set_value (vadj, value + distance);
-        }
-    }
-
-  return GDK_EVENT_PROPAGATE;
-}
-
-static gboolean
-selection_to_true (GBinding     *binding,
-                   const GValue *from_value,
-                   GValue       *to_value,
-                   gpointer      user_data)
-{
-  if (G_VALUE_HOLDS_STRING (from_value) && G_VALUE_HOLDS_BOOLEAN (to_value))
-    {
-      const gchar *str;
-
-      str = g_value_get_string (from_value);
-      g_value_set_boolean (to_value, ide_str_equal0 (str, "selection"));
-
-      return TRUE;
-    }
-
-  return FALSE;
-}
-
-static void
-ide_greeter_perspective__row_notify_selected (IdeGreeterPerspective *self,
-                                              GParamSpec            *pspec,
-                                              IdeGreeterProjectRow  *row)
-{
-  gboolean selected = FALSE;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (pspec != NULL);
-  g_assert (IDE_IS_GREETER_PROJECT_ROW (row));
-
-  g_object_get (row, "selected", &selected, NULL);
-  self->selected_count += selected ? 1 : -1;
-
-  dzl_gtk_widget_action_set (GTK_WIDGET (self), "greeter", "delete-selected-rows",
-                             "enabled", self->selected_count > 0,
-                             NULL);
-}
-
-static void
-recent_projects_items_changed (IdeGreeterPerspective *self,
-                               guint                  position,
-                               guint                  removed,
-                               guint                  added,
-                               GListModel            *list_model)
-{
-  IdeGreeterProjectRow *row;
-  gsize i;
-
-  /*
-   * TODO: We ignore removed out of simplicity for now.
-   *       But IdeRecentProjects doesn't currently remove anything anyway.
-   */
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (G_IS_LIST_MODEL (list_model));
-  g_assert (IDE_IS_RECENT_PROJECTS (list_model));
-
-  if (g_list_model_get_n_items (list_model) > 0)
-    {
-      if (ide_str_equal0 ("empty-state", gtk_stack_get_visible_child_name (self->stack)))
-        gtk_stack_set_visible_child_name (self->stack, "projects");
-    }
-
-  for (i = 0; i < added; i++)
-    {
-      IdeProjectInfo *project_info;
-
-      project_info = g_list_model_get_item (list_model, position + i);
-
-      row = g_object_new (IDE_TYPE_GREETER_PROJECT_ROW,
-                          "visible", TRUE,
-                          "project-info", project_info,
-                          NULL);
-      g_signal_connect_object (row,
-                               "focus-in-event",
-                               G_CALLBACK (row_focus_in_event),
-                               self,
-                               G_CONNECT_SWAPPED);
-      g_signal_connect_object (row,
-                               "notify::selected",
-                               G_CALLBACK (ide_greeter_perspective__row_notify_selected),
-                               self,
-                               G_CONNECT_SWAPPED);
-
-      if (ide_project_info_get_is_recent (project_info))
-        {
-          GtkListBox *list_box = self->my_projects_list_box;
-
-          g_object_bind_property_full (self->state_machine, "state",
-                                       row, "selection-mode",
-                                       G_BINDING_SYNC_CREATE,
-                                       selection_to_true, NULL,
-                                       NULL, NULL);
-          gtk_container_add (GTK_CONTAINER (list_box), GTK_WIDGET (row));
-        }
-    }
-
-  ide_greeter_perspective_apply_filter_all (self);
-}
-
-static gint
-ide_greeter_perspective_sort_rows (GtkListBoxRow *row1,
-                                   GtkListBoxRow *row2,
-                                   gpointer       user_data)
-{
-  IdeProjectInfo *info1;
-  IdeProjectInfo *info2;
-
-  info1 = ide_greeter_project_row_get_project_info (IDE_GREETER_PROJECT_ROW (row1));
-  info2 = ide_greeter_project_row_get_project_info (IDE_GREETER_PROJECT_ROW (row2));
-
-  return ide_project_info_compare (info1, info2);
-}
-
-static void
-ide_greeter_perspective_set_recent_projects (IdeGreeterPerspective *self,
-                                             IdeRecentProjects     *recent_projects)
-{
-  g_return_if_fail (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_return_if_fail (!recent_projects || IDE_IS_RECENT_PROJECTS (recent_projects));
-
-  if (g_set_object (&self->recent_projects, recent_projects))
-    {
-      dzl_signal_group_set_target (self->signal_group, recent_projects);
-
-      if (recent_projects != NULL)
-        {
-          GListModel *list_model;
-          guint n_items;
-
-          list_model = G_LIST_MODEL (recent_projects);
-          n_items = g_list_model_get_n_items (list_model);
-          recent_projects_items_changed (self, 0, 0, n_items, list_model);
-        }
-
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RECENT_PROJECTS]);
-    }
-}
-
-static gboolean
-ide_greeter_perspective_filter_row (GtkListBoxRow *row,
-                                    gpointer       user_data)
-{
-  IdeGreeterPerspective *self = user_data;
-  IdeGreeterProjectRow *project_row = (IdeGreeterProjectRow *)row;
-  const gchar *search_text;
-  gboolean ret;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (IDE_IS_GREETER_PROJECT_ROW (project_row));
-
-  if (self->pattern_spec == NULL)
-    return TRUE;
-
-  search_text = ide_greeter_project_row_get_search_text (project_row);
-  ret = dzl_pattern_spec_match (self->pattern_spec, search_text);
-
-  return ret;
-}
-
 static void
 ide_greeter_perspective_open_project_cb (GObject      *object,
                                          GAsyncResult *result,
@@ -482,102 +232,6 @@ ide_greeter_perspective_open_project_cb (GObject      *object,
 }
 
 static void
-ide_greeter_perspective__row_activated (IdeGreeterPerspective *self,
-                                        IdeGreeterProjectRow  *row,
-                                        GtkListBox            *list_box)
-{
-  IdeProjectInfo *project_info;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (IDE_IS_GREETER_PROJECT_ROW (row));
-  g_assert (GTK_IS_LIST_BOX (list_box));
-
-  if (dzl_state_machine_is_state (self->state_machine, "selection"))
-    {
-      gboolean selected = FALSE;
-
-      g_object_get (row, "selected", &selected, NULL);
-      g_object_set (row, "selected", !selected, NULL);
-
-      return;
-    }
-
-  project_info = ide_greeter_project_row_get_project_info (row);
-
-  ide_greeter_perspective_load_project (self, project_info);
-}
-
-static gboolean
-ide_greeter_perspective__keynav_failed (IdeGreeterPerspective *self,
-                                        GtkDirectionType       dir,
-                                        GtkListBox            *list_box)
-{
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (GTK_IS_LIST_BOX (list_box));
-
-#if 0
-  /* TODO: Navigate to icon grid */
-  if ((list_box == self->my_projects_list_box) && (dir == GTK_DIR_DOWN))
-    {
-      gtk_widget_child_focus (GTK_WIDGET (self->other_projects_list_box), GTK_DIR_DOWN);
-      return GDK_EVENT_STOP;
-    }
-  else if ((list_box == self->other_projects_list_box) && (dir == GTK_DIR_UP))
-    {
-      gtk_widget_child_focus (GTK_WIDGET (self->my_projects_list_box), GTK_DIR_UP);
-      return GDK_EVENT_STOP;
-    }
-#endif
-
-  return GDK_EVENT_PROPAGATE;
-}
-
-static void
-delete_selected_rows (GSimpleAction *action,
-                      GVariant      *parameter,
-                      gpointer       user_data)
-{
-  IdeGreeterPerspective *self = user_data;
-  GList *rows;
-  GList *iter;
-  GList *projects = NULL;
-
-  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
-  g_assert (G_IS_SIMPLE_ACTION (action));
-
-  rows = gtk_container_get_children (GTK_CONTAINER (self->my_projects_list_box));
-
-  for (iter = rows; iter; iter = iter->next)
-    {
-      IdeGreeterProjectRow *row = iter->data;
-      gboolean selected = FALSE;
-
-      g_object_get (row, "selected", &selected, NULL);
-
-      if (selected)
-        {
-          IdeProjectInfo *info;
-
-          info = ide_greeter_project_row_get_project_info (row);
-          projects = g_list_prepend (projects, g_object_ref (info));
-          gtk_container_remove (GTK_CONTAINER (self->my_projects_list_box), iter->data);
-        }
-    }
-
-  g_list_free (rows);
-
-  ide_recent_projects_remove (self->recent_projects, projects);
-  g_list_free_full (projects, g_object_unref);
-
-  self->selected_count = 0;
-  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE);
-
-  dzl_state_machine_set_state (self->state_machine, "browse");
-
-  ide_greeter_perspective_apply_filter_all (self);
-}
-
-static void
 ide_greeter_perspective_dialog_response (IdeGreeterPerspective *self,
                                          gint                   response_id,
                                          GtkFileChooserDialog  *dialog)
@@ -589,7 +243,9 @@ ide_greeter_perspective_dialog_response (IdeGreeterPerspective *self,
     {
       IdeWorkbench *workbench;
 
-      if (NULL != (workbench = ide_widget_get_workbench (GTK_WIDGET (self))))
+      workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+
+      if (workbench != NULL)
         {
           g_autoptr(GFile) project_file = NULL;
 
@@ -597,7 +253,12 @@ ide_greeter_perspective_dialog_response (IdeGreeterPerspective *self,
           gtk_widget_set_sensitive (GTK_WIDGET (self->titlebar), FALSE);
 
           project_file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
-          ide_workbench_open_project_async (workbench, project_file, NULL, NULL, NULL);
+
+          ide_workbench_open_project_async (workbench,
+                                            project_file,
+                                            NULL,
+                                            ide_greeter_perspective_open_project_cb,
+                                            g_object_ref (self));
         }
     }
 
@@ -802,8 +463,10 @@ ide_greeter_perspective_genesis_cancel_clicked (IdeGreeterPerspective *self,
   g_assert (GTK_IS_BUTTON (genesis_cancel_button));
 
   g_cancellable_cancel (self->cancellable);
-  dzl_state_machine_set_state (self->state_machine, "browse");
   ide_greeter_perspective_apply_filter_all (self);
+
+  /* TODO: If there are items, we need to go back to empty */
+  dzl_state_machine_set_state (self->state_machine, "browse");
 }
 
 static void
@@ -1269,7 +932,12 @@ ide_greeter_perspective_section_added (PeasExtensionSet *set,
                                      GTK_WIDGET (section),
                                      "priority", priority,
                                      NULL);
-  gtk_widget_show (GTK_WIDGET (section));
+
+  if (ide_greeter_section_filter (section, self->pattern_spec))
+    {
+      dzl_state_machine_set_state (self->state_machine, "browse");
+      gtk_widget_show (GTK_WIDGET (section));
+    }
 
   IDE_EXIT;
 }
@@ -1297,6 +965,8 @@ ide_greeter_perspective_section_removed (PeasExtensionSet *set,
   gtk_container_remove (GTK_CONTAINER (self->sections_container),
                         GTK_WIDGET (section));
 
+  /* TODO: Might have to switch to empty state */
+
   IDE_EXIT;
 }
 
@@ -1304,13 +974,9 @@ static void
 ide_greeter_perspective_constructed (GObject *object)
 {
   IdeGreeterPerspective *self = (IdeGreeterPerspective *)object;
-  IdeRecentProjects *recent_projects;
 
   G_OBJECT_CLASS (ide_greeter_perspective_parent_class)->constructed (object);
 
-  recent_projects = ide_application_get_recent_projects (IDE_APPLICATION_DEFAULT);
-  ide_greeter_perspective_set_recent_projects (self, recent_projects);
-
   ide_greeter_perspective_load_genesis_addins (self);
 
   self->sections = peas_extension_set_new (peas_engine_get_default (),
@@ -1330,6 +996,20 @@ ide_greeter_perspective_constructed (GObject *object)
 }
 
 static void
+delete_selected_rows (GSimpleAction *simple,
+                      GVariant      *param,
+                      gpointer       user_data)
+{
+  IdeGreeterPerspective *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_GREETER_PERSPECTIVE (self));
+
+  IDE_EXIT;
+}
+
+static void
 ide_greeter_perspective_destroy (GtkWidget *widget)
 {
   IdeGreeterPerspective *self = (IdeGreeterPerspective *)widget;
@@ -1349,52 +1029,12 @@ ide_greeter_perspective_finalize (GObject *object)
 
   ide_clear_weak_pointer (&self->ready_binding);
   g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
-  g_clear_object (&self->signal_group);
-  g_clear_object (&self->recent_projects);
   g_clear_object (&self->cancellable);
 
   G_OBJECT_CLASS (ide_greeter_perspective_parent_class)->finalize (object);
 }
 
 static void
-ide_greeter_perspective_get_property (GObject    *object,
-                                      guint       prop_id,
-                                      GValue     *value,
-                                      GParamSpec *pspec)
-{
-  IdeGreeterPerspective *self = IDE_GREETER_PERSPECTIVE (object);
-
-  switch (prop_id)
-    {
-    case PROP_RECENT_PROJECTS:
-      g_value_set_object (value, ide_greeter_perspective_get_recent_projects (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-ide_greeter_perspective_set_property (GObject      *object,
-                                      guint         prop_id,
-                                      const GValue *value,
-                                      GParamSpec   *pspec)
-{
-  IdeGreeterPerspective *self = IDE_GREETER_PERSPECTIVE (object);
-
-  switch (prop_id)
-    {
-    case PROP_RECENT_PROJECTS:
-      ide_greeter_perspective_set_recent_projects (self, g_value_get_object (value));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
 ide_greeter_perspective_class_init (IdeGreeterPerspectiveClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
@@ -1402,20 +1042,9 @@ ide_greeter_perspective_class_init (IdeGreeterPerspectiveClass *klass)
 
   object_class->finalize = ide_greeter_perspective_finalize;
   object_class->constructed = ide_greeter_perspective_constructed;
-  object_class->get_property = ide_greeter_perspective_get_property;
-  object_class->set_property = ide_greeter_perspective_set_property;
 
   widget_class->destroy = ide_greeter_perspective_destroy;
 
-  properties [PROP_RECENT_PROJECTS] =
-    g_param_spec_object ("recent-projects",
-                         "Recent Projects",
-                         "The recent projects that have been mined.",
-                         IDE_TYPE_RECENT_PROJECTS,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
-
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-greeter-perspective.ui");
   gtk_widget_class_set_css_name (widget_class, "greeter");
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, cancel_button);
@@ -1427,8 +1056,6 @@ ide_greeter_perspective_class_init (IdeGreeterPerspectiveClass *klass)
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, info_bar);
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, info_bar_label);
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, info_bar_revealer);
-  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, my_projects_container);
-  gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, my_projects_list_box);
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, open_button);
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, remove_button);
   gtk_widget_class_bind_template_child (widget_class, IdeGreeterPerspective, scrolled_window);
@@ -1451,13 +1078,6 @@ ide_greeter_perspective_init (IdeGreeterPerspective *self)
   g_autoptr(GSimpleActionGroup) group = NULL;
   g_autoptr(GAction) state = NULL;
 
-  self->signal_group = dzl_signal_group_new (IDE_TYPE_RECENT_PROJECTS);
-  dzl_signal_group_connect_object (self->signal_group,
-                                   "items-changed",
-                                   G_CALLBACK (recent_projects_items_changed),
-                                   self,
-                                   G_CONNECT_SWAPPED);
-
   gtk_widget_init_template (GTK_WIDGET (self));
 
   g_signal_connect (self->titlebar,
@@ -1477,18 +1097,6 @@ ide_greeter_perspective_init (IdeGreeterPerspective *self)
                            self,
                            G_CONNECT_SWAPPED);
 
-  g_signal_connect_object (self->my_projects_list_box,
-                           "row-activated",
-                           G_CALLBACK (ide_greeter_perspective__row_activated),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  g_signal_connect_object (self->my_projects_list_box,
-                           "keynav-failed",
-                           G_CALLBACK (ide_greeter_perspective__keynav_failed),
-                           self,
-                           G_CONNECT_SWAPPED);
-
   g_signal_connect_object (self->top_stack,
                            "notify::visible-child",
                            G_CALLBACK (ide_greeter_perspective_genesis_changed),
@@ -1525,14 +1133,6 @@ ide_greeter_perspective_init (IdeGreeterPerspective *self)
                            self,
                            G_CONNECT_SWAPPED);
 
-  gtk_list_box_set_sort_func (self->my_projects_list_box,
-                              ide_greeter_perspective_sort_rows,
-                              NULL, NULL);
-
-  gtk_list_box_set_filter_func (self->my_projects_list_box,
-                                ide_greeter_perspective_filter_row,
-                                self, NULL);
-
   group = g_simple_action_group_new ();
   state = dzl_state_machine_create_action (self->state_machine, "state");
   g_action_map_add_action (G_ACTION_MAP (group), state);
diff --git a/src/libide/greeter/ide-greeter-perspective.ui b/src/libide/greeter/ide-greeter-perspective.ui
index 3411887..f47d37d 100644
--- a/src/libide/greeter/ide-greeter-perspective.ui
+++ b/src/libide/greeter/ide-greeter-perspective.ui
@@ -91,56 +91,6 @@
                                             <property name="visible">true</property>
                                           </object>
                                         </child>
-                                        <child>
-                                          <object class="GtkBox" id="my_projects_container">
-                                            <property name="orientation">vertical</property>
-                                            <property name="spacing">6</property>
-                                            <property name="visible">true</property>
-                                            <child>
-                                              <object class="GtkBox">
-                                                <property name="visible">true</property>
-                                                <child>
-                                                  <object class="GtkLabel" id="my_projects_label">
-                                                    <property name="label" translatable="yes">Recent 
Projects</property>
-                                                    <property name="visible">true</property>
-                                                    <property name="xalign">0.0</property>
-                                                    <property name="hexpand">true</property>
-                                                    <style>
-                                                      <class name="dim-label"/>
-                                                    </style>
-                                                    <attributes>
-                                                      <attribute name="weight" value="bold"/>
-                                                    </attributes>
-                                                  </object>
-                                                </child>
-                                                <child>
-                                                  <object class="GtkLabel">
-                                                    <property name="visible">true</property>
-                                                    <property name="label" 
translatable="yes">Updated</property>
-                                                    <property name="margin-end">10</property>
-                                                    <property name="halign">end</property>
-                                                    <style>
-                                                      <class name="dim-label"/>
-                                                    </style>
-                                                  </object>
-                                                </child>
-                                              </object>
-                                            </child>
-                                            <child>
-                                              <object class="GtkFrame">
-                                                <property name="halign">center</property>
-                                                <property name="visible">true</property>
-                                                <property name="width-request">600</property>
-                                                <child>
-                                                  <object class="GtkListBox" id="my_projects_list_box">
-                                                    <property name="visible">true</property>
-                                                    <property name="selection-mode">none</property>
-                                                  </object>
-                                                </child>
-                                              </object>
-                                            </child>
-                                          </object>
-                                        </child>
                                       </object>
                                     </child>
                                   </object>
@@ -227,6 +177,15 @@
             </attributes>
           </object>
         </child>
+        <child>
+          <object class="GtkLabel" id="empty_title">
+            <property name="visible">true</property>
+            <property name="label" translatable="yes">Builder</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
       </object>
     </child>
     <child>
@@ -305,7 +264,7 @@
     </child>
   </object>
   <object class="DzlStateMachine" id="state_machine">
-    <property name="state">browse</property>
+    <property name="state">empty</property>
     <states>
       <state name="browse">
         <object id="titlebar">
@@ -335,6 +294,9 @@
         <object id="genesis_continue_button">
           <property name="visible">false</property>
         </object>
+        <object id="stack">
+          <property name="visible-child-name">projects</property>
+        </object>
         <object id="top_stack">
           <property name="visible-child-name">greeter</property>
         </object>
@@ -403,6 +365,35 @@
           <property name="visible-child-name">genesis</property>
         </object>
       </state>
+      <state name="empty">
+        <object id="titlebar">
+          <property name="show-close-button">true</property>
+        </object>
+        <object id="action_bar">
+          <property name="visible">false</property>
+        </object>
+        <object id="title_stack">
+          <property name="visible-child">empty_title</property>
+        </object>
+        <object id="cancel_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="selection_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="genesis_buttons">
+          <property name="visible">true</property>
+        </object>
+        <object id="genesis_cancel_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="genesis_continue_button">
+          <property name="visible">false</property>
+        </object>
+        <object id="stack">
+          <property name="visible-child-name">empty-state</property>
+        </object>
+      </state>
     </states>
   </object>
 </interface>
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 10eadee..aaa620e 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -54,6 +54,7 @@ subdir('project-tree')
 subdir('python-gi-imports-completion')
 subdir('python-pack')
 subdir('quick-highlight')
+subdir('recent')
 subdir('retab')
 subdir('rust-langserv')
 subdir('rustup')
diff --git a/src/plugins/recent/gbp-recent-project-row.c b/src/plugins/recent/gbp-recent-project-row.c
new file mode 100644
index 0000000..174ca4d
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-project-row.c
@@ -0,0 +1,387 @@
+/* gbp-recent-project-row.c
+ *
+ * Copyright © 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-recent-project-row"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "gbp-recent-project-row.h"
+
+struct _GbpRecentProjectRow
+{
+  GtkListBoxRow    parent_instance;
+
+  IdeProjectInfo  *project_info;
+  DzlBindingGroup *bindings;
+  gchar           *search_text;
+
+  GtkLabel        *date_label;
+  GtkLabel        *description_label;
+  GtkBox          *tags_box;
+  GtkLabel        *location_label;
+  GtkLabel        *title_label;
+  GtkCheckButton  *checkbox;
+};
+
+G_DEFINE_TYPE (GbpRecentProjectRow, gbp_recent_project_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_PROJECT_INFO,
+  PROP_SELECTED,
+  PROP_SELECTION_MODE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+static GFile      *home_dir;
+
+void
+gbp_recent_project_row_set_selection_mode (GbpRecentProjectRow *self,
+                                            gboolean              selection_mode)
+{
+  g_return_if_fail (GBP_IS_RECENT_PROJECT_ROW (self));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->checkbox), selection_mode);
+}
+
+IdeProjectInfo *
+gbp_recent_project_row_get_project_info (GbpRecentProjectRow *self)
+{
+  g_return_val_if_fail (GBP_IS_RECENT_PROJECT_ROW (self), NULL);
+
+  return self->project_info;
+}
+
+static void
+gbp_recent_project_row_create_search_text (GbpRecentProjectRow *self,
+                                            IdeProjectInfo       *project_info)
+{
+  const gchar *tmp;
+  IdeDoap *doap;
+  GString *str;
+  GFile *file;
+
+  g_assert (GBP_IS_RECENT_PROJECT_ROW (self));
+
+  str = g_string_new (NULL);
+
+  if ((tmp = ide_project_info_get_name (project_info)))
+    {
+      g_autofree gchar *downcase = g_utf8_strdown (tmp, -1);
+
+      g_string_append (str, tmp);
+      g_string_append (str, " ");
+      g_string_append (str, downcase);
+      g_string_append (str, " ");
+    }
+
+  if ((tmp = ide_project_info_get_description (project_info)))
+    {
+      g_string_append (str, tmp);
+      g_string_append (str, " ");
+    }
+
+  doap = ide_project_info_get_doap (project_info);
+
+  if (doap != NULL)
+    {
+      if ((tmp = ide_doap_get_description (doap)))
+        {
+          g_string_append (str, tmp);
+          g_string_append (str, " ");
+        }
+    }
+
+  file = ide_project_info_get_file (project_info);
+
+  if (file != NULL)
+    {
+      g_autoptr(GFile) parent = g_file_get_parent (file);
+      g_autofree gchar *dir = parent ? g_file_get_basename (parent) : NULL;
+      g_autofree gchar *base = g_file_get_basename (file);
+
+      if (dir != NULL)
+        {
+          g_string_append (str, dir);
+          g_string_append (str, " ");
+        }
+
+      if (base != NULL)
+        {
+          g_string_append (str, base);
+          g_string_append (str, " ");
+        }
+    }
+
+  g_free (self->search_text);
+  self->search_text = g_strdelimit (g_string_free (str, FALSE), "\n", ' ');
+}
+
+static void
+gbp_recent_project_row_add_tags (GbpRecentProjectRow *self,
+                                  IdeProjectInfo       *project_info)
+{
+  const gchar * const *languages;
+  const gchar *build_system_name;
+
+  g_return_if_fail (GBP_IS_RECENT_PROJECT_ROW (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  languages = ide_project_info_get_languages (project_info);
+
+  if (languages != NULL)
+    {
+      guint len = g_strv_length ((gchar **)languages);
+      gsize i;
+
+      for (i = len; i > 0; i--)
+        {
+          const gchar *name = languages [i - 1];
+          GtkWidget *pill;
+
+          pill = g_object_new (DZL_TYPE_PILL_BOX,
+                               "visible", TRUE,
+                               "label", name,
+                               NULL);
+          gtk_container_add (GTK_CONTAINER (self->tags_box), pill);
+        }
+    }
+
+  build_system_name = ide_project_info_get_build_system_name (project_info);
+  if (!ide_str_empty0 (build_system_name))
+    {
+      GtkWidget *pill;
+
+      pill = g_object_new (DZL_TYPE_PILL_BOX,
+                           "visible", TRUE,
+                           "label", build_system_name,
+                           NULL);
+      gtk_container_add (GTK_CONTAINER (self->tags_box), pill);
+    }
+}
+
+static void
+gbp_recent_project_row_set_project_info (GbpRecentProjectRow *self,
+                                          IdeProjectInfo       *project_info)
+{
+  g_return_if_fail (GBP_IS_RECENT_PROJECT_ROW (self));
+  g_return_if_fail (!project_info || IDE_IS_PROJECT_INFO (project_info));
+
+  if (g_set_object (&self->project_info, project_info))
+    {
+      dzl_binding_group_set_source (self->bindings, project_info);
+
+      if (project_info != NULL)
+        {
+          gbp_recent_project_row_add_tags (self, project_info);
+          gbp_recent_project_row_create_search_text (self, project_info);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROJECT_INFO]);
+    }
+}
+
+static gboolean
+humanize_date_time (GBinding     *binding,
+                    const GValue *from_value,
+                    GValue       *to_value,
+                    gpointer      user_data)
+{
+  GDateTime *dt;
+  gchar *str;
+
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_DATE_TIME));
+  g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
+
+  if (!(dt = g_value_get_boxed (from_value)))
+    return FALSE;
+
+  str = dzl_g_date_time_format_for_display (dt);
+  g_value_take_string (to_value, str);
+
+  return TRUE;
+}
+
+static gboolean
+truncate_location (GBinding     *binding,
+                   const GValue *from_value,
+                   GValue       *to_value,
+                   gpointer      user_data)
+{
+  GFile *file;
+  gchar *uri;
+
+  g_assert (G_VALUE_HOLDS (from_value, G_TYPE_FILE));
+  g_assert (G_VALUE_HOLDS (to_value, G_TYPE_STRING));
+
+  if (!(file = g_value_get_object (from_value)))
+    return FALSE;
+
+  if (g_file_is_native (file))
+    {
+      gchar *relative_path;
+
+      if ((relative_path = g_file_get_relative_path (home_dir, file)) ||
+          (relative_path = g_file_get_path (file)))
+        {
+          g_value_take_string (to_value, relative_path);
+          return TRUE;
+        }
+
+      g_free (relative_path);
+    }
+
+  uri = g_file_get_uri (file);
+  g_value_set_string (to_value, uri);
+
+  return TRUE;
+}
+
+const gchar *
+gbp_recent_project_row_get_search_text (GbpRecentProjectRow *self)
+{
+  g_return_val_if_fail (GBP_IS_RECENT_PROJECT_ROW (self), NULL);
+
+  return self->search_text;
+}
+
+static void
+gbp_recent_project_row_finalize (GObject *object)
+{
+  GbpRecentProjectRow *self = (GbpRecentProjectRow *)object;
+
+  g_clear_object (&self->project_info);
+  g_clear_object (&self->bindings);
+  g_clear_pointer (&self->search_text, g_free);
+
+  G_OBJECT_CLASS (gbp_recent_project_row_parent_class)->finalize (object);
+}
+
+static void
+gbp_recent_project_row_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  GbpRecentProjectRow *self = GBP_RECENT_PROJECT_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_INFO:
+      g_value_set_object (value, gbp_recent_project_row_get_project_info (self));
+      break;
+
+    case PROP_SELECTED:
+      g_object_get_property (G_OBJECT (self->checkbox), "active", value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_recent_project_row_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  GbpRecentProjectRow *self = GBP_RECENT_PROJECT_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTED:
+      g_object_set_property (G_OBJECT (self->checkbox), "active", value);
+      break;
+
+    case PROP_SELECTION_MODE:
+      gbp_recent_project_row_set_selection_mode (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_PROJECT_INFO:
+      gbp_recent_project_row_set_project_info (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_recent_project_row_class_init (GbpRecentProjectRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_recent_project_row_finalize;
+  object_class->get_property = gbp_recent_project_row_get_property;
+  object_class->set_property = gbp_recent_project_row_set_property;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               
"/org/gnome/builder/plugins/recent-plugin/gbp-recent-project-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, checkbox);
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, date_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, description_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, location_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, tags_box);
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, title_label);
+
+  properties [PROP_SELECTED] =
+    g_param_spec_boolean ("selected",
+                          "Selected",
+                          "Selected",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SELECTION_MODE] =
+    g_param_spec_boolean ("selection-mode",
+                          "Selection Mode",
+                          "Selection Mode",
+                          FALSE,
+                          (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PROJECT_INFO] =
+    g_param_spec_object ("project-info",
+                         "Project Information",
+                         "The project information to render.",
+                         IDE_TYPE_PROJECT_INFO,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  home_dir = g_file_new_for_path (g_get_home_dir ());
+}
+
+static void
+gbp_recent_project_row_init (GbpRecentProjectRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "name", self->title_label, "label", 0);
+  dzl_binding_group_bind_full (self->bindings, "last-modified-at", self->date_label, "label", 0,
+                               humanize_date_time, NULL, NULL, NULL);
+  dzl_binding_group_bind_full (self->bindings, "directory", self->location_label, "label", 0,
+                               truncate_location, NULL, NULL, NULL);
+  dzl_binding_group_bind (self->bindings, "description", self->description_label, "label", 0);
+
+  g_object_bind_property (self->checkbox, "active", self, "selected", 0);
+}
diff --git a/src/plugins/recent/gbp-recent-project-row.h b/src/plugins/recent/gbp-recent-project-row.h
new file mode 100644
index 0000000..ae06239
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-project-row.h
@@ -0,0 +1,34 @@
+/* ide-greeter-project-row.h
+ *
+ * Copyright © 2015 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#include <ide.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_RECENT_PROJECT_ROW (gbp_recent_project_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRecentProjectRow, gbp_recent_project_row, GBP, RECENT_PROJECT_ROW, GtkListBoxRow)
+
+IdeProjectInfo *gbp_recent_project_row_get_project_info   (GbpRecentProjectRow *self);
+const gchar    *gbp_recent_project_row_get_search_text    (GbpRecentProjectRow *self);
+void            gbp_recent_project_row_set_selection_mode (GbpRecentProjectRow *self,
+                                                           gboolean             selection_mode);
+
+G_END_DECLS
diff --git a/src/plugins/recent/gbp-recent-project-row.ui b/src/plugins/recent/gbp-recent-project-row.ui
new file mode 100644
index 0000000..4f924a3
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-project-row.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="GbpRecentProjectRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkCheckButton" id="checkbox">
+            <property name="margin-start">12</property>
+            <property name="valign">center</property>
+            <property name="vexpand">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="margin">12</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel" id="title_label">
+                    <property name="visible">true</property>
+                    <property name="hexpand">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">0.0</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="date_label">
+                    <property name="visible">true</property>
+                    <property name="hexpand">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">1.0</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="description_label">
+                <property name="single-line-mode">true</property>
+                <property name="ellipsize">end</property>
+                <property name="valign">baseline</property>
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+                <property name="spacing">6</property>
+                <child>
+                  <object class="GtkLabel" id="location_label">
+                    <property name="hexpand">true</property>
+                    <property name="visible">true</property>
+                    <property name="valign">baseline</property>
+                    <property name="xalign">0.0</property>
+                    <property name="ellipsize">middle</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox" id="tags_box">
+                    <property name="spacing">3</property>
+                    <property name="orientation">horizontal</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/recent/gbp-recent-section.c b/src/plugins/recent/gbp-recent-section.c
new file mode 100644
index 0000000..fca623b
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-section.c
@@ -0,0 +1,212 @@
+/* gbp-recent-section.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#define G_LOG_DOMAIN "gbp-recent-section"
+
+#include <ide.h>
+
+#include "gbp-recent-project-row.h"
+#include "gbp-recent-section.h"
+
+struct _GbpRecentSection
+{
+  GtkBin      parent_instance;
+  GtkListBox *listbox;
+};
+
+static gint
+gbp_recent_section_get_priority (IdeGreeterSection *section)
+{
+  return 0;
+}
+
+static void
+gbp_recent_section_filter_cb (GtkListBoxRow *row,
+                              gpointer       user_data)
+{
+  struct {
+    DzlPatternSpec *spec;
+    gboolean found;
+  } *filter = user_data;
+  gboolean match;
+
+  g_assert (GBP_IS_RECENT_PROJECT_ROW (row));
+  g_assert (filter != NULL);
+
+  if (filter->spec != NULL)
+    {
+      const gchar *search_text;
+
+      search_text = gbp_recent_project_row_get_search_text (GBP_RECENT_PROJECT_ROW (row));
+      match = dzl_pattern_spec_match (filter->spec, search_text);
+    }
+
+  gtk_widget_set_visible (GTK_WIDGET (row), match);
+
+  filter->found |= match;
+}
+
+static gboolean
+gbp_recent_section_filter (IdeGreeterSection *section,
+                           DzlPatternSpec    *spec)
+{
+  GbpRecentSection *self = (GbpRecentSection *)section;
+  struct {
+    DzlPatternSpec *spec;
+    gboolean found;
+  } filter = { spec, FALSE };
+
+  g_assert (GBP_IS_RECENT_SECTION (self));
+
+  /* We don't use filter func here so that we know if any
+   * rows matched. We have to hide our widget if there are
+   * no visible matches.
+   */
+
+  gtk_container_foreach (GTK_CONTAINER (self->listbox),
+                         (GtkCallback) gbp_recent_section_filter_cb,
+                         &filter);
+
+  return filter.found;
+}
+
+static void
+gbp_recent_section_activate_first_cb (GtkWidget *widget,
+                                      gpointer   user_data)
+{
+  IdeProjectInfo *project_info;
+  struct {
+    GbpRecentSection *self;
+    gboolean handled;
+  } *activate = user_data;
+
+  g_assert (GBP_IS_RECENT_PROJECT_ROW (widget));
+  g_assert (activate != NULL);
+
+  if (activate->handled || !gtk_widget_get_visible (widget))
+    return;
+
+  project_info = gbp_recent_project_row_get_project_info (GBP_RECENT_PROJECT_ROW (widget));
+  ide_greeter_section_emit_project_activated (IDE_GREETER_SECTION (activate->self), project_info);
+
+  activate->handled = TRUE;
+}
+
+static gboolean
+gbp_recent_section_activate_first (IdeGreeterSection *section)
+{
+  GbpRecentSection *self = (GbpRecentSection *)section;
+  struct {
+    GbpRecentSection *self;
+    gboolean handled;
+  } activate;
+
+  g_return_val_if_fail (GBP_IS_RECENT_SECTION (self), FALSE);
+
+  activate.self = self;
+  activate.handled = FALSE;
+
+  gtk_container_foreach (GTK_CONTAINER (self->listbox),
+                         gbp_recent_section_activate_first_cb,
+                         &activate);
+
+  return activate.handled;
+}
+
+static void
+greeter_section_iface_init (IdeGreeterSectionInterface *iface)
+{
+  iface->get_priority = gbp_recent_section_get_priority;
+  iface->filter = gbp_recent_section_filter;
+  iface->activate_first = gbp_recent_section_activate_first;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpRecentSection, gbp_recent_section, GTK_TYPE_BIN,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_GREETER_SECTION,
+                                                greeter_section_iface_init))
+
+static void
+gbp_recent_section_row_activated (GbpRecentSection    *self,
+                                  GbpRecentProjectRow *row,
+                                  GtkListBox          *list_box)
+{
+  IdeProjectInfo *project_info;
+
+  g_assert (GBP_IS_RECENT_SECTION (self));
+  g_assert (GBP_IS_RECENT_PROJECT_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  project_info = gbp_recent_project_row_get_project_info (row);
+
+  ide_greeter_section_emit_project_activated (IDE_GREETER_SECTION (self), project_info);
+}
+
+static GtkWidget *
+create_widget_func (gpointer item,
+                    gpointer user_data)
+{
+  IdeProjectInfo *project_info = item;
+  GbpRecentProjectRow *row;
+
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  row = g_object_new (GBP_TYPE_RECENT_PROJECT_ROW,
+                      "project-info", project_info,
+                      "visible", TRUE,
+                      NULL);
+
+  return GTK_WIDGET (row);
+}
+
+static void
+gbp_recent_section_constructed (GObject *object)
+{
+  GbpRecentSection *self = (GbpRecentSection *)object;
+  IdeRecentProjects *projects;
+
+  G_OBJECT_CLASS (gbp_recent_section_parent_class)->constructed (object);
+
+  projects = ide_application_get_recent_projects (IDE_APPLICATION_DEFAULT);
+
+  gtk_list_box_bind_model (self->listbox,
+                           G_LIST_MODEL (projects),
+                           create_widget_func, self, NULL);
+}
+
+static void
+gbp_recent_section_class_init (GbpRecentSectionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = gbp_recent_section_constructed;
+
+  gtk_widget_class_set_css_name (widget_class, "recent");
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               
"/org/gnome/builder/plugins/recent-plugin/gbp-recent-section.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpRecentSection, listbox);
+  gtk_widget_class_bind_template_callback (widget_class, gbp_recent_section_row_activated);
+
+  g_type_ensure (GBP_TYPE_RECENT_PROJECT_ROW);
+}
+
+static void
+gbp_recent_section_init (GbpRecentSection *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/plugins/recent/gbp-recent-section.h b/src/plugins/recent/gbp-recent-section.h
new file mode 100644
index 0000000..63a0593
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-section.h
@@ -0,0 +1,29 @@
+/* gbp-recent-section.h
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_RECENT_SECTION (gbp_recent_section_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRecentSection, gbp_recent_section, GBP, RECENT_SECTION, GtkBin)
+
+G_END_DECLS
diff --git a/src/plugins/recent/gbp-recent-section.ui b/src/plugins/recent/gbp-recent-section.ui
new file mode 100644
index 0000000..bbe07d2
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-section.ui
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpRecentSection" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlBox">
+            <property name="halign">center</property>
+            <property name="hexpand">false</property>
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <property name="max-width-request">600</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="spacing">6</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Recent Projects</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">0.0</property>
+                        <property name="hexpand">true</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">true</property>
+                        <property name="label" translatable="yes">Updated</property>
+                        <property name="margin-end">10</property>
+                        <property name="halign">end</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkFrame">
+                    <property name="halign">center</property>
+                    <property name="visible">true</property>
+                    <property name="width-request">600</property>
+                    <child>
+                      <object class="GtkListBox" id="listbox">
+                        <property name="visible">true</property>
+                        <property name="selection-mode">none</property>
+                        <signal name="row-activated" swapped="true" object="GbpRecentSection" 
handler="gbp_recent_section_row_activated"/>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/recent/meson.build b/src/plugins/recent/meson.build
new file mode 100644
index 0000000..e8501df
--- /dev/null
+++ b/src/plugins/recent/meson.build
@@ -0,0 +1,16 @@
+recent_resources = gnome.compile_resources(
+  'recent-resources',
+  'recent.gresource.xml',
+  c_name: 'gbp_recent',
+)
+
+recent_sources = [
+  'recent-plugin.c',
+  'gbp-recent-project-row.c',
+  'gbp-recent-project-row.h',
+  'gbp-recent-section.c',
+  'gbp-recent-section.h',
+]
+
+gnome_builder_plugins_sources += files(recent_sources)
+gnome_builder_plugins_sources += recent_resources[0]
diff --git a/src/plugins/recent/recent-plugin.c b/src/plugins/recent/recent-plugin.c
new file mode 100644
index 0000000..6c669da
--- /dev/null
+++ b/src/plugins/recent/recent-plugin.c
@@ -0,0 +1,30 @@
+/* recent-plugin.c
+ *
+ * Copyright (C) 2017 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/>.
+ */
+
+#include <libpeas/peas.h>
+#include <ide.h>
+
+#include "gbp-recent-section.h"
+
+void
+gbp_recent_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_GREETER_SECTION,
+                                              GBP_TYPE_RECENT_SECTION);
+}
diff --git a/src/plugins/recent/recent.gresource.xml b/src/plugins/recent/recent.gresource.xml
new file mode 100644
index 0000000..587e695
--- /dev/null
+++ b/src/plugins/recent/recent.gresource.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder/plugins">
+    <file>recent.plugin</file>
+  </gresource>
+  <gresource prefix="/org/gnome/builder/plugins/recent-plugin">
+    <file>gbp-recent-project-row.ui</file>
+    <file>gbp-recent-section.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/recent/recent.plugin b/src/plugins/recent/recent.plugin
new file mode 100644
index 0000000..d5c3385
--- /dev/null
+++ b/src/plugins/recent/recent.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Module=recent-plugin
+Name=Recent Projects
+Description=Shows recent projects in the greeter
+Authors=Christian Hergert <christian hergert me>
+Copyright=Copyright © 2017 Christian Hergert
+Builtin=true
+Hidden=true
+Embedded=gbp_recent_register_types


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