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 }