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 }