1 module dlangide.ui.wspanel; 2 3 import dlangui; 4 import dlangide.workspace.workspace; 5 import dlangide.workspace.project; 6 import dlangide.ui.commands; 7 8 enum ProjectItemType : int { 9 None, 10 SourceFile, 11 SourceFolder, 12 Project, 13 Workspace 14 } 15 16 interface SourceFileSelectionHandler { 17 bool onSourceFileSelected(ProjectSourceFile file, bool activate); 18 } 19 20 interface WorkspaceActionHandler { 21 bool onWorkspaceAction(const Action a); 22 } 23 24 class WorkspacePanel : DockWindow { 25 protected Workspace _workspace; 26 protected TreeWidget _tree; 27 28 /// handle source file selection change 29 Signal!SourceFileSelectionHandler sourceFileSelectionListener; 30 Signal!WorkspaceActionHandler workspaceActionListener; 31 32 this(string id) { 33 super(id); 34 workspace = null; 35 //layoutWidth = 200; 36 _caption.text = "Workspace Explorer"d; 37 } 38 39 bool selectItem(ProjectItem projectItem) { 40 if (projectItem) { 41 TreeItem item = _tree.findItemById(projectItem.filename); 42 if (item) { 43 _tree.selectItem(item); 44 return true; 45 } 46 } else { 47 _tree.clearSelection(); 48 return true; 49 } 50 return false; 51 } 52 53 void onTreeItemSelected(TreeItems source, TreeItem selectedItem, bool activated) { 54 if (!selectedItem) 55 return; 56 if (_workspace) { 57 // save selected item id 58 ProjectItem item = cast(ProjectItem)selectedItem.objectParam; 59 if (item) { 60 string id = item.filename; 61 if (id) 62 _workspace.selectedWorkspaceItem = id; 63 } 64 } 65 if (selectedItem.intParam == ProjectItemType.SourceFile) { 66 // file selected 67 if (sourceFileSelectionListener.assigned) { 68 ProjectSourceFile sourceFile = cast(ProjectSourceFile)selectedItem.objectParam; 69 if (sourceFile) { 70 sourceFileSelectionListener(sourceFile, activated); 71 } 72 } 73 } else if (selectedItem.intParam == ProjectItemType.SourceFolder) { 74 // folder selected 75 } 76 } 77 78 override protected Widget createBodyWidget() { 79 _tree = new TreeWidget("wstree"); 80 _tree.layoutHeight(FILL_PARENT).layoutHeight(FILL_PARENT); 81 _tree.selectionChange = &onTreeItemSelected; 82 _tree.expandedChange.connect(&onTreeExpandedStateChange); 83 _tree.fontSize = 16; 84 _tree.noCollapseForSingleTopLevelItem = true; 85 _tree.popupMenu = &onTreeItemPopupMenu; 86 87 _workspacePopupMenu = new MenuItem(); 88 _workspacePopupMenu.add(ACTION_PROJECT_FOLDER_REFRESH, 89 ACTION_FILE_WORKSPACE_CLOSE); 90 91 _projectPopupMenu = new MenuItem(); 92 _projectPopupMenu.add(ACTION_PROJECT_SET_STARTUP, 93 ACTION_PROJECT_FOLDER_REFRESH, 94 ACTION_FILE_NEW_SOURCE_FILE, 95 //ACTION_PROJECT_FOLDER_OPEN_ITEM, 96 ACTION_PROJECT_BUILD, 97 ACTION_PROJECT_REBUILD, 98 ACTION_PROJECT_CLEAN, 99 ACTION_PROJECT_UPDATE_DEPENDENCIES, 100 ACTION_PROJECT_REVEAL_IN_EXPLORER, 101 ACTION_PROJECT_SETTINGS, 102 //ACTION_PROJECT_FOLDER_REMOVE_ITEM 103 ); 104 105 _folderPopupMenu = new MenuItem(); 106 _folderPopupMenu.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_PROJECT_FOLDER_REFRESH, ACTION_PROJECT_FOLDER_OPEN_ITEM, 107 //ACTION_PROJECT_FOLDER_REMOVE_ITEM, 108 //ACTION_PROJECT_FOLDER_RENAME_ITEM 109 ); 110 111 _filePopupMenu = new MenuItem(); 112 _filePopupMenu.add(ACTION_FILE_NEW_SOURCE_FILE, ACTION_PROJECT_FOLDER_REFRESH, 113 ACTION_PROJECT_FOLDER_OPEN_ITEM, 114 ACTION_PROJECT_FOLDER_REMOVE_ITEM, 115 //ACTION_PROJECT_FOLDER_RENAME_ITEM 116 ); 117 return _tree; 118 } 119 120 protected MenuItem _workspacePopupMenu; 121 protected MenuItem _projectPopupMenu; 122 protected MenuItem _folderPopupMenu; 123 protected MenuItem _filePopupMenu; 124 protected string _popupMenuSelectedItemId; 125 protected void onPopupMenuItem(MenuItem item) { 126 if (item.action) 127 handleAction(item.action); 128 } 129 130 protected MenuItem onTreeItemPopupMenu(TreeItems source, TreeItem selectedItem) { 131 MenuItem menu = null; 132 _popupMenuSelectedItemId = selectedItem.id; 133 if (selectedItem.intParam == ProjectItemType.SourceFolder) { 134 menu = _folderPopupMenu; 135 } else if (selectedItem.intParam == ProjectItemType.SourceFile) { 136 menu = _filePopupMenu; 137 } else if (selectedItem.intParam == ProjectItemType.Project) { 138 menu = _projectPopupMenu; 139 } else if (selectedItem.intParam == ProjectItemType.Workspace) { 140 menu = _workspacePopupMenu; 141 } 142 if (menu && menu.subitemCount) { 143 for (int i = 0; i < menu.subitemCount; i++) { 144 Action a = menu.subitem(i).action.clone(); 145 a.objectParam = selectedItem.objectParam; 146 menu.subitem(i).action = a; 147 //menu.subitem(i).menuItemAction = &handleAction; 148 } 149 //menu.onMenuItem = &onPopupMenuItem; 150 //menu.menuItemClick = &onPopupMenuItem; 151 menu.menuItemAction = &handleAction; 152 menu.updateActionState(this); 153 return menu; 154 } 155 return null; 156 } 157 158 @property Workspace workspace() { 159 return _workspace; 160 } 161 162 /// returns currently selected project item 163 @property ProjectItem selectedProjectItem() { 164 TreeItem ti = _tree.items.selectedItem; 165 if (!ti) 166 return null; 167 Object obj = ti.objectParam; 168 if (!obj) 169 return null; 170 return cast(ProjectItem)obj; 171 } 172 173 ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) { 174 if (_workspace) 175 return _workspace.findSourceFileItem(filename, fullFileName); 176 return null; 177 } 178 179 void addProjectItems(TreeItem root, ProjectItem items) { 180 for (int i = 0; i < items.childCount; i++) { 181 ProjectItem child = items.child(i); 182 if (child.isFolder) { 183 TreeItem p = root.newChild(child.filename, child.name, "folder"); 184 p.intParam = ProjectItemType.SourceFolder; 185 p.objectParam = child; 186 if (restoreItemState(child.filename)) 187 p.expand(); 188 else 189 p.collapse(); 190 addProjectItems(p, child); 191 } else { 192 string icon = "text-other"; 193 if (child.isDSourceFile) 194 icon = "text-d"; 195 if (child.isJsonFile) 196 icon = "text-json"; 197 if (child.isDMLFile) 198 icon = "text-dml"; 199 TreeItem p = root.newChild(child.filename, child.name, icon); 200 p.intParam = ProjectItemType.SourceFile; 201 p.objectParam = child; 202 } 203 } 204 } 205 206 void updateDefault() { 207 TreeItem defaultItem = null; 208 if (_workspace && _tree.items.childCount && _workspace.startupProject) { 209 for (int i = 0; i < _tree.items.child(0).childCount; i++) { 210 TreeItem p = _tree.items.child(0).child(i); 211 if (p.objectParam is _workspace.startupProject) 212 defaultItem = p; 213 } 214 } 215 _tree.items.setDefaultItem(defaultItem); 216 } 217 218 protected bool[string] _itemStates; 219 protected bool _itemStatesDirty; 220 protected void readExpandedStateFromWorkspace() { 221 _itemStates.clear(); 222 if (_workspace) { 223 string[] items = _workspace.expandedItems; 224 foreach(item; items) 225 _itemStates[item] = true; 226 } 227 } 228 protected void saveItemState(string itemPath, bool expanded) { 229 bool changed = restoreItemState(itemPath) != expanded; 230 if (!_itemStatesDirty && changed) 231 _itemStatesDirty = true; 232 if (changed) { 233 if (expanded) { 234 _itemStates[itemPath] = true; 235 } else { 236 _itemStates.remove(itemPath); 237 } 238 string[] items; 239 items.assumeSafeAppend; 240 foreach(k,v; _itemStates) { 241 items ~= k; 242 } 243 _workspace.expandedItems = items; 244 } 245 debug Log.d("stored Expanded state ", expanded, " for ", itemPath); 246 } 247 protected bool restoreItemState(string itemPath) { 248 if (auto p = itemPath in _itemStates) { 249 debug Log.d("restored Expanded state for ", itemPath); 250 return *p; 251 } 252 return false; 253 } 254 255 void onTreeExpandedStateChange(TreeItems source, TreeItem item) { 256 bool expanded = item.expanded; 257 ProjectItem prjItem = cast(ProjectItem)item.objectParam; 258 if (prjItem) { 259 string fn = prjItem.filename; 260 debug Log.d("onTreeExpandedStateChange expanded=", expanded, " fn=", fn); 261 saveItemState(fn, expanded); 262 } 263 } 264 265 void reloadItems() { 266 _tree.expandedChange.disconnect(&onTreeExpandedStateChange); 267 _tree.clearAllItems(); 268 if (_workspace) { 269 TreeItem defaultItem = null; 270 TreeItem root = _tree.items.newChild(_workspace.filename, _workspace.name, "project-development"); 271 root.intParam = ProjectItemType.Workspace; 272 foreach(project; _workspace.projects) { 273 TreeItem p = root.newChild(project.filename, project.name, project.isDependency ? "project-d-dependency" : "project-d"); 274 p.intParam = ProjectItemType.Project; 275 p.objectParam = project; 276 if (restoreItemState(project.filename)) 277 p.expand(); 278 else 279 p.collapse(); 280 if (project && _workspace.startupProject is project) 281 defaultItem = p; 282 addProjectItems(p, project.items); 283 } 284 _tree.items.setDefaultItem(defaultItem); 285 } else { 286 _tree.items.newChild("none", "No workspace"d, "project-development"); 287 } 288 _tree.expandedChange.connect(&onTreeExpandedStateChange); 289 290 // expand default project if no information about expanded items 291 if (!_itemStates.length) { 292 if (_workspace && _workspace.startupProject) { 293 string fn = _workspace.startupProject.filename; 294 TreeItem startupProjectItem = _tree.items.findItemById(fn); 295 if (startupProjectItem) { 296 startupProjectItem.expand(); 297 saveItemState(fn, true); 298 } 299 } 300 } 301 if (_workspace) { 302 // restore selection 303 string id = _workspace.selectedWorkspaceItem; 304 _tree.selectItem(id); 305 } 306 307 updateDefault(); 308 } 309 310 @property void workspace(Workspace w) { 311 _workspace = w; 312 readExpandedStateFromWorkspace(); 313 reloadItems(); 314 } 315 316 /// override to handle specific actions 317 override bool handleAction(const Action a) { 318 if (workspaceActionListener.assigned) 319 return workspaceActionListener(a); 320 return false; 321 } 322 }