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 
25 import std.conv;
26 import std.utf;
27 import std.algorithm;
28 import std.path;
29 
30 bool isSupportedSourceTextFileFormat(string filename) {
31     return (filename.endsWith(".d") || filename.endsWith(".txt") || filename.endsWith(".cpp") || filename.endsWith(".h") || filename.endsWith(".c")
32         || filename.endsWith(".json") || filename.endsWith(".dd") || filename.endsWith(".ddoc") || filename.endsWith(".xml") || filename.endsWith(".html")
33         || filename.endsWith(".html") || filename.endsWith(".css") || filename.endsWith(".log") || filename.endsWith(".hpp"));
34 }
35 
36 /// DIDE app frame
37 class IDEFrame : AppFrame {
38 
39     MenuItem mainMenuItems;
40     WorkspacePanel _wsPanel;
41     OutputPanel _logPanel;
42     DockHost _dockHost;
43     TabWidget _tabs;
44 
45     dstring frameWindowCaptionSuffix = "DLangIDE"d;
46 
47     this(Window window) {
48         super();
49         window.mainWidget = this;
50     }
51 
52     override protected void init() {
53         super.init();
54     }
55 
56     /// move focus to editor in currently selected tab
57     void focusEditor(string id) {
58         Widget w = _tabs.tabBody(id);
59         if (w) {
60             if (w.visible)
61                 w.setFocus();
62         }
63     }
64 
65     /// source file selected in workspace tree
66     bool onSourceFileSelected(ProjectSourceFile file, bool activate) {
67         Log.d("onSourceFileSelected ", file.filename);
68         return openSourceFile(file.filename, file, activate);
69     }
70 
71     void onModifiedStateChange(Widget source, bool modified) {
72         //
73         Log.d("onModifiedStateChange ", source.id, " modified=", modified);
74         int index = _tabs.tabIndex(source.id);
75         if (index >= 0) {
76             dstring name = toUTF32((modified ? "* " : "") ~ baseName(source.id));
77             _tabs.renameTab(index, name);
78         }
79     }
80 
81     bool openSourceFile(string filename, ProjectSourceFile file = null, bool activate = true) {
82         if (!file)
83             file = _wsPanel.findSourceFileItem(filename);
84         Log.d("openSourceFile ", filename);
85         int index = _tabs.tabIndex(filename);
86         if (index >= 0) {
87             // file is already opened in tab
88             _tabs.selectTab(index, true);
89         } else {
90             // open new file
91             DSourceEdit editor = new DSourceEdit(filename);
92             if (file ? editor.load(file) : editor.load(filename)) {
93                 _tabs.addTab(editor, toUTF32(baseName(filename)));
94                 index = _tabs.tabIndex(filename);
95                 TabItem tab = _tabs.tab(filename);
96                 tab.objectParam = file;
97                 editor.onModifiedStateChangeListener = &onModifiedStateChange;
98                 _tabs.selectTab(index, true);
99             } else {
100                 destroy(editor);
101                 if (window)
102                     window.showMessageBox(UIString("File open error"d), UIString("Failed to open file "d ~ toUTF32(file.filename)));
103                 return false;
104             }
105         }
106         if (activate) {
107             focusEditor(filename);
108         }
109         requestLayout();
110         return true;
111     }
112 
113     static immutable HOME_SCREEN_ID = "HOME_SCREEN";
114     void showHomeScreen() {
115         int index = _tabs.tabIndex(HOME_SCREEN_ID);
116         if (index >= 0) {
117             _tabs.selectTab(index, true);
118         } else {
119             HomeScreen home = new HomeScreen(HOME_SCREEN_ID, this);
120             _tabs.addTab(home, "Home"d);
121             _tabs.selectTab(HOME_SCREEN_ID, true);
122         }
123     }
124 
125     void onTabChanged(string newActiveTabId, string previousTabId) {
126         int index = _tabs.tabIndex(newActiveTabId);
127         if (index >= 0) {
128             TabItem tab = _tabs.tab(index);
129             ProjectSourceFile file = cast(ProjectSourceFile)tab.objectParam;
130             if (file) {
131                 // tab is source file editor
132                 _wsPanel.selectItem(file);
133                 focusEditor(file.filename);
134             }
135             window.windowCaption(tab.text.value ~ " - "d ~ frameWindowCaptionSuffix);
136         }
137     }
138 
139     /// close tab w/o confirmation
140     void closeTab(string tabId) {
141         _wsPanel.selectItem(null);
142         _tabs.removeTab(tabId);
143     }
144 
145     /// close all editor tabs
146     void closeAllDocuments() {
147         for (int i = _tabs.tabCount - 1; i >= 0; i--) {
148             DSourceEdit ed = cast(DSourceEdit)_tabs.tabBody(i);
149             if (ed) {
150                 closeTab(ed.id);
151             }
152         }
153     }
154 
155     /// returns first unsaved document
156     protected DSourceEdit hasUnsavedEdits() {
157         for (int i = _tabs.tabCount - 1; i >= 0; i--) {
158             DSourceEdit ed = cast(DSourceEdit)_tabs.tabBody(i);
159             if (ed && ed.content.modified) {
160                 return ed;
161             }
162         }
163         return null;
164     }
165 
166     protected void askForUnsavedEdits(void delegate() onConfirm) {
167         DSourceEdit ed = hasUnsavedEdits();
168         if (!ed) {
169             // no unsaved edits
170             onConfirm();
171             return;
172         }
173         string tabId = ed.id;
174         // tab content is modified - ask for confirmation
175         window.showMessageBox(UIString("Close file "d ~ toUTF32(baseName(tabId))), UIString("Content of this file has been changed."d), 
176                               [ACTION_SAVE, ACTION_SAVE_ALL, ACTION_DISCARD_CHANGES, ACTION_DISCARD_ALL, ACTION_CANCEL], 
177                               0, delegate(const Action result) {
178                                   if (result == StandardAction.Save) {
179                                       // save and close
180                                       ed.save();
181                                       askForUnsavedEdits(onConfirm);
182                                   } else if (result == StandardAction.DiscardChanges) {
183                                       // close, don't save
184                                       closeTab(tabId);
185                                       closeAllDocuments();
186                                       onConfirm();
187                                   } else if (result == StandardAction.SaveAll) {
188                                       ed.save();
189                                       for(;;) {
190                                           DSourceEdit editor = hasUnsavedEdits();
191                                           if (!editor)
192                                               break;
193                                           editor.save();
194                                       }
195                                       closeAllDocuments();
196                                       onConfirm();
197                                   } else if (result == StandardAction.DiscardAll) {
198                                       // close, don't save
199                                       closeAllDocuments();
200                                       onConfirm();
201                                   }
202                                   // else ignore
203                                   return true;
204                               });
205     }
206 
207     protected void onTabClose(string tabId) {
208         Log.d("onTabClose ", tabId);
209         int index = _tabs.tabIndex(tabId);
210         if (index >= 0) {
211             DSourceEdit d = cast(DSourceEdit)_tabs.tabBody(tabId);
212             if (d && d.content.modified) {
213                 // tab content is modified - ask for confirmation
214                 window.showMessageBox(UIString("Close tab"d), UIString("Content of "d ~ toUTF32(baseName(tabId)) ~ " file has been changed."d), 
215                                       [ACTION_SAVE, ACTION_DISCARD_CHANGES, ACTION_CANCEL], 
216                                       0, delegate(const Action result) {
217                                           if (result == StandardAction.Save) {
218                                               // save and close
219                                               d.save();
220                                               closeTab(tabId);
221                                           } else if (result == StandardAction.DiscardChanges) {
222                                               // close, don't save
223                                               closeTab(tabId);
224                                           }
225                                           // else ignore
226                                           return true;
227                                       });
228             } else {
229                 closeTab(tabId);
230             }
231         }
232     }
233 
234     /// create app body widget
235     override protected Widget createBody() {
236         _dockHost = new DockHost();
237 
238         //=============================================================
239         // Create body - Tabs
240 
241         // editor tabs
242         _tabs = new TabWidget("TABS");
243         _tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT);
244         _tabs.onTabChangedListener = &onTabChanged;
245         _tabs.onTabCloseListener = &onTabClose;
246 
247         _dockHost.bodyWidget = _tabs;
248 
249         //=============================================================
250         // Create workspace docked panel
251         _wsPanel = new WorkspacePanel("workspace");
252         _wsPanel.sourceFileSelectionListener = &onSourceFileSelected;
253         _dockHost.addDockedWindow(_wsPanel);
254 
255         _logPanel = new OutputPanel("output");
256         _logPanel.addLogLines(null, "Line 1"d);
257         _logPanel.addLogLines(null, "Line 2"d);
258         _logPanel.addLogLines(null, "Line 3"d, "Line 4"d);
259 
260         _dockHost.addDockedWindow(_logPanel);
261 
262         return _dockHost;
263     }
264 
265     /// create main menu
266     override protected MainMenu createMainMenu() {
267 
268         mainMenuItems = new MenuItem();
269         MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE"));
270         MenuItem fileNewItem = new MenuItem(new Action(1, "MENU_FILE_NEW"));
271         fileNewItem.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_FILE_NEW_WORKSPACE, ACTION_FILE_NEW_PROJECT);
272         fileItem.add(fileNewItem);
273         fileItem.add(ACTION_FILE_OPEN_WORKSPACE, ACTION_FILE_OPEN, 
274                      ACTION_FILE_SAVE, ACTION_FILE_SAVE_AS, ACTION_FILE_SAVE_ALL, ACTION_FILE_EXIT);
275 
276         MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT"));
277 		editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, 
278                      ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO);
279 
280 		editItem.add(new Action(20, "MENU_EDIT_PREFERENCES"));
281 
282         MenuItem projectItem = new MenuItem(new Action(21, "MENU_PROJECT"));
283         projectItem.add(ACTION_PROJECT_SET_STARTUP, ACTION_PROJECT_SETTINGS);
284 
285         MenuItem buildItem = new MenuItem(new Action(22, "MENU_BUILD"));
286         buildItem.add(ACTION_WORKSPACE_BUILD, ACTION_WORKSPACE_REBUILD, ACTION_WORKSPACE_CLEAN,
287                      ACTION_PROJECT_BUILD, ACTION_PROJECT_REBUILD, ACTION_PROJECT_CLEAN);
288 
289         MenuItem debugItem = new MenuItem(new Action(23, "MENU_DEBUG"));
290         debugItem.add(ACTION_DEBUG_START, ACTION_DEBUG_START_NO_DEBUG, 
291                       ACTION_DEBUG_CONTINUE, ACTION_DEBUG_STOP, ACTION_DEBUG_PAUSE);
292 
293 
294 		MenuItem windowItem = new MenuItem(new Action(3, "MENU_WINDOW"c));
295         windowItem.add(new Action(30, "MENU_WINDOW_PREFERENCES"));
296         windowItem.add(ACTION_WINDOW_CLOSE_ALL_DOCUMENTS);
297         MenuItem helpItem = new MenuItem(new Action(4, "MENU_HELP"c));
298         helpItem.add(new Action(40, "MENU_HELP_VIEW_HELP"));
299         helpItem.add(ACTION_HELP_ABOUT);
300         mainMenuItems.add(fileItem);
301         mainMenuItems.add(editItem);
302         mainMenuItems.add(projectItem);
303         mainMenuItems.add(buildItem);
304         mainMenuItems.add(debugItem);
305 		//mainMenuItems.add(viewItem);
306 		mainMenuItems.add(windowItem);
307         mainMenuItems.add(helpItem);
308 
309         MainMenu mainMenu = new MainMenu(mainMenuItems);
310         mainMenu.backgroundColor = 0xd6dbe9;
311         return mainMenu;
312     }
313 	
314     /// create app toolbars
315     override protected ToolBarHost createToolbars() {
316         ToolBarHost res = new ToolBarHost();
317         ToolBar tb;
318         tb = res.getOrAddToolbar("Standard");
319         tb.addButtons(ACTION_FILE_OPEN, ACTION_FILE_SAVE, ACTION_SEPARATOR);
320 
321         tb.addButtons(ACTION_DEBUG_START);
322         ToolBarComboBox cbBuildConfiguration = new ToolBarComboBox("buildConfig", ["Debug"d, "Release"d, "Unittest"d]);
323         tb.addControl(cbBuildConfiguration);
324 
325         tb = res.getOrAddToolbar("Edit");
326         tb.addButtons(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_SEPARATOR,
327                       ACTION_EDIT_UNDO, ACTION_EDIT_REDO);
328         return res;
329     }
330 
331     /// override to handle specific actions
332 	override bool handleAction(const Action a) {
333         if (a) {
334             switch (a.id) {
335                 case IDEActions.FileExit:
336                     window.close();
337                     return true;
338                 case IDEActions.HelpAbout:
339                     Window wnd = Platform.instance.createWindow("About...", window, WindowFlag.Modal);
340                     wnd.mainWidget = createAboutWidget();
341                     wnd.show();
342                     return true;
343                 case StandardAction.OpenUrl:
344                     platform.openURL(a.stringParam);
345                     return true;
346                 case IDEActions.FileOpen:
347                     UIString caption;
348                     caption = "Open Text File"d;
349                     FileDialog dlg = new FileDialog(caption, window, null);
350                     dlg.addFilter(FileFilterEntry(UIString("Source files"d), "*.d;*.dd;*.ddoc;*.dh;*.json;*.xml;*.ini"));
351                     dlg.onDialogResult = delegate(Dialog dlg, const Action result) {
352 						if (result.id == ACTION_OPEN.id) {
353                             string filename = result.stringParam;
354                             if (isSupportedSourceTextFileFormat(filename)) {
355                                 openSourceFile(filename);
356                             }
357                         }
358                     };
359                     dlg.show();
360                     return true;
361                 case IDEActions.WindowCloseAllDocuments:
362                     askForUnsavedEdits(delegate() {
363                         closeAllDocuments();
364                     });
365                     return true;
366                 case IDEActions.FileOpenWorkspace:
367                     UIString caption;
368                     caption = "Open Workspace or Project"d;
369                     FileDialog dlg = new FileDialog(caption, window, null);
370                     dlg.addFilter(FileFilterEntry(UIString("Workspace and project files"d), "*.dlangidews;dub.json"));
371                     dlg.onDialogResult = delegate(Dialog dlg, const Action result) {
372 						if (result.id == ACTION_OPEN.id) {
373                             string filename = result.stringParam;
374                         }
375                     };
376                     dlg.show();
377                     return true;
378                 default:
379                     return super.handleAction(a);
380             }
381         }
382 		return false;
383 	}
384 
385     bool loadWorkspace(string path) {
386         // testing workspace loader
387         Workspace ws = new Workspace();
388         ws.load(path);
389         currentWorkspace = ws;
390         _wsPanel.workspace = ws;
391         return true;
392     }
393 }
394 
395 Widget createAboutWidget() 
396 {
397 	LinearLayout res = new VerticalLayout();
398 	res.padding(Rect(10,10,10,10));
399 	res.addChild(new TextWidget(null, "DLangIDE"d));
400 	res.addChild(new TextWidget(null, "(C) Vadim Lopatin, 2014"d));
401 	res.addChild(new TextWidget(null, "http://github.com/buggins/dlangide"d));
402 	res.addChild(new TextWidget(null, "So far, it's just a test for DLangUI library."d));
403 	res.addChild(new TextWidget(null, "Later I hope to make working IDE :)"d));
404 	Button closeButton = new Button("close", "Close"d);
405 	closeButton.onClickListener = delegate(Widget src) {
406 		Log.i("Closing window");
407 		res.window.close();
408 		return true;
409 	};
410 	res.addChild(closeButton);
411 	return res;
412 }