1 /* gbp-create-project-surface.c
2  *
3  * Copyright 2016-2019 Christian Hergert <christian@hergert.me>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "gbp-create-project-surface"
22 
23 #include <dazzle.h>
24 #include <glib/gi18n.h>
25 #include <libide-greeter.h>
26 #include <libide-projects.h>
27 #include <libide-vcs.h>
28 #include <libpeas/peas.h>
29 #include <stdlib.h>
30 #include <string.h>
31 
32 #include "ide-greeter-private.h"
33 
34 #include "gbp-create-project-template-icon.h"
35 #include "gbp-create-project-surface.h"
36 
37 struct _GbpCreateProjectSurface
38 {
39   IdeSurface            parent;
40 
41   PeasExtensionSet     *providers;
42 
43   GtkEntry             *app_id_entry;
44   GtkEntry             *project_name_entry;
45   DzlFileChooserEntry  *project_location_entry;
46   DzlRadioBox          *project_language_chooser;
47   GtkFlowBox           *project_template_chooser;
48   GtkSwitch            *versioning_switch;
49   DzlRadioBox          *license_chooser;
50   GtkLabel             *destination_label;
51   GtkButton            *create_button;
52 
53   guint                 invalid_directory : 1;
54 };
55 
56 enum {
57   PROP_0,
58   PROP_IS_READY,
59   N_PROPS
60 };
61 
62 static GParamSpec *properties [N_PROPS];
63 
G_DEFINE_FINAL_TYPE(GbpCreateProjectSurface,gbp_create_project_surface,IDE_TYPE_SURFACE)64 G_DEFINE_FINAL_TYPE (GbpCreateProjectSurface, gbp_create_project_surface, IDE_TYPE_SURFACE)
65 
66 static gboolean
67 is_preferred (const gchar *name)
68 {
69   return 0 == strcasecmp (name, "c") ||
70          0 == strcasecmp (name, "rust") ||
71          0 == strcasecmp (name, "javascript") ||
72          0 == strcasecmp (name, "python");
73 }
74 
75 static int
sort_by_name(gconstpointer a,gconstpointer b)76 sort_by_name (gconstpointer a,
77               gconstpointer b)
78 {
79   const gchar * const *astr = a;
80   const gchar * const *bstr = b;
81   gboolean apref = is_preferred (*astr);
82   gboolean bpref = is_preferred (*bstr);
83 
84   if (apref && !bpref)
85     return -1;
86   else if (!apref && bpref)
87     return 1;
88 
89   return g_utf8_collate (*astr, *bstr);
90 }
91 
92 static void
gbp_create_project_surface_add_languages(GbpCreateProjectSurface * self,const GList * templates)93 gbp_create_project_surface_add_languages (GbpCreateProjectSurface *self,
94                                          const GList            *templates)
95 {
96   g_autoptr(GHashTable) languages = NULL;
97   g_autofree const gchar **keys = NULL;
98   const GList *iter;
99   guint len;
100 
101   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
102 
103   languages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
104 
105   for (iter = templates; iter != NULL; iter = iter->next)
106     {
107       IdeProjectTemplate *template = iter->data;
108       g_auto(GStrv) template_languages = NULL;
109 
110       g_assert (IDE_IS_PROJECT_TEMPLATE (template));
111 
112       template_languages = ide_project_template_get_languages (template);
113 
114       for (guint i = 0; template_languages [i]; i++)
115         g_hash_table_add (languages, g_strdup (template_languages [i]));
116     }
117 
118   keys = (const gchar **)g_hash_table_get_keys_as_array (languages, &len);
119   qsort (keys, len, sizeof (gchar *), sort_by_name);
120   for (guint i = 0; keys[i]; i++)
121     dzl_radio_box_add_item (self->project_language_chooser, keys[i], keys[i]);
122 }
123 
124 static gboolean
validate_name(const gchar * name)125 validate_name (const gchar *name)
126 {
127   if (name == NULL)
128     return FALSE;
129 
130   if (g_unichar_isdigit (g_utf8_get_char (name)))
131     return FALSE;
132 
133   // meson reserved this as keyword and therefore its not allowed as project name
134   if (ide_str_equal0 (name, "test"))
135     return FALSE;
136 
137   for (; *name; name = g_utf8_next_char (name))
138     {
139       gunichar ch = g_utf8_get_char (name);
140 
141       if (g_unichar_isspace (ch))
142         return FALSE;
143 
144       if (ch == '/')
145         return FALSE;
146     }
147 
148   return TRUE;
149 }
150 
151 static gboolean
directory_exists(GbpCreateProjectSurface * self,const gchar * name)152 directory_exists (GbpCreateProjectSurface *self,
153                   const gchar            *name)
154 {
155   g_autoptr(GFile) directory = NULL;
156   g_autoptr(GFile) child = NULL;
157 
158   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
159   g_assert (name != NULL);
160 
161   directory = dzl_file_chooser_entry_get_file (self->project_location_entry);
162   child = g_file_get_child (directory, name);
163 
164   self->invalid_directory = g_file_query_exists (child, NULL);
165 
166   return self->invalid_directory;
167 }
168 
169 static void
gbp_create_project_surface_create_cb(GObject * object,GAsyncResult * result,gpointer user_data)170 gbp_create_project_surface_create_cb (GObject      *object,
171                                       GAsyncResult *result,
172                                       gpointer      user_data)
173 {
174   GbpCreateProjectSurface *self = (GbpCreateProjectSurface *)object;
175   g_autoptr(GError) error = NULL;
176 
177   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
178   g_assert (G_IS_ASYNC_RESULT (result));
179   g_assert (user_data == NULL);
180 
181   if (!gbp_create_project_surface_create_finish (self, result, &error))
182     {
183       g_warning ("Failed to create project: %s", error->message);
184     }
185 }
186 
187 static void
gbp_create_project_surface_create_clicked(GbpCreateProjectSurface * self,GtkButton * button)188 gbp_create_project_surface_create_clicked (GbpCreateProjectSurface *self,
189                                            GtkButton               *button)
190 {
191   GCancellable *cancellable;
192   GtkWidget *workspace;
193 
194   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
195   g_assert (GTK_IS_BUTTON (button));
196 
197   workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_WORKSPACE);
198   cancellable = ide_workspace_get_cancellable (IDE_WORKSPACE (workspace));
199 
200   gbp_create_project_surface_create_async (self,
201                                            cancellable,
202                                            gbp_create_project_surface_create_cb,
203                                            NULL);
204 }
205 
206 static void
gbp_create_project_surface_name_changed(GbpCreateProjectSurface * self,GtkEntry * entry)207 gbp_create_project_surface_name_changed (GbpCreateProjectSurface *self,
208                                         GtkEntry               *entry)
209 {
210   g_autofree gchar *project_name = NULL;
211   const gchar *text;
212 
213   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
214   g_assert (GTK_IS_ENTRY (entry));
215 
216   text = gtk_entry_get_text (entry);
217   project_name = g_strstrip (g_strdup (text));
218 
219   if (ide_str_empty0 (project_name) || !validate_name (project_name))
220     {
221       g_object_set (self->project_name_entry,
222                     "secondary-icon-name", "dialog-warning-symbolic",
223                     "tooltip-text", _("Characters were used which might cause technical issues as a project name"),
224                     NULL);
225       gtk_label_set_label (self->destination_label,
226                            _("Your project will be created within a new child directory."));
227     }
228   else if (directory_exists (self, project_name))
229     {
230       g_object_set (self->project_name_entry,
231                     "secondary-icon-name", "dialog-warning-symbolic",
232                     "tooltip-text", _("Directory already exists with that name"),
233                     NULL);
234       gtk_label_set_label (self->destination_label, NULL);
235     }
236   else
237     {
238       g_autofree gchar *formatted = NULL;
239       g_autoptr(GFile) file = dzl_file_chooser_entry_get_file (self->project_location_entry);
240       g_autoptr(GFile) child = g_file_get_child (file, project_name);
241       g_autofree gchar *path = g_file_get_path (child);
242       g_autofree gchar *collapsed = ide_path_collapse (path);
243 
244       g_object_set (self->project_name_entry,
245                     "secondary-icon-name", NULL,
246                     "tooltip-text", NULL,
247                     NULL);
248 
249       /* translators: %s is replaced with a short-form file-system path to the project */
250       formatted = g_strdup_printf (_("Your project will be created within %s."), collapsed);
251       gtk_label_set_label (self->destination_label, formatted);
252     }
253 
254   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
255 }
256 
257 static guint
count_chars(const gchar * str,gunichar ch)258 count_chars (const gchar *str,
259              gunichar     ch)
260 {
261   guint count = 0;
262   for (; *str; str = g_utf8_next_char (str))
263     count += *str == ch;
264   return count;
265 }
266 
267 static gboolean
application_id_is_valid(const char * app_id)268 application_id_is_valid (const char *app_id)
269 {
270   /* g_application_id_is_valid() is necessary, but also not restrictive
271    * enough for new application ids that we need to work with flatpak.
272    */
273   if (!g_application_id_is_valid (app_id))
274     return FALSE;
275 
276   /* We need at least a.b.c */
277   if (count_chars (app_id, '.') < 2)
278     return FALSE;
279 
280   /* - isn't allowed for Flatpak application ids */
281   if (strchr (app_id, '-') != NULL)
282     return FALSE;
283 
284   return TRUE;
285 }
286 
287 static void
gbp_create_project_surface_app_id_changed(GbpCreateProjectSurface * self,GtkEntry * entry)288 gbp_create_project_surface_app_id_changed (GbpCreateProjectSurface *self,
289                                           GtkEntry               *entry)
290 {
291   const gchar *app_id;
292 
293   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
294   g_assert (GTK_IS_ENTRY (entry));
295 
296   app_id = gtk_entry_get_text (entry);
297 
298   if (app_id[0] && !application_id_is_valid (app_id))
299     g_object_set (self->app_id_entry,
300                   "secondary-icon-name", "dialog-warning-symbolic",
301                   "tooltip-text", _("Application ID is not valid."),
302                   NULL);
303   else
304     g_object_set (self->app_id_entry,
305                   "secondary-icon-name", NULL,
306                   "tooltip-text", NULL,
307                   NULL);
308 
309   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
310 }
311 
312 static void
gbp_create_project_surface_location_changed(GbpCreateProjectSurface * self,GParamSpec * pspec,DzlFileChooserEntry * chooser)313 gbp_create_project_surface_location_changed (GbpCreateProjectSurface *self,
314                                             GParamSpec             *pspec,
315                                             DzlFileChooserEntry    *chooser)
316 {
317   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
318   g_assert (DZL_IS_FILE_CHOOSER_ENTRY (chooser));
319 
320   /* Piggyback on the name changed signal to update things */
321   gbp_create_project_surface_name_changed (self, self->project_name_entry);
322 }
323 
324 static void
update_language_sensitivity(GtkWidget * widget,gpointer data)325 update_language_sensitivity (GtkWidget *widget,
326                              gpointer   data)
327 {
328   GbpCreateProjectSurface *self = data;
329   GbpCreateProjectTemplateIcon *template_icon;
330   IdeProjectTemplate *template;
331   g_auto(GStrv) template_languages = NULL;
332   const gchar *language;
333   gboolean sensitive = FALSE;
334   gint i;
335 
336   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
337   g_assert (GTK_IS_FLOW_BOX_CHILD (widget));
338 
339   language = dzl_radio_box_get_active_id (self->project_language_chooser);
340 
341   if (ide_str_empty0 (language))
342     goto apply;
343 
344   template_icon = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (widget)));
345   g_object_get (template_icon,
346                 "template", &template,
347                 NULL);
348   template_languages = ide_project_template_get_languages (template);
349 
350   for (i = 0; template_languages [i]; i++)
351     {
352       if (g_str_equal (language, template_languages [i]))
353         {
354           sensitive = TRUE;
355           goto apply;
356         }
357     }
358 
359 apply:
360   gtk_widget_set_sensitive (widget, sensitive);
361 }
362 
363 static void
gbp_create_project_surface_refilter(GbpCreateProjectSurface * self)364 gbp_create_project_surface_refilter (GbpCreateProjectSurface *self)
365 {
366   gtk_container_foreach (GTK_CONTAINER (self->project_template_chooser),
367                          update_language_sensitivity,
368                          self);
369 }
370 
371 static void
gbp_create_project_surface_language_changed(GbpCreateProjectSurface * self,DzlRadioBox * language_chooser)372 gbp_create_project_surface_language_changed (GbpCreateProjectSurface *self,
373                                             DzlRadioBox            *language_chooser)
374 {
375   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
376   g_assert (DZL_IS_RADIO_BOX (language_chooser));
377 
378   gbp_create_project_surface_refilter (self);
379 
380   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
381 }
382 
383 static void
gbp_create_project_surface_template_selected(GbpCreateProjectSurface * self,GtkFlowBox * box,GtkFlowBoxChild * child)384 gbp_create_project_surface_template_selected (GbpCreateProjectSurface *self,
385                                              GtkFlowBox             *box,
386                                              GtkFlowBoxChild        *child)
387 {
388   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
389 
390   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
391 }
392 
393 static gint
project_template_sort_func(GtkFlowBoxChild * child1,GtkFlowBoxChild * child2,gpointer user_data)394 project_template_sort_func (GtkFlowBoxChild *child1,
395                             GtkFlowBoxChild *child2,
396                             gpointer         user_data)
397 {
398   GbpCreateProjectTemplateIcon *icon1;
399   GbpCreateProjectTemplateIcon *icon2;
400   IdeProjectTemplate *tmpl1;
401   IdeProjectTemplate *tmpl2;
402 
403   icon1 = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (child1)));
404   icon2 = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (child2)));
405 
406   tmpl1 = gbp_create_project_template_icon_get_template (icon1);
407   tmpl2 = gbp_create_project_template_icon_get_template (icon2);
408 
409   return ide_project_template_compare (tmpl1, tmpl2);
410 }
411 
412 static void
gbp_create_project_surface_add_template_buttons(GbpCreateProjectSurface * self,GList * templates)413 gbp_create_project_surface_add_template_buttons (GbpCreateProjectSurface *self,
414                                                  GList                   *templates)
415 {
416   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
417 
418   for (const GList *iter = templates; iter; iter = iter->next)
419     {
420       IdeProjectTemplate *template = iter->data;
421       GbpCreateProjectTemplateIcon *template_icon;
422       GtkFlowBoxChild *template_container;
423 
424       g_assert (IDE_IS_PROJECT_TEMPLATE (template));
425 
426       template_icon = g_object_new (GBP_TYPE_CREATE_PROJECT_TEMPLATE_ICON,
427                                     "visible", TRUE,
428                                     "template", template,
429                                     NULL);
430 
431       template_container = g_object_new (GTK_TYPE_FLOW_BOX_CHILD,
432                                          "visible", TRUE,
433                                          NULL);
434       gtk_container_add (GTK_CONTAINER (template_container), GTK_WIDGET (template_icon));
435       gtk_flow_box_insert (self->project_template_chooser, GTK_WIDGET (template_container), -1);
436     }
437 }
438 
439 static void
gbp_create_project_surface_provider_added_cb(PeasExtensionSet * set,PeasPluginInfo * plugin_info,PeasExtension * exten,gpointer user_data)440 gbp_create_project_surface_provider_added_cb (PeasExtensionSet *set,
441                                               PeasPluginInfo   *plugin_info,
442                                               PeasExtension    *exten,
443                                               gpointer          user_data)
444 {
445   GbpCreateProjectSurface *self = user_data;
446   IdeTemplateProvider *provider = (IdeTemplateProvider *)exten;
447   g_autolist(IdeProjectTemplate) templates = NULL;
448   GtkFlowBoxChild *child;
449 
450   g_assert (IDE_IS_MAIN_THREAD ());
451   g_assert (PEAS_IS_EXTENSION_SET (set));
452   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
453   g_assert (IDE_IS_TEMPLATE_PROVIDER (provider));
454 
455   templates = ide_template_provider_get_project_templates (provider);
456 
457   gbp_create_project_surface_add_template_buttons (self, templates);
458   gbp_create_project_surface_add_languages (self, templates);
459 
460   gtk_flow_box_invalidate_sort (self->project_template_chooser);
461   gbp_create_project_surface_refilter (self);
462 
463   /*
464    * We do the following after every add, because we might get some delayed
465    * additions for templates during startup.
466    */
467 
468   /* Default to C, always. We might investigate setting this to the
469    * previously selected item in the future.
470    */
471   dzl_radio_box_set_active_id (self->project_language_chooser, "C");
472 
473   /* Select the first template that is visible so we have a selection
474    * initially without the user having to select. We might also try to
475    * re-select a previous item in the future.
476    */
477   if ((child = gtk_flow_box_get_child_at_index (self->project_template_chooser, 0)))
478     gtk_flow_box_select_child (self->project_template_chooser, child);
479 }
480 
481 static GFile *
gbp_create_project_surface_get_directory(GbpCreateProjectSurface * self)482 gbp_create_project_surface_get_directory (GbpCreateProjectSurface *self)
483 {
484   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
485 
486   return dzl_file_chooser_entry_get_file (self->project_location_entry);
487 }
488 
489 static void
gbp_create_project_surface_set_directory(GbpCreateProjectSurface * self,GFile * directory)490 gbp_create_project_surface_set_directory (GbpCreateProjectSurface *self,
491                                          GFile                  *directory)
492 {
493   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
494   g_assert (G_IS_FILE (directory));
495 
496   dzl_file_chooser_entry_set_file (self->project_location_entry, directory);
497 }
498 
499 static void
gbp_create_project_surface_constructed(GObject * object)500 gbp_create_project_surface_constructed (GObject *object)
501 {
502   GbpCreateProjectSurface *self = GBP_CREATE_PROJECT_SURFACE (object);
503 
504   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
505 
506   G_OBJECT_CLASS (gbp_create_project_surface_parent_class)->constructed (object);
507 
508   self->providers = peas_extension_set_new (peas_engine_get_default (),
509                                             IDE_TYPE_TEMPLATE_PROVIDER,
510                                             NULL);
511 
512   g_signal_connect (self->providers,
513                     "extension-added",
514                     G_CALLBACK (gbp_create_project_surface_provider_added_cb),
515                     self);
516 
517   peas_extension_set_foreach (self->providers,
518                               gbp_create_project_surface_provider_added_cb,
519                               self);
520 }
521 
522 static void
gbp_create_project_surface_destroy(GtkWidget * widget)523 gbp_create_project_surface_destroy (GtkWidget *widget)
524 {
525   GbpCreateProjectSurface *self = (GbpCreateProjectSurface *)widget;
526 
527   g_clear_object (&self->providers);
528 
529   GTK_WIDGET_CLASS (gbp_create_project_surface_parent_class)->destroy (widget);
530 }
531 
532 static gboolean
gbp_create_project_surface_is_ready(GbpCreateProjectSurface * self)533 gbp_create_project_surface_is_ready (GbpCreateProjectSurface *self)
534 {
535   const gchar *text;
536   g_autofree gchar *project_name = NULL;
537   const gchar *app_id;
538   const gchar *language = NULL;
539   GList *selected_template = NULL;
540   gboolean ret = FALSE;
541 
542   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
543 
544   if (self->invalid_directory)
545     return FALSE;
546 
547   text = gtk_entry_get_text (self->project_name_entry);
548   project_name = g_strstrip (g_strdup (text));
549 
550   if (ide_str_empty0 (project_name) || !validate_name (project_name))
551     return FALSE;
552 
553   app_id = gtk_entry_get_text (self->app_id_entry);
554   if (app_id[0] && !application_id_is_valid (app_id))
555     return FALSE;
556 
557   language = dzl_radio_box_get_active_id (self->project_language_chooser);
558 
559   if (ide_str_empty0 (language))
560     return FALSE;
561 
562   selected_template = gtk_flow_box_get_selected_children (self->project_template_chooser);
563 
564   if (selected_template == NULL)
565     return FALSE;
566 
567   ret = gtk_widget_get_sensitive (selected_template->data);
568 
569   g_list_free (selected_template);
570 
571   return ret;
572 }
573 
574 static void
gbp_create_project_surface_grab_focus(GtkWidget * widget)575 gbp_create_project_surface_grab_focus (GtkWidget *widget)
576 {
577   gtk_widget_grab_focus (GTK_WIDGET (GBP_CREATE_PROJECT_SURFACE (widget)->project_name_entry));
578 }
579 
580 static void
gbp_create_project_surface_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)581 gbp_create_project_surface_get_property (GObject    *object,
582                                         guint       prop_id,
583                                         GValue     *value,
584                                         GParamSpec *pspec)
585 {
586   GbpCreateProjectSurface *self = GBP_CREATE_PROJECT_SURFACE(object);
587 
588   switch (prop_id)
589     {
590     case PROP_IS_READY:
591       g_value_set_boolean (value, gbp_create_project_surface_is_ready (self));
592       break;
593 
594     default:
595       G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
596     }
597 }
598 
599 static void
gbp_create_project_surface_class_init(GbpCreateProjectSurfaceClass * klass)600 gbp_create_project_surface_class_init (GbpCreateProjectSurfaceClass *klass)
601 {
602   GObjectClass *object_class = G_OBJECT_CLASS (klass);
603   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
604 
605   object_class->constructed = gbp_create_project_surface_constructed;
606   object_class->get_property = gbp_create_project_surface_get_property;
607 
608   widget_class->destroy = gbp_create_project_surface_destroy;
609   widget_class->grab_focus = gbp_create_project_surface_grab_focus;
610 
611   properties [PROP_IS_READY] =
612     g_param_spec_boolean ("is-ready",
613                           "Is Ready",
614                           "Is Ready",
615                           FALSE,
616                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
617 
618   g_object_class_install_properties (object_class, N_PROPS, properties);
619 
620   gtk_widget_class_set_css_name (widget_class, "createprojectsurface");
621   gtk_widget_class_set_template_from_resource (widget_class, "/plugins/create-project/gbp-create-project-surface.ui");
622   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, app_id_entry);
623   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, create_button);
624   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, destination_label);
625   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, license_chooser);
626   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_language_chooser);
627   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_location_entry);
628   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_name_entry);
629   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_template_chooser);
630   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, versioning_switch);
631 }
632 
633 static void
gbp_create_project_surface_init(GbpCreateProjectSurface * self)634 gbp_create_project_surface_init (GbpCreateProjectSurface *self)
635 {
636   g_autoptr(GFile) projects_dir = NULL;
637 
638   gtk_widget_init_template (GTK_WIDGET (self));
639 
640   gtk_widget_set_name (GTK_WIDGET (self), "create-project");
641   ide_surface_set_title (IDE_SURFACE (self), C_("title", "Start New Project"));
642 
643   projects_dir = g_file_new_for_path (ide_get_projects_dir ());
644   gbp_create_project_surface_set_directory (self, projects_dir);
645 
646   g_signal_connect_object (self->project_name_entry,
647                            "changed",
648                            G_CALLBACK (gbp_create_project_surface_name_changed),
649                            self,
650                            G_CONNECT_SWAPPED);
651 
652   g_signal_connect_object (self->app_id_entry,
653                            "changed",
654                            G_CALLBACK (gbp_create_project_surface_app_id_changed),
655                            self,
656                            G_CONNECT_SWAPPED);
657 
658   g_signal_connect_object (self->project_location_entry,
659                            "notify::file",
660                            G_CALLBACK (gbp_create_project_surface_location_changed),
661                            self,
662                            G_CONNECT_SWAPPED);
663 
664   g_signal_connect_object (self->project_language_chooser,
665                            "changed",
666                            G_CALLBACK (gbp_create_project_surface_language_changed),
667                            self,
668                            G_CONNECT_SWAPPED);
669 
670   g_signal_connect_object (self->project_template_chooser,
671                            "child-activated",
672                            G_CALLBACK (gbp_create_project_surface_template_selected),
673                            self,
674                            G_CONNECT_SWAPPED);
675 
676   g_signal_connect_object (self->create_button,
677                            "clicked",
678                            G_CALLBACK (gbp_create_project_surface_create_clicked),
679                            self,
680                            G_CONNECT_SWAPPED);
681 
682   gtk_flow_box_set_sort_func (self->project_template_chooser,
683                               project_template_sort_func,
684                               NULL, NULL);
685 
686   g_object_bind_property (self, "is-ready", self->create_button, "sensitive",
687                           G_BINDING_SYNC_CREATE);
688 }
689 
690 static void
init_vcs_cb(GObject * object,GAsyncResult * result,gpointer user_data)691 init_vcs_cb (GObject      *object,
692              GAsyncResult *result,
693              gpointer      user_data)
694 {
695   IdeVcsInitializer *vcs = (IdeVcsInitializer *)object;
696   g_autoptr(IdeTask) task = user_data;
697   g_autoptr(IdeProjectInfo) project_info = NULL;
698   g_autoptr(GError) error = NULL;
699   GbpCreateProjectSurface *self;
700   GtkWidget *workspace;
701   GFile *project_file;
702 
703   g_assert (G_IS_ASYNC_RESULT (result));
704   g_assert (IDE_IS_TASK (task));
705 
706   if (!ide_vcs_initializer_initialize_finish (vcs, result, &error))
707     {
708       ide_task_return_error (task, g_steal_pointer (&error));
709       goto cleanup;
710     }
711 
712   self = ide_task_get_source_object (task);
713   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
714 
715   project_file = ide_task_get_task_data (task);
716 
717   project_info = ide_project_info_new ();
718   ide_project_info_set_file (project_info, project_file);
719   ide_project_info_set_directory (project_info, project_file);
720 
721   workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
722   ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
723 
724   ide_task_return_boolean (task, TRUE);
725 
726 cleanup:
727   ide_object_destroy (IDE_OBJECT (vcs));
728 }
729 
730 static void
extract_cb(GObject * object,GAsyncResult * result,gpointer user_data)731 extract_cb (GObject      *object,
732             GAsyncResult *result,
733             gpointer      user_data)
734 {
735   IdeProjectTemplate *template = (IdeProjectTemplate *)object;
736   g_autoptr(IdeTask) task = user_data;
737   g_autoptr(IdeVcsInitializer) vcs = NULL;
738   g_autoptr(GError) error = NULL;
739   GbpCreateProjectSurface *self;
740   PeasPluginInfo *plugin_info;
741   PeasEngine *engine;
742   IdeContext *context;
743   GFile *project_file;
744 
745   /* To keep the UI simple, we only support git from
746    * the creation today. However, at the time of writing
747    * that is our only supported VCS anyway. If you'd like to
748    * add support for an additional VCS, we need to redesign
749    * this part of the UI.
750    */
751   const gchar *vcs_id = "git";
752 
753   g_assert (IDE_IS_PROJECT_TEMPLATE (template));
754   g_assert (G_IS_ASYNC_RESULT (result));
755   g_assert (IDE_IS_TASK (task));
756 
757   if (!ide_project_template_expand_finish (template, result, &error))
758     {
759       ide_task_return_error (task, g_steal_pointer (&error));
760       return;
761     }
762 
763   self = ide_task_get_source_object (task);
764   g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
765 
766   project_file = ide_task_get_task_data (task);
767   g_assert (G_IS_FILE (project_file));
768 
769   if (!gtk_switch_get_active (self->versioning_switch))
770     {
771       g_autoptr(IdeProjectInfo) project_info = NULL;
772       GtkWidget *workspace;
773 
774       project_info = ide_project_info_new ();
775       ide_project_info_set_file (project_info, project_file);
776       ide_project_info_set_directory (project_info, project_file);
777 
778       workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
779       ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
780 
781       ide_task_return_boolean (task, TRUE);
782       return;
783     }
784 
785   engine = peas_engine_get_default ();
786   plugin_info = peas_engine_get_plugin_info (engine, vcs_id);
787   if (plugin_info == NULL)
788     IDE_GOTO (failure);
789 
790   context = ide_widget_get_context (GTK_WIDGET (self));
791   vcs = (IdeVcsInitializer *)peas_engine_create_extension (engine, plugin_info,
792                                                            IDE_TYPE_VCS_INITIALIZER,
793                                                            "parent", context,
794                                                            NULL);
795   if (vcs == NULL)
796     IDE_GOTO (failure);
797 
798   ide_vcs_initializer_initialize_async (vcs,
799                                         project_file,
800                                         ide_task_get_cancellable (task),
801                                         init_vcs_cb,
802                                         g_object_ref (task));
803 
804   return;
805 
806 failure:
807   ide_task_return_new_error (task,
808                              G_IO_ERROR,
809                              G_IO_ERROR_FAILED,
810                              _("A failure occurred while initializing version control"));
811 }
812 
813 void
gbp_create_project_surface_create_async(GbpCreateProjectSurface * self,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)814 gbp_create_project_surface_create_async (GbpCreateProjectSurface *self,
815                                          GCancellable            *cancellable,
816                                          GAsyncReadyCallback      callback,
817                                          gpointer                 user_data)
818 {
819   g_autoptr(IdeTask) task = NULL;
820   g_autoptr(GHashTable) params = NULL;
821   g_autoptr(IdeProjectTemplate) template = NULL;
822   g_autoptr(IdeVcsConfig) vcs_conf = NULL;
823   GValue str = G_VALUE_INIT;
824   g_autofree gchar *name = NULL;
825   g_autofree gchar *path = NULL;
826   g_autoptr(GFile) location = NULL;
827   g_autoptr(GFile) child = NULL;
828   const gchar *language = NULL;
829   const gchar *license_id = NULL;
830   GtkFlowBoxChild *template_container;
831   GbpCreateProjectTemplateIcon *template_icon;
832   PeasEngine *engine;
833   PeasPluginInfo *plugin_info;
834   const gchar *text;
835   const gchar *app_id;
836   const gchar *vcs_id = "git";
837   const gchar *author_name;
838   GList *selected_box_child;
839 
840   g_return_if_fail (GBP_CREATE_PROJECT_SURFACE (self));
841   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
842 
843   gtk_widget_set_sensitive (GTK_WIDGET (self->create_button), FALSE);
844   gtk_widget_set_sensitive (GTK_WIDGET (self->license_chooser), FALSE);
845   gtk_widget_set_sensitive (GTK_WIDGET (self->project_language_chooser), FALSE);
846   gtk_widget_set_sensitive (GTK_WIDGET (self->project_location_entry), FALSE);
847   gtk_widget_set_sensitive (GTK_WIDGET (self->project_name_entry), FALSE);
848   gtk_widget_set_sensitive (GTK_WIDGET (self->project_template_chooser), FALSE);
849   gtk_widget_set_sensitive (GTK_WIDGET (self->versioning_switch), FALSE);
850 
851   selected_box_child = gtk_flow_box_get_selected_children (self->project_template_chooser);
852   template_container = selected_box_child->data;
853   template_icon = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (template_container)));
854   g_object_get (template_icon,
855                 "template", &template,
856                 NULL);
857   g_list_free (selected_box_child);
858 
859   params = g_hash_table_new_full (g_str_hash,
860                                   g_str_equal,
861                                   g_free,
862                                   (GDestroyNotify)g_variant_unref);
863 
864   text = gtk_entry_get_text (self->project_name_entry);
865   name = g_strstrip (g_strdup (text));
866   g_hash_table_insert (params,
867                        g_strdup ("name"),
868                        g_variant_ref_sink (g_variant_new_string (g_strdelimit (name, " ", '-'))));
869 
870   location = gbp_create_project_surface_get_directory (self);
871   child = g_file_get_child (location, name);
872   path = g_file_get_path (child);
873 
874   g_hash_table_insert (params,
875                        g_strdup ("path"),
876                        g_variant_ref_sink (g_variant_new_string (path)));
877 
878   language = dzl_radio_box_get_active_id (self->project_language_chooser);
879   g_hash_table_insert (params,
880                        g_strdup ("language"),
881                        g_variant_ref_sink (g_variant_new_string (language)));
882 
883   license_id = dzl_radio_box_get_active_id (DZL_RADIO_BOX (self->license_chooser));
884 
885   if (!g_str_equal (license_id, "none"))
886     {
887       g_autofree gchar *license_full_path = NULL;
888       g_autofree gchar *license_short_path = NULL;
889 
890       license_full_path = g_strjoin (NULL, "resource://", "/plugins/create-project/license/full/", license_id, NULL);
891       license_short_path = g_strjoin (NULL, "resource://", "/plugins/create-project/license/short/", license_id, NULL);
892 
893       g_hash_table_insert (params,
894                            g_strdup ("license_full"),
895                            g_variant_ref_sink (g_variant_new_string (license_full_path)));
896 
897       g_hash_table_insert (params,
898                            g_strdup ("license_short"),
899                            g_variant_ref_sink (g_variant_new_string (license_short_path)));
900     }
901 
902   if (gtk_switch_get_active (self->versioning_switch))
903     {
904       g_hash_table_insert (params,
905                            g_strdup ("versioning"),
906                            g_variant_ref_sink (g_variant_new_string ("git")));
907 
908       engine = peas_engine_get_default ();
909       plugin_info = peas_engine_get_plugin_info (engine, vcs_id);
910 
911       if (plugin_info != NULL)
912         {
913           IdeContext *context;
914 
915           context = ide_widget_get_context (GTK_WIDGET (self));
916           vcs_conf = (IdeVcsConfig *)peas_engine_create_extension (engine, plugin_info,
917                                                                    IDE_TYPE_VCS_CONFIG,
918                                                                    "parent", context,
919                                                                    NULL);
920 
921           if (vcs_conf != NULL)
922             {
923               g_value_init (&str, G_TYPE_STRING);
924               ide_vcs_config_get_config (vcs_conf, IDE_VCS_CONFIG_FULL_NAME, &str);
925             }
926         }
927     }
928 
929   if (G_VALUE_HOLDS_STRING (&str) && !ide_str_empty0 (g_value_get_string (&str)))
930     author_name = g_value_get_string (&str);
931   else
932     author_name = g_get_real_name ();
933 
934   app_id = gtk_entry_get_text (self->app_id_entry);
935 
936   if (ide_str_empty0 (app_id))
937     app_id = "org.example.App";
938 
939   g_hash_table_insert (params,
940                        g_strdup ("author"),
941                        g_variant_take_ref (g_variant_new_string (author_name)));
942 
943   g_hash_table_insert (params,
944                        g_strdup ("app-id"),
945                        g_variant_take_ref (g_variant_new_string (app_id)));
946 
947   g_value_unset (&str);
948 
949   task = ide_task_new (self, cancellable, callback, user_data);
950   ide_task_set_task_data (task, g_file_new_for_path (path), g_object_unref);
951 
952   ide_project_template_expand_async (template,
953                                      params,
954                                      NULL,
955                                      extract_cb,
956                                      g_object_ref (task));
957 }
958 
959 gboolean
gbp_create_project_surface_create_finish(GbpCreateProjectSurface * self,GAsyncResult * result,GError ** error)960 gbp_create_project_surface_create_finish (GbpCreateProjectSurface  *self,
961                                           GAsyncResult             *result,
962                                           GError                  **error)
963 {
964   g_return_val_if_fail (GBP_IS_CREATE_PROJECT_SURFACE (self), FALSE);
965   g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
966 
967   gtk_widget_set_sensitive (GTK_WIDGET (self->create_button), TRUE);
968   gtk_widget_set_sensitive (GTK_WIDGET (self->license_chooser), TRUE);
969   gtk_widget_set_sensitive (GTK_WIDGET (self->project_language_chooser), TRUE);
970   gtk_widget_set_sensitive (GTK_WIDGET (self->project_location_entry), TRUE);
971   gtk_widget_set_sensitive (GTK_WIDGET (self->project_name_entry), TRUE);
972   gtk_widget_set_sensitive (GTK_WIDGET (self->project_template_chooser), TRUE);
973   gtk_widget_set_sensitive (GTK_WIDGET (self->versioning_switch), TRUE);
974 
975   return ide_task_propagate_boolean (IDE_TASK (result), error);
976 }
977