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 }