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