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 }