1 module dlangide.ui.wspanel; 2 3 import std..string; 4 import dlangui; 5 import dlangide.workspace.workspace; 6 import dlangide.workspace.project; 7 import dlangide.ui.commands; 8 9 enum ProjectItemType : int { 10 None, 11 SourceFile, 12 SourceFolder, 13 Project, 14 Workspace 15 } 16 17 interface SourceFileSelectionHandler { 18 bool onSourceFileSelected(ProjectSourceFile file, bool activate); 19 } 20 21 interface WorkspaceActionHandler { 22 bool onWorkspaceAction(const Action a); 23 } 24 25 class WorkspacePanel : DockWindow { 26 protected Workspace _workspace; 27 protected TreeWidget _tree; 28 29 /// handle source file selection change 30 Signal!SourceFileSelectionHandler sourceFileSelectionListener; 31 Signal!WorkspaceActionHandler workspaceActionListener; 32 33 this(string id) { 34 super(id); 35 workspace = null; 36 //layoutWidth = 200; 37 _caption.text = "Workspace Explorer"d; 38 acceleratorMap.add([ACTION_PROJECT_FOLDER_EXPAND_ALL, ACTION_PROJECT_FOLDER_COLLAPSE_ALL, 39 ACTION_PROJECT_FOLDER_REFRESH, ACTION_PROJECT_FOLDER_RENAME_ITEM, 40 ACTION_PROJECT_FOLDER_REMOVE_ITEM, ACTION_PROJECT_FOLDER_OPEN_ITEM]); 41 } 42 43 bool selectItem(ProjectItem projectItem) { 44 if (projectItem) { 45 TreeItem item = _tree.findItemById(projectItem.filename); 46 if (item) { 47 if (item.parent && !item.parent.isFullyExpanded) 48 _tree.items.toggleExpand(item.parent); 49 _tree.makeItemVisible(item); 50 _tree.selectItem(item); 51 return true; 52 } 53 } else { 54 _tree.clearSelection(); 55 return true; 56 } 57 return false; 58 } 59 60 void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated) { 61 if (!selectedItem) 62 return; 63 if (_workspace) { 64 // save selected item id 65 ProjectItem item = cast(ProjectItem)selectedItem.objectParam; 66 if (item) { 67 string id = item.filename; 68 if (id) 69 _workspace.selectedWorkspaceItem = id; 70 } 71 } 72 if (selectedItem.intParam == ProjectItemType.SourceFile) { 73 // file selected 74 if (sourceFileSelectionListener.assigned) { 75 ProjectSourceFile sourceFile = cast(ProjectSourceFile)selectedItem.objectParam; 76 if (sourceFile) { 77 sourceFileSelectionListener(sourceFile, activated); 78 } 79 } 80 } else if (selectedItem.intParam == ProjectItemType.SourceFolder) { 81 // folder selected 82 } 83 } 84 85 override protected Widget createBodyWidget() { 86 _tree = new TreeWidget("wstree", ScrollBarMode.Auto, ScrollBarMode.Auto); 87 _tree.layoutHeight(FILL_PARENT).layoutHeight(FILL_PARENT); 88 _tree.selectionChange = &onTreeItemSelected; 89 _tree.expandedChange.connect(&onTreeExpandedStateChange); 90 _tree.fontSize = 16; 91 _tree.noCollapseForSingleTopLevelItem = true; 92 _tree.popupMenu = &onTreeItemPopupMenu; 93 94 _workspacePopupMenu = new MenuItem(); 95 _workspacePopupMenu.add(ACTION_FILE_NEW_PROJECT, 96 ACTION_PROJECT_FOLDER_REFRESH, 97 ACTION_FILE_WORKSPACE_CLOSE, 98 ACTION_PROJECT_FOLDER_EXPAND_ALL, 99 ACTION_PROJECT_FOLDER_COLLAPSE_ALL 100 ); 101 102 _projectPopupMenu = new MenuItem(); 103 _projectPopupMenu.add(ACTION_PROJECT_SET_STARTUP, 104 ACTION_PROJECT_FOLDER_REFRESH, 105 ACTION_FILE_NEW_SOURCE_FILE, 106 //ACTION_PROJECT_FOLDER_OPEN_ITEM, 107 ACTION_PROJECT_BUILD, 108 ACTION_PROJECT_REBUILD, 109 ACTION_PROJECT_CLEAN, 110 ACTION_PROJECT_UPDATE_DEPENDENCIES, 111 ACTION_PROJECT_REVEAL_IN_EXPLORER, 112 ACTION_PROJECT_SETTINGS, 113 ACTION_PROJECT_FOLDER_EXPAND_ALL, 114 ACTION_PROJECT_FOLDER_COLLAPSE_ALL 115 //ACTION_PROJECT_FOLDER_REMOVE_ITEM 116 ); 117 118 _folderPopupMenu = new MenuItem(); 119 _folderPopupMenu.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_PROJECT_FOLDER_REFRESH, ACTION_PROJECT_FOLDER_OPEN_ITEM, 120 ACTION_PROJECT_FOLDER_EXPAND_ALL, ACTION_PROJECT_FOLDER_COLLAPSE_ALL 121 //ACTION_PROJECT_FOLDER_REMOVE_ITEM, 122 //ACTION_PROJECT_FOLDER_RENAME_ITEM 123 ); 124 125 _filePopupMenu = new MenuItem(); 126 _filePopupMenu.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_PROJECT_FOLDER_REFRESH, 127 ACTION_PROJECT_FOLDER_OPEN_ITEM, 128 ACTION_PROJECT_FOLDER_REMOVE_ITEM, 129 //ACTION_PROJECT_FOLDER_RENAME_ITEM 130 ); 131 return _tree; 132 } 133 134 protected MenuItem _workspacePopupMenu; 135 protected MenuItem _projectPopupMenu; 136 protected MenuItem _folderPopupMenu; 137 protected MenuItem _filePopupMenu; 138 protected string _popupMenuSelectedItemId; 139 protected TreeItem _popupMenuSelectedItem; 140 protected void onPopupMenuItem(MenuItem item) { 141 if (item.action) 142 handleAction(item.action); 143 } 144 145 protected MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem) { 146 MenuItem menu = null; 147 _popupMenuSelectedItemId = selectedItem.id; 148 _popupMenuSelectedItem = selectedItem; 149 if (selectedItem.intParam == ProjectItemType.SourceFolder) { 150 menu = _folderPopupMenu; 151 } else if (selectedItem.intParam == ProjectItemType.SourceFile) { 152 menu = _filePopupMenu; 153 } else if (selectedItem.intParam == ProjectItemType.Project) { 154 menu = _projectPopupMenu; 155 } else if (selectedItem.intParam == ProjectItemType.Workspace) { 156 menu = _workspacePopupMenu; 157 } 158 if (menu && menu.subitemCount) { 159 for (int i = 0; i < menu.subitemCount; i++) { 160 Action a = menu.subitem(i).action.clone(); 161 a.objectParam = selectedItem.objectParam; 162 menu.subitem(i).action = a; 163 //menu.subitem(i).menuItemAction = &handleAction; 164 } 165 //menu.onMenuItem = &onPopupMenuItem; 166 //menu.menuItemClick = &onPopupMenuItem; 167 menu.menuItemAction = &handleAction; 168 menu.updateActionState(this); 169 return menu; 170 } 171 return null; 172 } 173 174 @property Workspace workspace() { 175 return _workspace; 176 } 177 178 /// returns currently selected project item 179 @property ProjectItem selectedProjectItem() { 180 TreeItem ti = _tree.items.selectedItem; 181 if (!ti) 182 return null; 183 Object obj = ti.objectParam; 184 if (!obj) 185 return null; 186 return cast(ProjectItem)obj; 187 } 188 189 ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true, dstring projectName=null) { 190 if (_workspace) 191 return _workspace.findSourceFileItem(filename, fullFileName, projectName); 192 return null; 193 } 194 195 /// Adding elements to the tree 196 void addProjectItems(TreeItem root, ProjectItem items) { 197 for (int i = 0; i < items.childCount; i++) { 198 ProjectItem child = items.child(i); 199 if (child.isFolder) { 200 TreeItem p = root.newChild(child.filename, child.name, "folder"); 201 p.intParam = ProjectItemType.SourceFolder; 202 p.objectParam = child; 203 if (restoreItemState(child.filename)) 204 p.expand(); 205 else 206 p.collapse(); 207 addProjectItems(p, child); 208 } else { 209 string icon = "text-other"; 210 if (child.isDSourceFile) 211 icon = "text-d"; 212 if (child.isJsonFile) 213 icon = "text-json"; 214 if (child.isDMLFile) 215 icon = "text-dml"; 216 TreeItem p = root.newChild(child.filename, child.name, icon); 217 p.intParam = ProjectItemType.SourceFile; 218 p.objectParam = child; 219 } 220 } 221 } 222 223 void updateDefault() { 224 TreeItem defaultItem = null; 225 if (_workspace && _tree.items.childCount && _workspace.startupProject) { 226 for (int i = 0; i < _tree.items.child(0).childCount; i++) { 227 TreeItem p = _tree.items.child(0).child(i); 228 if (p.objectParam is _workspace.startupProject) 229 defaultItem = p; 230 } 231 } 232 _tree.items.setDefaultItem(defaultItem); 233 } 234 235 /// map key to action 236 override Action findKeyAction(uint keyCode, uint flags) { 237 Action action = _acceleratorMap.findByKey(keyCode, flags); 238 if (action) { 239 if (TreeItem ti = _tree.items.selectedItem) { 240 _popupMenuSelectedItem = ti; 241 action.objectParam = ti.objectParam; 242 } else { 243 return null; 244 } 245 return action; 246 } 247 return super.findKeyAction(keyCode, flags); 248 } 249 250 void expandAll(const Action a) { 251 if (!_workspace) 252 return; 253 if (_popupMenuSelectedItem) 254 _popupMenuSelectedItem.expandAll(); 255 } 256 257 void collapseAll(const Action a) { 258 if (!_workspace) 259 return; 260 if (_popupMenuSelectedItem) 261 _popupMenuSelectedItem.collapseAll(); 262 } 263 264 protected bool[string] _itemStates; 265 protected bool _itemStatesDirty; 266 protected void readExpandedStateFromWorkspace() { 267 _itemStates.clear(); 268 if (_workspace) { 269 string[] items = _workspace.expandedItems; 270 foreach(item; items) 271 _itemStates[item] = true; 272 } 273 } 274 275 /// Saving items collapse/expand state 276 protected void saveItemState(string itemPath, bool expanded) { 277 bool changed = restoreItemState(itemPath) != expanded; 278 if (!_itemStatesDirty && changed) 279 _itemStatesDirty = true; 280 if (changed) { 281 if (expanded) { 282 _itemStates[itemPath] = true; 283 } else { 284 _itemStates.remove(itemPath); 285 } 286 string[] items; 287 items.assumeSafeAppend; 288 foreach(k,v; _itemStates) { 289 items ~= k; 290 } 291 _workspace.expandedItems = items; 292 debug Log.d("stored Expanded state ", expanded, " for ", itemPath); 293 } 294 } 295 /// Is need to expand item? 296 protected bool restoreItemState(string itemPath) { 297 if (auto p = itemPath in _itemStates) { 298 // Item itself must expand, but upper items may be collapsed 299 return *p; 300 /* 301 auto path = itemPath; 302 while (path.length > 0 && !path.endsWith("src") && path in _itemStates) { 303 auto pos = lastIndexOf(path, '/'); 304 path = pos > -1 ? path[ 0 .. pos ] : ""; 305 } 306 if (path.length == 0 || path.endsWith("src")) { 307 debug Log.d("restored Expanded state for ", itemPath); 308 return *p; 309 } 310 */ 311 } 312 return false; 313 } 314 315 void onTreeExpandedStateChange(TreeItems source, TreeItem item) { 316 bool expanded = item.expanded; 317 ProjectItem prjItem = cast(ProjectItem)item.objectParam; 318 if (prjItem) { 319 string fn = prjItem.filename; 320 debug Log.d("onTreeExpandedStateChange expanded=", expanded, " fn=", fn); 321 saveItemState(fn, expanded); 322 } 323 } 324 325 void reloadItems() { 326 _tree.expandedChange.disconnect(&onTreeExpandedStateChange); 327 _tree.selectionChange.disconnect(&onTreeItemSelected); 328 _tree.clearAllItems(); 329 330 if (_workspace) { 331 TreeItem defaultItem = null; 332 TreeItem root = _tree.items.newChild(_workspace.filename, _workspace.name, "project-development"); 333 root.intParam = ProjectItemType.Workspace; 334 foreach(project; _workspace.projects) { 335 TreeItem p = root.newChild(project.filename, project.name, project.isDependency ? "project-d-dependency" : "project-d"); 336 p.intParam = ProjectItemType.Project; 337 p.objectParam = project; 338 if (restoreItemState(project.filename)) 339 p.expand(); 340 else 341 p.collapse(); 342 if (project && _workspace.startupProject is project) 343 defaultItem = p; 344 addProjectItems(p, project.items); 345 } 346 _tree.items.setDefaultItem(defaultItem); 347 } else { 348 _tree.items.newChild("none", "No workspace"d, "project-development"); 349 } 350 _tree.expandedChange.connect(&onTreeExpandedStateChange); 351 _tree.selectionChange.connect(&onTreeItemSelected); 352 353 // expand default project if no information about expanded items 354 if (!_itemStates.length) { 355 if (_workspace && _workspace.startupProject) { 356 string fn = _workspace.startupProject.filename; 357 TreeItem startupProjectItem = _tree.items.findItemById(fn); 358 if (startupProjectItem) { 359 startupProjectItem.expand(); 360 saveItemState(fn, true); 361 } 362 } 363 } 364 if (_workspace) { 365 // restore selection 366 string id = _workspace.selectedWorkspaceItem; 367 _tree.selectItem(id); 368 } 369 370 updateDefault(); 371 } 372 373 @property void workspace(Workspace w) { 374 _workspace = w; 375 readExpandedStateFromWorkspace(); 376 reloadItems(); 377 } 378 379 /// override to handle specific actions 380 override bool handleAction(const Action a) { 381 if (workspaceActionListener.assigned) 382 return workspaceActionListener(a); 383 return false; 384 } 385 386 override protected bool onCloseButtonClick(Widget source) { 387 hide(); 388 return true; 389 } 390 391 /// hide workspace panel 392 void hide() { 393 visibility = Visibility.Gone; 394 parent.layout(parent.pos); 395 } 396 397 // activate workspace panel if hidden 398 void activate() { 399 if (visibility == Visibility.Gone) { 400 visibility = Visibility.Visible; 401 parent.layout(parent.pos); 402 } 403 setFocus(); 404 } 405 }