1 module dlangide.ui.newproject;
2 
3 import dlangui.core.types;
4 import dlangui.core.i18n;
5 import dlangui.platforms.common.platform;
6 import dlangui.dialogs.dialog;
7 import dlangui.dialogs.filedlg;
8 import dlangui.widgets.widget;
9 import dlangui.widgets.layouts;
10 import dlangui.widgets.editors;
11 import dlangui.widgets.controls;
12 import dlangui.widgets.lists;
13 import dlangui.dml.parser;
14 import dlangui.core.stdaction;
15 import dlangui.core.files;
16 import dlangide.workspace.project;
17 import dlangide.workspace.workspace;
18 import dlangide.ui.commands;
19 import dlangide.ui.frame;
20 
21 import std.path;
22 import std.file;
23 import std.array : empty;
24 import std.utf : toUTF32, toUTF8;
25 
26 class ProjectCreationResult {
27     Workspace workspace;
28     Project project;
29     this(Workspace workspace, Project project) {
30         this.workspace = workspace;
31         this.project = project;
32     }
33 }
34 
35 class NewProjectDlg : Dialog {
36 
37     Workspace _currentWorkspace;
38     IDEFrame _ide;
39 
40     this(IDEFrame parent, bool newWorkspace, Workspace currentWorkspace) {
41         super(newWorkspace ? UIString("New Workspace"d) : UIString("New Project"d), parent.window, 
42               DialogFlag.Modal | DialogFlag.Resizable | DialogFlag.Popup, 500, 400);
43         _ide = parent;
44         _icon = "dlangui-logo1";
45         this._currentWorkspace = currentWorkspace;
46         _newWorkspace = newWorkspace;
47         _location = currentWorkspace !is null ? currentWorkspace.dir : currentDir;
48     }
49 
50     /// override to implement creation of dialog controls
51     override void initialize() {
52         super.initialize();
53         initTemplates();
54         Widget content;
55         try {
56             content = parseML(q{
57                     VerticalLayout {
58                         id: vlayout
59                         padding: Rect { 5, 5, 5, 5 }
60                         layoutWidth: fill; layoutHeight: fill
61                         HorizontalLayout {
62                             layoutWidth: fill; layoutHeight: fill
63                             VerticalLayout {
64                                 margins: 5
65                                 layoutWidth: 25%; layoutHeight: fill
66                                 TextWidget { text: "Project template" }
67                                 StringListWidget { 
68                                     id: projectTemplateList 
69                                     layoutWidth: wrap; layoutHeight: fill
70                                 }
71                             }
72                             VerticalLayout {
73                                 margins: 5
74                                 layoutWidth: 40%; layoutHeight: fill
75                                 TextWidget { text: "Template description" }
76                                 EditBox { 
77                                     id: templateDescription; readOnly: true 
78                                     layoutWidth: fill; layoutHeight: fill
79                                 }
80                             }
81                             VerticalLayout {
82                                 layoutWidth: 35%; layoutHeight: fill
83                                 margins: 5
84                                 TextWidget { text: "Directory layout" }
85                                 EditBox { 
86                                     id: directoryLayout; readOnly: true
87                                     layoutWidth: fill; layoutHeight: fill
88                                 }
89                             }
90                         }
91                         TableLayout {
92                             margins: 5
93                             colCount: 2
94                             layoutWidth: fill; layoutHeight: wrap
95                             TextWidget { text: "" }
96                             CheckBox { id: cbCreateWorkspace; text: "Create new solution"; checked: true }
97                             TextWidget { text: "Workspace name" }
98                             EditLine { id: edWorkspaceName; text: "newworkspace"; layoutWidth: fill }
99                             TextWidget { text: "" }
100                             CheckBox { id: cbCreateWorkspaceSubdir; text: "Create subdirectory for workspace"; checked: true }
101                             TextWidget { text: "Project name" }
102                             EditLine { id: edProjectName; text: "newproject"; layoutWidth: fill }
103                             TextWidget { text: "" }
104                             CheckBox { id: cbCreateSubdir; text: "Create subdirectory for project"; checked: true }
105                             TextWidget { text: "Location" }
106                             DirEditLine { id: edLocation; layoutWidth: fill }
107                         }
108                         TextWidget { id: statusText; text: ""; layoutWidth: fill }
109                     }
110                 });
111         } catch (Exception e) {
112             Log.e("Exceptin while parsing DML", e);
113             throw e;
114         }
115 
116 
117         _projectTemplateList = content.childById!StringListWidget("projectTemplateList");
118         _templateDescription = content.childById!EditBox("templateDescription");
119         _directoryLayout = content.childById!EditBox("directoryLayout");
120         _edProjectName = content.childById!EditLine("edProjectName");
121         _edWorkspaceName = content.childById!EditLine("edWorkspaceName");
122         _cbCreateSubdir = content.childById!CheckBox("cbCreateSubdir");
123         _cbCreateWorkspace = content.childById!CheckBox("cbCreateWorkspace");
124         _cbCreateWorkspaceSubdir = content.childById!CheckBox("cbCreateWorkspaceSubdir");
125         _edLocation = content.childById!DirEditLine("edLocation");
126         _edLocation.text = toUTF32(_location);
127         _statusText = content.childById!TextWidget("statusText");
128 
129         _edLocation.filetypeIcons[".d"] = "text-d";
130         _edLocation.filetypeIcons["dub.json"] = "project-d";
131         _edLocation.filetypeIcons["package.json"] = "project-d";
132         _edLocation.filetypeIcons[".dlangidews"] = "project-development";
133         _edLocation.addFilter(FileFilterEntry(UIString("DlangIDE files"d), "*.dlangidews;*.d;*.dd;*.di;*.ddoc;*.dh;*.json;*.xml;*.ini"));
134         _edLocation.caption = "Select directory"d;
135 
136         if (_currentWorkspace) {
137             _workspaceName = toUTF8(_currentWorkspace.name);
138             _edWorkspaceName.text = toUTF32(_workspaceName);
139             _cbCreateWorkspaceSubdir.enabled = false;
140             _cbCreateWorkspaceSubdir.checked = false;
141         } else {
142             _cbCreateWorkspace.checked = true;
143             _cbCreateWorkspace.enabled = false;
144         }
145         if (!_newWorkspace) {
146             _cbCreateWorkspace.checked = false;
147             _cbCreateWorkspace.enabled = _currentWorkspace !is null;
148             _edWorkspaceName.readOnly = true;
149         }
150 
151 
152         // fill templates
153         dstring[] names;
154         foreach(t; _templates)
155             names ~= t.name;
156         _projectTemplateList.items = names;
157         _projectTemplateList.selectedItemIndex = 0;
158 
159         templateSelected(0);
160         updateDirLayout();
161 
162         // listeners
163         _edProjectName.contentChange = delegate (EditableContent source) {
164             _projectName = toUTF8(source.text);
165             updateDirLayout();
166         };
167 
168         _edWorkspaceName.contentChange = delegate (EditableContent source) {
169             _workspaceName = toUTF8(source.text);
170             updateDirLayout();
171         };
172 
173         _edLocation.contentChange = delegate (EditableContent source) {
174             _location = toUTF8(source.text);
175             updateDirLayout();
176         };
177 
178         _cbCreateWorkspace.checkChange = delegate (Widget source, bool checked) {
179             _edWorkspaceName.readOnly = !checked;
180             _cbCreateWorkspaceSubdir.enabled = checked;
181             updateDirLayout();
182             return true;
183         };
184 
185         _cbCreateSubdir.checkChange = delegate (Widget source, bool checked) {
186             updateDirLayout();
187             return true;
188         };
189 
190         _cbCreateWorkspaceSubdir.checkChange = delegate (Widget source, bool checked) {
191             _edWorkspaceName.readOnly = !checked;
192             updateDirLayout();
193             return true;
194         };
195 
196         _projectTemplateList.itemSelected = delegate (Widget source, int itemIndex) {
197             templateSelected(itemIndex);
198             return true;
199         };
200         _projectTemplateList.itemClick = delegate (Widget source, int itemIndex) {
201             templateSelected(itemIndex);
202             return true;
203         };
204 
205         addChild(content);
206         addChild(createButtonsPanel([_newWorkspace ? ACTION_FILE_NEW_WORKSPACE : ACTION_FILE_NEW_PROJECT, ACTION_CANCEL], 0, 0));
207     }
208 
209     bool _newWorkspace;
210     StringListWidget _projectTypeList;
211     StringListWidget _projectTemplateList;
212     EditBox _templateDescription;
213     EditBox _directoryLayout;
214     DirEditLine _edLocation;
215     EditLine _edWorkspaceName;
216     EditLine _edProjectName;
217     CheckBox _cbCreateSubdir;
218     CheckBox _cbCreateWorkspace;
219     CheckBox _cbCreateWorkspaceSubdir;
220     TextWidget _statusText;
221     string _projectName = "newproject";
222     string _workspaceName = "newworkspace";
223     string _location;
224 
225     int _currentTemplateIndex = -1;
226     ProjectTemplate _currentTemplate;
227     ProjectTemplate[] _templates;
228 
229     protected void templateSelected(int index) {
230         if (_currentTemplateIndex == index)
231             return;
232         _currentTemplateIndex = index;
233         _currentTemplate = _templates[index];
234         _templateDescription.text = _currentTemplate.description;
235         updateDirLayout();
236     }
237 
238     protected void updateDirLayout() {
239         dchar[] buf;
240         dstring level = "";
241         if (_cbCreateWorkspaceSubdir.checked) {
242             buf ~= toUTF32(_workspaceName) ~ "/\n";
243             level ~= "    ";
244         }
245         if (_cbCreateWorkspace.checked) {
246             buf ~= level ~ toUTF32(_workspaceName) ~ toUTF32(WORKSPACE_EXTENSION) ~ "\n";
247         }
248         if (_cbCreateSubdir.checked) {
249             buf ~= level ~ toUTF32(_projectName) ~ "/\n";
250             level ~= "    ";
251         }
252         buf ~= level ~ "dub.json" ~ "\n";
253         buf ~= level ~ "source/" ~ "\n";
254         if (!_currentTemplate.srcfile.empty) {
255             buf ~= level ~ "    " ~ toUTF32(_currentTemplate.srcfile) ~ "\n";
256         }
257         _directoryLayout.text = buf.dup;
258         validate();
259     }
260 
261     bool setError(dstring msg) {
262         _statusText.text = msg;
263         return msg.empty;
264     }
265     dstring getError() {
266         return _statusText.text;
267     }
268 
269     ProjectCreationResult _result;
270 
271     override void close(const Action action) {
272         Action newaction = action.clone();
273         if (action.id == IDEActions.FileNewWorkspace || action.id == IDEActions.FileNewProject) {
274             if (!exists(_location)) {
275                 // show message box with OK and CANCEL buttons, cancel by default, and handle its result
276                 window.showMessageBox(UIString("Cannot create project"d), UIString("The target location does not exist.\nDo you want to create the target directory?"), [ACTION_YES, ACTION_CANCEL], 1, delegate(const Action a) {
277                     if (a.id == StandardAction.Yes) {
278                         try {
279                             mkdirRecurse(_location);
280                             close(action);
281                         } catch (Exception e) {
282                             setError("Cannot create target location");
283                             window.showMessageBox(UIString("Cannot create project"d), UIString(getError()));
284                         }
285                     }
286                     return true;
287                 });
288                 return;
289             }
290             if (!validate()) {
291                 window.showMessageBox(UIString("Cannot create project"d), UIString(getError()));
292                 return;
293             }
294             if (!createProject()) {
295                 window.showMessageBox(UIString("Cannot create project"d), UIString("Failed to create project"));
296                 return;
297             }
298             newaction.objectParam = _result;
299         }
300         super.close(newaction);
301     }
302 
303     bool validate() {
304         if (!exists(_location)) {
305             return setError("The location directory does not exist");
306         }
307         if(!isDir(_location)) {
308             return setError("The location is not a directory");
309         }
310         if (!isValidProjectName(_projectName))
311             return setError("Invalid project name");
312         if (!isValidProjectName(_workspaceName))
313             return setError("Invalid workspace name");
314         return setError("");
315     }
316 
317     bool createProject() {
318         if (!validate())
319             return false;
320         Workspace ws = _currentWorkspace;
321         string wsdir = _location;
322         if (_newWorkspace) {
323             if (_cbCreateWorkspaceSubdir.checked) {
324                 wsdir = buildNormalizedPath(wsdir, _workspaceName);
325                 if (wsdir.exists) {
326                     if (!wsdir.isDir)
327                         return setError("Cannot create workspace directory");
328                 } else {
329                     try {
330                         mkdir(wsdir);
331                     } catch (Exception e) {
332                         return setError("Cannot create workspace directory");
333                     }
334                 }
335             }
336             string wsfilename = buildNormalizedPath(wsdir, _workspaceName ~ WORKSPACE_EXTENSION);
337             ws = new Workspace(_ide, wsfilename);
338             ws.name = toUTF32(_workspaceName);
339             if (!ws.save())
340                 return setError("Cannot create workspace file");
341         }
342         string pdir = wsdir;
343         if (_cbCreateSubdir.checked) {
344             pdir = buildNormalizedPath(pdir, _projectName);
345             if (pdir.exists) {
346                 if (!pdir.isDir)
347                     return setError("Cannot create project directory");
348             } else {
349                 try {
350                     mkdir(pdir);
351                 } catch (Exception e) {
352                     return setError("Cannot create project directory");
353                 }
354             }
355         }
356         string pfile = buildNormalizedPath(pdir, "dub.json");
357         Project project = new Project(ws, pfile);
358         project.name = toUTF32(_projectName);
359         if (!project.save())
360             return setError("Cannot save project");
361         project.content.setString("targetName", _projectName);
362         if (_currentTemplate.isLibrary) {
363             project.content.setString("targetType", "staticLibrary");
364             project.content.setString("targetPath", "lib");
365         } else {
366             project.content.setString("targetType", "executable");
367             project.content.setString("targetPath", "bin");
368         }
369         if (_currentTemplate.json)
370             project.content.merge(_currentTemplate.json);
371         project.save();
372         ws.addProject(project);
373         if (ws.startupProject is null)
374             ws.startupProject = project;
375         string srcdir = buildNormalizedPath(pdir, "source");
376         if (!exists(srcdir))
377             mkdir(srcdir);
378         if (!_currentTemplate.srcfile.empty && !_currentTemplate.srccode.empty) {
379             string srcfile = buildNormalizedPath(srcdir, _currentTemplate.srcfile);
380             write(srcfile, _currentTemplate.srccode);
381         }
382         if (!project.save())
383             return setError("Cannot save project file");
384         if (!ws.save())
385             return setError("Cannot save workspace file");
386         project.load();
387         _result = new ProjectCreationResult(ws, project);
388         return true;
389     }
390 
391     void initTemplates() {
392         _templates ~= new ProjectTemplate("Hello world app"d, "Hello world application."d, "app.d",
393                     SOURCE_CODE_HELLOWORLD, false);
394         _templates ~= new ProjectTemplate("DlangUI: hello world app"d, "Hello world application\nbased on DlangUI library"d, "app.d",
395                     SOURCE_CODE_DLANGUI_HELLOWORLD, false, DUB_JSON_DLANGUI_HELLOWORLD);
396         _templates ~= new ProjectTemplate("vibe.d: Hello world app"d, "Hello world application\nbased on vibe.d framework"d, "app.d",
397                     SOURCE_CODE_VIBED_HELLOWORLD, false, DUB_JSON_VIBED_HELLOWORLD);
398         _templates ~= new ProjectTemplate("Empty app project"d, "Empty application project.\nNo source files."d, null, null, false);
399         _templates ~= new ProjectTemplate("Empty library project"d, "Empty library project.\nNo Source files."d, null, null, true);
400     }
401 }
402 
403 immutable string SOURCE_CODE_HELLOWORLD = q{
404 import std.stdio;
405 void main(string[] args) {
406     writeln("Hello World!");
407     writeln("Press enter...");
408     readln();
409 }
410 };
411 
412 immutable string SOURCE_CODE_VIBED_HELLOWORLD = q{
413 import vibe.d;
414 
415 shared static this()
416 {
417     auto settings = new HTTPServerSettings;
418     settings.port = 8080;
419 
420     listenHTTP(settings, &handleRequest);
421 }
422 
423 void handleRequest(HTTPServerRequest req,
424                     HTTPServerResponse res)
425 {
426     if (req.path == "/")
427         res.writeBody("Hello, World!", "text/plain");
428 }
429 };
430 
431 immutable string DUB_JSON_VIBED_HELLOWORLD = q{
432 {
433     "dependencies": {
434         "vibe-d": "~>0.7.26"
435     },
436     "versions": ["VibeDefaultMain"]
437 }
438 };
439 
440 immutable string SOURCE_CODE_DLANGUI_HELLOWORLD = q{
441 import dlangui;
442 
443 mixin APP_ENTRY_POINT;
444 
445 /// entry point for dlangui based application
446 extern (C) int UIAppMain(string[] args) {
447     // create window
448     Window window = Platform.instance.createWindow("DlangUI HelloWorld", null);
449 
450     // create some widget to show in window
451     //window.mainWidget = (new Button()).text("Hello, world!"d).margins(Rect(20,20,20,20));
452     window.mainWidget = parseML(q{
453         VerticalLayout {
454             margins: 10
455             padding: 10
456             backgroundColor: "#C0E0E070" // semitransparent yellow background
457             // red bold text with size = 150% of base style size and font face Arial
458             TextWidget { text: "Hello World example for DlangUI"; textColor: "red"; fontSize: 150%; fontWeight: 800; fontFace: "Arial" }
459             // arrange controls as form - table with two columns
460             TableLayout {
461                 colCount: 2
462                 TextWidget { text: "param 1" }
463                 EditLine { id: edit1; text: "some text" }
464                 TextWidget { text: "param 2" }
465                 EditLine { id: edit2; text: "some text for param2" }
466                 TextWidget { text: "some radio buttons" }
467                 // arrange some radio buttons vertically
468                 VerticalLayout {
469                     RadioButton { id: rb1; text: "Item 1" }
470                     RadioButton { id: rb2; text: "Item 2" }
471                     RadioButton { id: rb3; text: "Item 3" }
472                 }
473                 TextWidget { text: "and checkboxes" }
474                 // arrange some checkboxes horizontally
475                 HorizontalLayout {
476                     CheckBox { id: cb1; text: "checkbox 1" }
477                     CheckBox { id: cb2; text: "checkbox 2" }
478                 }
479             }
480             HorizontalLayout {
481                 Button { id: btnOk; text: "Ok" }
482                 Button { id: btnCancel; text: "Cancel" }
483             }
484         }
485     });
486     // you can access loaded items by id - e.g. to assign signal listeners
487     auto edit1 = window.mainWidget.childById!EditLine("edit1");
488     auto edit2 = window.mainWidget.childById!EditLine("edit2");
489     // close window on Cancel button click
490     window.mainWidget.childById!Button("btnCancel").click = delegate(Widget w) {
491         window.close();
492         return true;
493     };
494     // show message box with content of editors
495     window.mainWidget.childById!Button("btnOk").click = delegate(Widget w) {
496         window.showMessageBox(UIString("Ok button pressed"d), 
497                               UIString("Editors content\nEdit1: "d ~ edit1.text ~ "\nEdit2: "d ~ edit2.text));
498         return true;
499     };
500 
501     // show window
502     window.show();
503 
504     // run message loop
505     return Platform.instance.enterMessageLoop();
506 }
507 };
508 
509 immutable string DUB_JSON_DLANGUI_HELLOWORLD = q{
510 {
511     "dependencies": {
512         "dlangui": "~master"
513     }
514 }
515 };
516 
517 class ProjectTemplate {
518     dstring name;
519     dstring description;
520     string srcfile;
521     string srccode;
522     bool isLibrary;
523     string json;
524     this(dstring name, dstring description, string srcfile, string srccode, bool isLibrary, string json = null) {
525         this.name = name;
526         this.description = description;
527         this.srcfile = srcfile;
528         this.srccode = srccode;
529         this.isLibrary = isLibrary;
530         this.json = json;
531     }
532 }