1 module dlangide.ui.frame;
2 
3 import dlangui.widgets.menu;
4 import dlangui.widgets.tabs;
5 import dlangui.widgets.layouts;
6 import dlangui.widgets.editors;
7 import dlangui.widgets.srcedit;
8 import dlangui.widgets.controls;
9 import dlangui.widgets.appframe;
10 import dlangui.widgets.docks;
11 import dlangui.widgets.toolbars;
12 import dlangui.widgets.combobox;
13 import dlangui.dialogs.dialog;
14 import dlangui.dialogs.filedlg;
15 import dlangui.core.stdaction;
16 
17 import dlangide.ui.commands;
18 import dlangide.ui.wspanel;
19 import dlangide.ui.outputpanel;
20 import dlangide.ui.dsourceedit;
21 import dlangide.ui.homescreen;
22 import dlangide.workspace.workspace;
23 import dlangide.workspace.project;
24 import dlangide.builders.builder;
25 
26 import std.conv;
27 import std.utf;
28 import std.algorithm;
29 import std.path;
30 
31 bool isSupportedSourceTextFileFormat(string filename) {
32     return (filename.endsWith(".d") || filename.endsWith(".txt") || filename.endsWith(".cpp") || filename.endsWith(".h") || filename.endsWith(".c")
33         || filename.endsWith(".json") || filename.endsWith(".dd") || filename.endsWith(".ddoc") || filename.endsWith(".xml") || filename.endsWith(".html")
34         || filename.endsWith(".html") || filename.endsWith(".css") || filename.endsWith(".log") || filename.endsWith(".hpp"));
35 }
36 
37 class BackgroundOperationWatcherTest : BackgroundOperationWatcher {
38     this(AppFrame frame) {
39         super(frame);
40     }
41     int _counter;
42     /// returns description of background operation to show in status line
43     override @property dstring description() { return "Test progress: "d ~ to!dstring(_counter); }
44     /// returns icon of background operation to show in status line
45     override @property string icon() { return "folder"; }
46     /// update background operation status
47     override void update() {
48         _counter++;
49         if (_counter >= 100)
50             _finished = true;
51         super.update();
52     }
53 }
54 
55 /// DIDE app frame
56 class IDEFrame : AppFrame {
57 
58     MenuItem mainMenuItems;
59     WorkspacePanel _wsPanel;
60     OutputPanel _logPanel;
61     DockHost _dockHost;
62     TabWidget _tabs;
63 
64     dstring frameWindowCaptionSuffix = "DLangIDE"d;
65 
66     this(Window window) {
67         super();
68         window.mainWidget = this;
69     }
70 
71     override protected void init() {
72         super.init();
73     }
74 
75     /// move focus to editor in currently selected tab
76     void focusEditor(string id) {
77         Widget w = _tabs.tabBody(id);
78         if (w) {
79             if (w.visible)
80                 w.setFocus();
81         }
82     }
83 
84     /// source file selected in workspace tree
85     bool onSourceFileSelected(ProjectSourceFile file, bool activate) {
86         Log.d("onSourceFileSelected ", file.filename);
87         return openSourceFile(file.filename, file, activate);
88     }
89 
90     void onModifiedStateChange(Widget source, bool modified) {
91         //
92         Log.d("onModifiedStateChange ", source.id, " modified=", modified);
93         int index = _tabs.tabIndex(source.id);
94         if (index >= 0) {
95             dstring name = toUTF32((modified ? "* " : "") ~ baseName(source.id));
96             _tabs.renameTab(index, name);
97         }
98     }
99 
100     bool openSourceFile(string filename, ProjectSourceFile file = null, bool activate = true) {
101         if (!file)
102             file = _wsPanel.findSourceFileItem(filename);
103         Log.d("openSourceFile ", filename);
104         int index = _tabs.tabIndex(filename);
105         if (index >= 0) {
106             // file is already opened in tab
107             _tabs.selectTab(index, true);
108         } else {
109             // open new file
110             DSourceEdit editor = new DSourceEdit(filename);
111             if (file ? editor.load(file) : editor.load(filename)) {
112                 _tabs.addTab(editor, toUTF32(baseName(filename)));
113                 index = _tabs.tabIndex(filename);
114                 TabItem tab = _tabs.tab(filename);
115                 tab.objectParam = file;
116                 editor.onModifiedStateChangeListener = &onModifiedStateChange;
117                 _tabs.selectTab(index, true);
118             } else {
119                 destroy(editor);
120                 if (window)
121                     window.showMessageBox(UIString("File open error"d), UIString("Failed to open file "d ~ toUTF32(file.filename)));
122                 return false;
123             }
124         }
125         if (activate) {
126             focusEditor(filename);
127         }
128         requestLayout();
129         return true;
130     }
131 
132     static immutable HOME_SCREEN_ID = "HOME_SCREEN";
133     void showHomeScreen() {
134         int index = _tabs.tabIndex(HOME_SCREEN_ID);
135         if (index >= 0) {
136             _tabs.selectTab(index, true);
137         } else {
138             HomeScreen home = new HomeScreen(HOME_SCREEN_ID, this);
139             _tabs.addTab(home, "Home"d);
140             _tabs.selectTab(HOME_SCREEN_ID, true);
141         }
142     }
143 
144     void onTabChanged(string newActiveTabId, string previousTabId) {
145         int index = _tabs.tabIndex(newActiveTabId);
146         if (index >= 0) {
147             TabItem tab = _tabs.tab(index);
148             ProjectSourceFile file = cast(ProjectSourceFile)tab.objectParam;
149             if (file) {
150                 //setCurrentProject(file.project);
151                 // tab is source file editor
152                 _wsPanel.selectItem(file);
153                 focusEditor(file.filename);
154             }
155             window.windowCaption(tab.text.value ~ " - "d ~ frameWindowCaptionSuffix);
156         }
157     }
158 
159     /// close tab w/o confirmation
160     void closeTab(string tabId) {
161         _wsPanel.selectItem(null);
162         _tabs.removeTab(tabId);
163     }
164 
165     /// close all editor tabs
166     void closeAllDocuments() {
167         for (int i = _tabs.tabCount - 1; i >= 0; i--) {
168             DSourceEdit ed = cast(DSourceEdit)_tabs.tabBody(i);
169             if (ed) {
170                 closeTab(ed.id);
171             }
172         }
173     }
174 
175     /// returns first unsaved document
176     protected DSourceEdit hasUnsavedEdits() {
177         for (int i = _tabs.tabCount - 1; i >= 0; i--) {
178             DSourceEdit ed = cast(DSourceEdit)_tabs.tabBody(i);
179             if (ed && ed.content.modified) {
180                 return ed;
181             }
182         }
183         return null;
184     }
185 
186     protected void askForUnsavedEdits(void delegate() onConfirm) {
187         DSourceEdit ed = hasUnsavedEdits();
188         if (!ed) {
189             // no unsaved edits
190             onConfirm();
191             return;
192         }
193         string tabId = ed.id;
194         // tab content is modified - ask for confirmation
195         window.showMessageBox(UIString("Close file "d ~ toUTF32(baseName(tabId))), UIString("Content of this file has been changed."d), 
196                               [ACTION_SAVE, ACTION_SAVE_ALL, ACTION_DISCARD_CHANGES, ACTION_DISCARD_ALL, ACTION_CANCEL], 
197                               0, delegate(const Action result) {
198                                   if (result == StandardAction.Save) {
199                                       // save and close
200                                       ed.save();
201                                       askForUnsavedEdits(onConfirm);
202                                   } else if (result == StandardAction.DiscardChanges) {
203                                       // close, don't save
204                                       closeTab(tabId);
205                                       closeAllDocuments();
206                                       onConfirm();
207                                   } else if (result == StandardAction.SaveAll) {
208                                       ed.save();
209                                       for(;;) {
210                                           DSourceEdit editor = hasUnsavedEdits();
211                                           if (!editor)
212                                               break;
213                                           editor.save();
214                                       }
215                                       closeAllDocuments();
216                                       onConfirm();
217                                   } else if (result == StandardAction.DiscardAll) {
218                                       // close, don't save
219                                       closeAllDocuments();
220                                       onConfirm();
221                                   }
222                                   // else ignore
223                                   return true;
224                               });
225     }
226 
227     protected void onTabClose(string tabId) {
228         Log.d("onTabClose ", tabId);
229         int index = _tabs.tabIndex(tabId);
230         if (index >= 0) {
231             DSourceEdit d = cast(DSourceEdit)_tabs.tabBody(tabId);
232             if (d && d.content.modified) {
233                 // tab content is modified - ask for confirmation
234                 window.showMessageBox(UIString("Close tab"d), UIString("Content of "d ~ toUTF32(baseName(tabId)) ~ " file has been changed."d), 
235                                       [ACTION_SAVE, ACTION_DISCARD_CHANGES, ACTION_CANCEL], 
236                                       0, delegate(const Action result) {
237                                           if (result == StandardAction.Save) {
238                                               // save and close
239                                               d.save();
240                                               closeTab(tabId);
241                                           } else if (result == StandardAction.DiscardChanges) {
242                                               // close, don't save
243                                               closeTab(tabId);
244                                           }
245                                           // else ignore
246                                           return true;
247                                       });
248             } else {
249                 closeTab(tabId);
250             }
251         }
252     }
253 
254     /// create app body widget
255     override protected Widget createBody() {
256         _dockHost = new DockHost();
257 
258         //=============================================================
259         // Create body - Tabs
260 
261         // editor tabs
262         _tabs = new TabWidget("TABS");
263         _tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT);
264         _tabs.onTabChangedListener = &onTabChanged;
265         _tabs.onTabCloseListener = &onTabClose;
266 
267         _dockHost.bodyWidget = _tabs;
268 
269         //=============================================================
270         // Create workspace docked panel
271         _wsPanel = new WorkspacePanel("workspace");
272         _wsPanel.sourceFileSelectionListener = &onSourceFileSelected;
273         _dockHost.addDockedWindow(_wsPanel);
274 
275         _logPanel = new OutputPanel("output");
276         _logPanel.appendText(null, "DlangIDE is started\nHINT: Try to open some DUB project\n"d);
277 
278         _dockHost.addDockedWindow(_logPanel);
279 
280         return _dockHost;
281     }
282 
283     /// create main menu
284     override protected MainMenu createMainMenu() {
285 
286         mainMenuItems = new MenuItem();
287         MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE"));
288         MenuItem fileNewItem = new MenuItem(new Action(1, "MENU_FILE_NEW"));
289         fileNewItem.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_FILE_NEW_WORKSPACE, ACTION_FILE_NEW_PROJECT);
290         fileItem.add(fileNewItem);
291         fileItem.add(ACTION_FILE_OPEN_WORKSPACE, ACTION_FILE_OPEN, 
292                      ACTION_FILE_SAVE, ACTION_FILE_SAVE_AS, ACTION_FILE_SAVE_ALL, ACTION_FILE_EXIT);
293 
294         MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT"));
295 		editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, 
296                      ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO);
297 
298 		editItem.add(new Action(20, "MENU_EDIT_PREFERENCES"));
299 
300         MenuItem projectItem = new MenuItem(new Action(21, "MENU_PROJECT"));
301         projectItem.add(ACTION_PROJECT_SET_STARTUP, ACTION_PROJECT_REFRESH, ACTION_PROJECT_UPDATE_DEPENDENCIES, ACTION_PROJECT_SETTINGS);
302 
303         MenuItem buildItem = new MenuItem(new Action(22, "MENU_BUILD"));
304         buildItem.add(ACTION_WORKSPACE_BUILD, ACTION_WORKSPACE_REBUILD, ACTION_WORKSPACE_CLEAN,
305                      ACTION_PROJECT_BUILD, ACTION_PROJECT_REBUILD, ACTION_PROJECT_CLEAN);
306 
307         MenuItem debugItem = new MenuItem(new Action(23, "MENU_DEBUG"));
308         debugItem.add(ACTION_DEBUG_START, ACTION_DEBUG_START_NO_DEBUG, 
309                       ACTION_DEBUG_CONTINUE, ACTION_DEBUG_STOP, ACTION_DEBUG_PAUSE);
310 
311 
312 		MenuItem windowItem = new MenuItem(new Action(3, "MENU_WINDOW"c));
313         windowItem.add(new Action(30, "MENU_WINDOW_PREFERENCES"));
314         windowItem.add(ACTION_WINDOW_CLOSE_ALL_DOCUMENTS);
315         MenuItem helpItem = new MenuItem(new Action(4, "MENU_HELP"c));
316         helpItem.add(new Action(40, "MENU_HELP_VIEW_HELP"));
317         helpItem.add(ACTION_HELP_ABOUT);
318         mainMenuItems.add(fileItem);
319         mainMenuItems.add(editItem);
320         mainMenuItems.add(projectItem);
321         mainMenuItems.add(buildItem);
322         mainMenuItems.add(debugItem);
323 		//mainMenuItems.add(viewItem);
324 		mainMenuItems.add(windowItem);
325         mainMenuItems.add(helpItem);
326 
327         MainMenu mainMenu = new MainMenu(mainMenuItems);
328         mainMenu.backgroundColor = 0xd6dbe9;
329         return mainMenu;
330     }
331 	
332     /// create app toolbars
333     override protected ToolBarHost createToolbars() {
334         ToolBarHost res = new ToolBarHost();
335         ToolBar tb;
336         tb = res.getOrAddToolbar("Standard");
337         tb.addButtons(ACTION_FILE_OPEN, ACTION_FILE_SAVE, ACTION_SEPARATOR);
338 
339         tb.addButtons(ACTION_DEBUG_START);
340         ToolBarComboBox cbBuildConfiguration = new ToolBarComboBox("buildConfig", ["Debug"d, "Release"d, "Unittest"d]);
341         cbBuildConfiguration.onItemClickListener = delegate(Widget source, int index) {
342             if (currentWorkspace) {
343                 switch(index) {
344                     case 0:
345                         currentWorkspace.buildConfiguration = BuildConfiguration.Debug;
346                         break;
347                     case 1:
348                         currentWorkspace.buildConfiguration = BuildConfiguration.Release;
349                         break;
350                     case 2:
351                         currentWorkspace.buildConfiguration = BuildConfiguration.Unittest;
352                         break;
353                     default:
354                         break;
355                 }
356             }
357             return true;
358         };
359         tb.addControl(cbBuildConfiguration);
360         tb.addButtons(ACTION_PROJECT_BUILD);
361 
362         tb = res.getOrAddToolbar("Edit");
363         tb.addButtons(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_SEPARATOR,
364                       ACTION_EDIT_UNDO, ACTION_EDIT_REDO);
365         return res;
366     }
367 
368     /// override to handle specific actions
369 	override bool handleAction(const Action a) {
370         if (a) {
371             switch (a.id) {
372                 case IDEActions.FileExit:
373                     window.close();
374                     return true;
375                 case IDEActions.HelpAbout:
376                     Window wnd = Platform.instance.createWindow("About...", window, WindowFlag.Modal);
377                     wnd.mainWidget = createAboutWidget();
378                     wnd.show();
379                     return true;
380                 case StandardAction.OpenUrl:
381                     platform.openURL(a.stringParam);
382                     return true;
383                 case IDEActions.FileOpen:
384                     UIString caption;
385                     caption = "Open Text File"d;
386                     FileDialog dlg = new FileDialog(caption, window, null);
387                     dlg.addFilter(FileFilterEntry(UIString("Source files"d), "*.d;*.dd;*.ddoc;*.dh;*.json;*.xml;*.ini"));
388                     dlg.onDialogResult = delegate(Dialog dlg, const Action result) {
389 						if (result.id == ACTION_OPEN.id) {
390                             string filename = result.stringParam;
391                             if (isSupportedSourceTextFileFormat(filename)) {
392                                 openSourceFile(filename);
393                             }
394                         }
395                     };
396                     dlg.show();
397                     return true;
398                 case IDEActions.BuildProject:
399                 case IDEActions.BuildWorkspace:
400                     buildProject(BuildOperation.Build);
401                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
402                     return true;
403                 case IDEActions.RebuildProject:
404                 case IDEActions.RebuildWorkspace:
405                     buildProject(BuildOperation.Rebuild);
406                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
407                     return true;
408                 case IDEActions.CleanProject:
409                 case IDEActions.CleanWorkspace:
410                     buildProject(BuildOperation.Clean);
411                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
412                     return true;
413                 case IDEActions.DebugStart:
414                 case IDEActions.DebugStartNoDebug:
415                 case IDEActions.DebugContinue:
416                     buildProject(BuildOperation.Run);
417                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
418                     return true;
419                 case IDEActions.UpdateProjectDependencies:
420                     buildProject(BuildOperation.Upgrade);
421                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
422                     return true;
423                 case IDEActions.RefreshProject:
424                     refreshWorkspace();
425                     //setBackgroundOperation(new BackgroundOperationWatcherTest(this));
426                     return true;
427                 case IDEActions.WindowCloseAllDocuments:
428                     askForUnsavedEdits(delegate() {
429                         closeAllDocuments();
430                     });
431                     return true;
432                 case IDEActions.FileOpenWorkspace:
433                     UIString caption;
434                     caption = "Open Workspace or Project"d;
435                     FileDialog dlg = new FileDialog(caption, window, null);
436                     dlg.addFilter(FileFilterEntry(UIString("Workspace and project files"d), "*.dlangidews;dub.json;package.json"));
437                     dlg.onDialogResult = delegate(Dialog dlg, const Action result) {
438 						if (result.id == ACTION_OPEN.id) {
439                             string filename = result.stringParam;
440                             if (filename.length)
441                                 openFileOrWorkspace(filename);
442                         }
443                     };
444                     dlg.show();
445                     return true;
446                 default:
447                     return super.handleAction(a);
448             }
449         }
450 		return false;
451 	}
452 
453     void openFileOrWorkspace(string filename) {
454         if (filename.isWorkspaceFile) {
455             Workspace ws = new Workspace();
456             if (ws.load(filename)) {
457                 askForUnsavedEdits(delegate() {
458                     setWorkspace(ws);
459                 });
460             } else {
461                 window.showMessageBox(UIString("Cannot open workspace"d), UIString("Error occured while opening workspace"d));
462                 return;
463             }
464         } else if (filename.isProjectFile) {
465             _logPanel.clear();
466             _logPanel.logLine("Trying to open project from " ~ filename);
467             Project project = new Project();
468             if (!project.load(filename)) {
469                 _logPanel.logLine("Cannot read project file " ~ filename);
470                 window.showMessageBox(UIString("Cannot open project"d), UIString("Error occured while opening project"d));
471                 return;
472             }
473             _logPanel.logLine("Project file is opened ok");
474             string defWsFile = project.defWorkspaceFile;
475             if (currentWorkspace) {
476                 Project existing = currentWorkspace.findProject(project.filename);
477                 if (existing) {
478                     _logPanel.logLine("This project already exists in current workspace");
479                     window.showMessageBox(UIString("Open project"d), UIString("Project is already in workspace"d));
480                     return;
481                 }
482                 window.showMessageBox(UIString("Open project"d), UIString("Do you want to create new workspace or use current one?"d),
483                                       [ACTION_ADD_TO_CURRENT_WORKSPACE, ACTION_CREATE_NEW_WORKSPACE, ACTION_CANCEL], 0, delegate(const Action result) {
484                                           if (result.id == IDEActions.CreateNewWorkspace) {
485                                               // new ws
486                                               createNewWorkspaceForExistingProject(project);
487                                           } else if (result.id == IDEActions.AddToCurrentWorkspace) {
488                                               // add to current
489                                               currentWorkspace.addProject(project);
490                                               currentWorkspace.save();
491                                               refreshWorkspace();
492                                           }
493                                           return true;
494                                       });
495             } else {
496                 // new workspace file
497                 createNewWorkspaceForExistingProject(project);
498             }
499         } else {
500             _logPanel.logLine("File is not recognized as DlangIDE project or workspace file");
501             window.showMessageBox(UIString("Invalid workspace file"d), UIString("This file is not a valid workspace or project file"d));
502         }
503     }
504 
505     void refreshWorkspace() {
506         _logPanel.logLine("Refreshing workspace");
507         _wsPanel.reloadItems();
508     }
509 
510     void createNewWorkspaceForExistingProject(Project project) {
511         string defWsFile = project.defWorkspaceFile;
512         _logPanel.logLine("Creating new workspace " ~ defWsFile);
513         // new ws
514         Workspace ws = new Workspace();
515         ws.name = project.name;
516         ws.description = project.description;
517         ws.addProject(project);
518         ws.save(defWsFile);
519         setWorkspace(ws);
520         _logPanel.logLine("Done");
521     }
522 
523     //bool loadWorkspace(string path) {
524     //    // testing workspace loader
525     //    Workspace ws = new Workspace();
526     //    ws.load(path);
527     //    setWorkspace(ws);
528     //    //ws.save(ws.filename ~ ".bak");
529     //    return true;
530     //}
531 
532     void setWorkspace(Workspace ws) {
533         closeAllDocuments();
534         currentWorkspace = ws;
535         _wsPanel.workspace = ws;
536         if (ws.startupProject && ws.startupProject.mainSourceFile) {
537             openSourceFile(ws.startupProject.mainSourceFile.filename);
538             _tabs.setFocus();
539         }
540     }
541 
542     void buildProject(BuildOperation buildOp) {
543         if (!currentWorkspace || !currentWorkspace.startupProject) {
544             _logPanel.logLine("No project is opened");
545             return;
546         }
547         Builder op = new Builder(this, currentWorkspace.startupProject, _logPanel, currentWorkspace.buildConfiguration, buildOp, false);
548         setBackgroundOperation(op);
549     }
550 }
551 
552 Widget createAboutWidget() 
553 {
554 	LinearLayout res = new VerticalLayout();
555 	res.padding(Rect(10,10,10,10));
556 	res.addChild(new TextWidget(null, "DLangIDE"d));
557 	res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d));
558 	res.addChild(new TextWidget(null, "http://github.com/buggins/dlangide"d));
559 	res.addChild(new TextWidget(null, "So far, it's just a test for DLangUI library."d));
560 	res.addChild(new TextWidget(null, "Later I hope to make working IDE :)"d));
561 	Button closeButton = new Button("close", "Close"d);
562 	closeButton.onClickListener = delegate(Widget src) {
563 		Log.i("Closing window");
564 		res.window.close();
565 		return true;
566 	};
567 	res.addChild(closeButton);
568 	return res;
569 }