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_DIRECTORY, 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, 120 ACTION_FILE_NEW_DIRECTORY, 121 ACTION_PROJECT_FOLDER_REFRESH, ACTION_PROJECT_FOLDER_OPEN_ITEM, 122 ACTION_PROJECT_FOLDER_EXPAND_ALL, ACTION_PROJECT_FOLDER_COLLAPSE_ALL 123 //ACTION_PROJECT_FOLDER_REMOVE_ITEM, 124 //ACTION_PROJECT_FOLDER_RENAME_ITEM 125 ); 126 127 _filePopupMenu = new MenuItem(); 128 _filePopupMenu.add(ACTION_FILE_NEW_SOURCE_FILE, 129 ACTION_PROJECT_FOLDER_REFRESH, 130 ACTION_PROJECT_FOLDER_OPEN_ITEM, 131 ACTION_PROJECT_FOLDER_REMOVE_ITEM, 132 //ACTION_PROJECT_FOLDER_RENAME_ITEM 133 ); 134 return _tree; 135 } 136 137 protected MenuItem _workspacePopupMenu; 138 protected MenuItem _projectPopupMenu; 139 protected MenuItem _folderPopupMenu; 140 protected MenuItem _filePopupMenu; 141 protected string _popupMenuSelectedItemId; 142 protected TreeItem _popupMenuSelectedItem; 143 protected void onPopupMenuItem(MenuItem item) { 144 if (item.action) 145 handleAction(item.action); 146 } 147 148 protected MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem) { 149 MenuItem menu = null; 150 _popupMenuSelectedItemId = selectedItem.id; 151 _popupMenuSelectedItem = selectedItem; 152 if (selectedItem.intParam == ProjectItemType.SourceFolder) { 153 menu = _folderPopupMenu; 154 } else if (selectedItem.intParam == ProjectItemType.SourceFile) { 155 menu = _filePopupMenu; 156 } else if (selectedItem.intParam == ProjectItemType.Project) { 157 menu = _projectPopupMenu; 158 } else if (selectedItem.intParam == ProjectItemType.Workspace) { 159 menu = _workspacePopupMenu; 160 } 161 if (menu && menu.subitemCount) { 162 for (int i = 0; i < menu.subitemCount; i++) { 163 Action a = menu.subitem(i).action.clone(); 164 a.objectParam = selectedItem.objectParam; 165 menu.subitem(i).action = a; 166 //menu.subitem(i).menuItemAction = &handleAction; 167 } 168 //menu.onMenuItem = &onPopupMenuItem; 169 //menu.menuItemClick = &onPopupMenuItem; 170 menu.menuItemAction = &handleAction; 171 menu.updateActionState(this); 172 return menu; 173 } 174 return null; 175 } 176 177 @property Workspace workspace() { 178 return _workspace; 179 } 180 181 /// returns currently selected project item 182 @property ProjectItem selectedProjectItem() { 183 TreeItem ti = _tree.items.selectedItem; 184 if (!ti) 185 return null; 186 Object obj = ti.objectParam; 187 if (!obj) 188 return null; 189 return cast(ProjectItem)obj; 190 } 191 192 ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true, dstring projectName=null) { 193 if (_workspace) 194 return _workspace.findSourceFileItem(filename, fullFileName, projectName); 195 return null; 196 } 197 198 /// Adding elements to the tree 199 void addProjectItems(TreeItem root, ProjectItem items) { 200 for (int i = 0; i < items.childCount; i++) { 201 ProjectItem child = items.child(i); 202 if (child.isFolder) { 203 TreeItem p = root.newChild(child.filename, child.name, "folder"); 204 p.intParam = ProjectItemType.SourceFolder; 205 p.objectParam = child; 206 if (restoreItemState(child.filename)) 207 p.expand(); 208 else 209 p.collapse(); 210 addProjectItems(p, child); 211 } else { 212 string icon = "text-other"; 213 if (child.isDSourceFile) 214 icon = "text-d"; 215 if (child.isJsonFile) 216 icon = "text-json"; 217 if (child.isDMLFile) 218 icon = "text-dml"; 219 TreeItem p = root.newChild(child.filename, child.name, icon); 220 p.intParam = ProjectItemType.SourceFile; 221 p.objectParam = child; 222 } 223 } 224 } 225 226 void updateDefault() { 227 TreeItem defaultItem = null; 228 if (_workspace && _tree.items.childCount && _workspace.startupProject) { 229 for (int i = 0; i < _tree.items.child(0).childCount; i++) { 230 TreeItem p = _tree.items.child(0).child(i); 231 if (p.objectParam is _workspace.startupProject) 232 defaultItem = p; 233 } 234 } 235 _tree.items.setDefaultItem(defaultItem); 236 } 237 238 /// map key to action 239 override Action findKeyAction(uint keyCode, uint flags) { 240 Action action = _acceleratorMap.findByKey(keyCode, flags); 241 if (action) { 242 if (TreeItem ti = _tree.items.selectedItem) { 243 _popupMenuSelectedItem = ti; 244 action.objectParam = ti.objectParam; 245 } else { 246 return null; 247 } 248 return action; 249 } 250 return super.findKeyAction(keyCode, flags); 251 } 252 253 void expandAll(const Action a) { 254 if (!_workspace) 255 return; 256 if (_popupMenuSelectedItem) 257 _popupMenuSelectedItem.expandAll(); 258 } 259 260 void collapseAll(const Action a) { 261 if (!_workspace) 262 return; 263 if (_popupMenuSelectedItem) 264 _popupMenuSelectedItem.collapseAll(); 265 } 266 267 protected bool[string] _itemStates; 268 protected bool _itemStatesDirty; 269 protected void readExpandedStateFromWorkspace() { 270 _itemStates.clear(); 271 if (_workspace) { 272 string[] items = _workspace.expandedItems; 273 foreach(item; items) 274 _itemStates[item] = true; 275 } 276 } 277 278 /// Saving items collapse/expand state 279 protected void saveItemState(string itemPath, bool expanded) { 280 bool changed = restoreItemState(itemPath) != expanded; 281 if (!_itemStatesDirty && changed) 282 _itemStatesDirty = true; 283 if (changed) { 284 if (expanded) { 285 _itemStates[itemPath] = true; 286 } else { 287 _itemStates.remove(itemPath); 288 } 289 string[] items; 290 items.assumeSafeAppend; 291 foreach(k,v; _itemStates) { 292 items ~= k; 293 } 294 _workspace.expandedItems = items; 295 debug Log.d("stored Expanded state ", expanded, " for ", itemPath); 296 } 297 } 298 /// Is need to expand item? 299 protected bool restoreItemState(string itemPath) { 300 if (auto p = itemPath in _itemStates) { 301 // Item itself must expand, but upper items may be collapsed 302 return *p; 303 /* 304 auto path = itemPath; 305 while (path.length > 0 && !path.endsWith("src") && path in _itemStates) { 306 auto pos = lastIndexOf(path, '/'); 307 path = pos > -1 ? path[ 0 .. pos ] : ""; 308 } 309 if (path.length == 0 || path.endsWith("src")) { 310 debug Log.d("restored Expanded state for ", itemPath); 311 return *p; 312 } 313 */ 314 } 315 return false; 316 } 317 318 void onTreeExpandedStateChange(TreeItems source, TreeItem item) { 319 bool expanded = item.expanded; 320 ProjectItem prjItem = cast(ProjectItem)item.objectParam; 321 if (prjItem) { 322 string fn = prjItem.filename; 323 debug Log.d("onTreeExpandedStateChange expanded=", expanded, " fn=", fn); 324 saveItemState(fn, expanded); 325 } 326 } 327 328 void reloadItems() { 329 _tree.expandedChange.disconnect(&onTreeExpandedStateChange); 330 _tree.selectionChange.disconnect(&onTreeItemSelected); 331 _tree.clearAllItems(); 332 333 if (_workspace) { 334 TreeItem defaultItem = null; 335 TreeItem root = _tree.items.newChild(_workspace.filename, _workspace.name, "project-development"); 336 root.intParam = ProjectItemType.Workspace; 337 foreach(project; _workspace.projects) { 338 TreeItem p = root.newChild(project.filename, project.name, project.isDependency ? "project-d-dependency" : "project-d"); 339 p.intParam = ProjectItemType.Project; 340 p.objectParam = project; 341 if (restoreItemState(project.filename)) 342 p.expand(); 343 else 344 p.collapse(); 345 if (project && _workspace.startupProject is project) 346 defaultItem = p; 347 addProjectItems(p, project.items); 348 } 349 _tree.items.setDefaultItem(defaultItem); 350 } else { 351 _tree.items.newChild("none", "No workspace"d, "project-development"); 352 } 353 _tree.expandedChange.connect(&onTreeExpandedStateChange); 354 _tree.selectionChange.connect(&onTreeItemSelected); 355 356 // expand default project if no information about expanded items 357 if (!_itemStates.length) { 358 if (_workspace && _workspace.startupProject) { 359 string fn = _workspace.startupProject.filename; 360 TreeItem startupProjectItem = _tree.items.findItemById(fn); 361 if (startupProjectItem) { 362 startupProjectItem.expand(); 363 saveItemState(fn, true); 364 } 365 } 366 } 367 if (_workspace) { 368 // restore selection 369 string id = _workspace.selectedWorkspaceItem; 370 _tree.selectItem(id); 371 } 372 373 updateDefault(); 374 } 375 376 @property void workspace(Workspace w) { 377 _workspace = w; 378 readExpandedStateFromWorkspace(); 379 reloadItems(); 380 } 381 382 /// override to handle specific actions 383 override bool handleAction(const Action a) { 384 if (workspaceActionListener.assigned) 385 return workspaceActionListener(a); 386 return false; 387 } 388 389 override protected bool onCloseButtonClick(Widget source) { 390 hide(); 391 return true; 392 } 393 394 /// hide workspace panel 395 void hide() { 396 visibility = Visibility.Gone; 397 parent.layout(parent.pos); 398 } 399 400 // activate workspace panel if hidden 401 void activate() { 402 if (visibility == Visibility.Gone) { 403 visibility = Visibility.Visible; 404 parent.layout(parent.pos); 405 } 406 setFocus(); 407 } 408 }