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 }