|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2008 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the multi project browser. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import glob |
|
12 |
|
13 from PyQt6.QtCore import Qt |
|
14 from PyQt6.QtWidgets import QTreeWidget, QTreeWidgetItem, QDialog, QMenu |
|
15 |
|
16 from EricWidgets.EricApplication import ericApp |
|
17 from EricWidgets import EricMessageBox |
|
18 |
|
19 import UI.PixmapCache |
|
20 |
|
21 |
|
22 class MultiProjectBrowser(QTreeWidget): |
|
23 """ |
|
24 Class implementing the multi project browser. |
|
25 """ |
|
26 ProjectFileNameRole = Qt.ItemDataRole.UserRole |
|
27 ProjectUidRole = Qt.ItemDataRole.UserRole + 1 |
|
28 |
|
29 def __init__(self, multiProject, project, parent=None): |
|
30 """ |
|
31 Constructor |
|
32 |
|
33 @param multiProject reference to the multi project object |
|
34 @type MultiProject |
|
35 @param project reference to the project object |
|
36 @type Project |
|
37 @param parent parent widget |
|
38 @type QWidget |
|
39 """ |
|
40 super().__init__(parent) |
|
41 self.multiProject = multiProject |
|
42 self.project = project |
|
43 |
|
44 self.setWindowIcon(UI.PixmapCache.getIcon("eric")) |
|
45 self.setAlternatingRowColors(True) |
|
46 self.setHeaderHidden(True) |
|
47 self.setItemsExpandable(False) |
|
48 self.setRootIsDecorated(False) |
|
49 self.setSortingEnabled(True) |
|
50 |
|
51 self.__openingProject = False |
|
52 |
|
53 self.multiProject.newMultiProject.connect( |
|
54 self.__newMultiProject) |
|
55 self.multiProject.multiProjectOpened.connect( |
|
56 self.__multiProjectOpened) |
|
57 self.multiProject.multiProjectClosed.connect( |
|
58 self.__multiProjectClosed) |
|
59 self.multiProject.projectDataChanged.connect( |
|
60 self.__projectDataChanged) |
|
61 self.multiProject.projectAdded.connect( |
|
62 self.__projectAdded) |
|
63 self.multiProject.projectRemoved.connect( |
|
64 self.__projectRemoved) |
|
65 |
|
66 self.project.projectOpened.connect(self.__projectOpened) |
|
67 self.project.projectClosed.connect(self.__projectClosed) |
|
68 |
|
69 self.__createPopupMenu() |
|
70 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) |
|
71 self.customContextMenuRequested.connect(self.__contextMenuRequested) |
|
72 self.itemActivated.connect(self.__openItem) |
|
73 |
|
74 self.setEnabled(False) |
|
75 |
|
76 ########################################################################### |
|
77 ## Slot handling methods below |
|
78 ########################################################################### |
|
79 |
|
80 def __newMultiProject(self): |
|
81 """ |
|
82 Private slot to handle the creation of a new multi project. |
|
83 """ |
|
84 self.clear() |
|
85 self.setEnabled(True) |
|
86 |
|
87 def __multiProjectOpened(self): |
|
88 """ |
|
89 Private slot to handle the opening of a multi project. |
|
90 """ |
|
91 for project in self.multiProject.getProjects(): |
|
92 self.__addProject(project) |
|
93 |
|
94 self.sortItems(0, Qt.SortOrder.AscendingOrder) |
|
95 |
|
96 self.setEnabled(True) |
|
97 |
|
98 def __multiProjectClosed(self): |
|
99 """ |
|
100 Private slot to handle the closing of a multi project. |
|
101 """ |
|
102 self.clear() |
|
103 self.setEnabled(False) |
|
104 |
|
105 def __projectAdded(self, project): |
|
106 """ |
|
107 Private slot to handle the addition of a project to the multi project. |
|
108 |
|
109 @param project reference to the project data dictionary |
|
110 """ |
|
111 self.__addProject(project) |
|
112 self.sortItems(0, Qt.SortOrder.AscendingOrder) |
|
113 |
|
114 def __projectRemoved(self, project): |
|
115 """ |
|
116 Private slot to handle the removal of a project from the multi project. |
|
117 |
|
118 @param project reference to the project data dictionary |
|
119 """ |
|
120 itm = self.__findProjectItem(project) |
|
121 if itm: |
|
122 parent = itm.parent() |
|
123 parent.removeChild(itm) |
|
124 del itm |
|
125 if parent.childCount() == 0: |
|
126 top = self.takeTopLevelItem(self.indexOfTopLevelItem(parent)) |
|
127 # __IGNORE_WARNING__ |
|
128 del top |
|
129 |
|
130 def __projectDataChanged(self, project): |
|
131 """ |
|
132 Private slot to handle the change of a project of the multi project. |
|
133 |
|
134 @param project reference to the project data dictionary |
|
135 """ |
|
136 itm = self.__findProjectItem(project) |
|
137 if itm: |
|
138 parent = itm.parent() |
|
139 if parent.text(0) != project["category"]: |
|
140 self.__projectRemoved(project) |
|
141 self.__addProject(project) |
|
142 else: |
|
143 self.__setItemData(itm, project) |
|
144 |
|
145 self.sortItems(0, Qt.SortOrder.AscendingOrder) |
|
146 |
|
147 def __projectOpened(self): |
|
148 """ |
|
149 Private slot to handle the opening of a project. |
|
150 """ |
|
151 projectfile = self.project.getProjectFile() |
|
152 project = { |
|
153 'name': "", |
|
154 'file': projectfile, |
|
155 'master': False, |
|
156 'description': "", |
|
157 'category': "", |
|
158 'uid': "", |
|
159 } |
|
160 itm = self.__findProjectItem(project) |
|
161 if itm: |
|
162 font = itm.font(0) |
|
163 font.setBold(True) |
|
164 itm.setFont(0, font) |
|
165 |
|
166 def __projectClosed(self): |
|
167 """ |
|
168 Private slot to handle the closing of a project. |
|
169 """ |
|
170 for topIndex in range(self.topLevelItemCount()): |
|
171 topItem = self.topLevelItem(topIndex) |
|
172 for childIndex in range(topItem.childCount()): |
|
173 childItem = topItem.child(childIndex) |
|
174 font = childItem.font(0) |
|
175 font.setBold(False) |
|
176 childItem.setFont(0, font) |
|
177 |
|
178 def __contextMenuRequested(self, coord): |
|
179 """ |
|
180 Private slot to show the context menu. |
|
181 |
|
182 @param coord the position of the mouse pointer (QPoint) |
|
183 """ |
|
184 itm = self.itemAt(coord) |
|
185 if itm is None or itm.parent() is None: |
|
186 self.__backMenu.popup(self.mapToGlobal(coord)) |
|
187 else: |
|
188 self.__menu.popup(self.mapToGlobal(coord)) |
|
189 |
|
190 def __openItem(self, itm=None): |
|
191 """ |
|
192 Private slot to open a project. |
|
193 |
|
194 @param itm reference to the project item to be opened (QTreeWidgetItem) |
|
195 """ |
|
196 if itm is None: |
|
197 itm = self.currentItem() |
|
198 if itm is None or itm.parent() is None: |
|
199 return |
|
200 |
|
201 if not self.__openingProject: |
|
202 filename = itm.data(0, MultiProjectBrowser.ProjectFileNameRole) |
|
203 if filename: |
|
204 self.__openingProject = True |
|
205 self.multiProject.openProject(filename) |
|
206 self.__openingProject = False |
|
207 |
|
208 ########################################################################### |
|
209 ## Private methods below |
|
210 ########################################################################### |
|
211 |
|
212 def __findCategoryItem(self, category): |
|
213 """ |
|
214 Private method to find the item for a category. |
|
215 |
|
216 @param category category to search for (string) |
|
217 @return reference to the category item or None, if there is |
|
218 no such item (QTreeWidgetItem or None) |
|
219 """ |
|
220 if category == "": |
|
221 category = self.tr("Not categorized") |
|
222 for index in range(self.topLevelItemCount()): |
|
223 itm = self.topLevelItem(index) |
|
224 if itm.text(0) == category: |
|
225 return itm |
|
226 |
|
227 return None |
|
228 |
|
229 def __addProject(self, project): |
|
230 """ |
|
231 Private method to add a project to the list. |
|
232 |
|
233 @param project reference to the project data dictionary |
|
234 """ |
|
235 parent = self.__findCategoryItem(project['category']) |
|
236 if parent is None: |
|
237 if project['category']: |
|
238 parent = QTreeWidgetItem(self, [project['category']]) |
|
239 else: |
|
240 parent = QTreeWidgetItem(self, [self.tr("Not categorized")]) |
|
241 parent.setExpanded(True) |
|
242 itm = QTreeWidgetItem(parent) |
|
243 self.__setItemData(itm, project) |
|
244 |
|
245 def __setItemData(self, itm, project): |
|
246 """ |
|
247 Private method to set the data of a project item. |
|
248 |
|
249 @param itm reference to the item to be set (QTreeWidgetItem) |
|
250 @param project reference to the project data dictionary |
|
251 """ |
|
252 itm.setText(0, project['name']) |
|
253 if project['master']: |
|
254 itm.setIcon(0, UI.PixmapCache.getIcon("masterProject")) |
|
255 else: |
|
256 itm.setIcon(0, UI.PixmapCache.getIcon("empty")) |
|
257 itm.setToolTip(0, project['file']) |
|
258 itm.setData(0, MultiProjectBrowser.ProjectFileNameRole, |
|
259 project['file']) |
|
260 itm.setData(0, MultiProjectBrowser.ProjectUidRole, project['uid']) |
|
261 |
|
262 def __findProjectItem(self, project): |
|
263 """ |
|
264 Private method to search a specific project item. |
|
265 |
|
266 @param project reference to the project data dictionary |
|
267 @return reference to the item (QTreeWidgetItem) or None |
|
268 """ |
|
269 if project["uid"]: |
|
270 compareData = project["uid"] |
|
271 compareRole = MultiProjectBrowser.ProjectUidRole |
|
272 else: |
|
273 compareData = project["file"] |
|
274 compareRole = MultiProjectBrowser.ProjectFileNameRole |
|
275 |
|
276 for topIndex in range(self.topLevelItemCount()): |
|
277 topItm = self.topLevelItem(topIndex) |
|
278 for childIndex in range(topItm.childCount()): |
|
279 itm = topItm.child(childIndex) |
|
280 data = itm.data(0, compareRole) |
|
281 if data == compareData: |
|
282 return itm |
|
283 |
|
284 return None |
|
285 |
|
286 def __removeProject(self): |
|
287 """ |
|
288 Private method to handle the Remove context menu entry. |
|
289 """ |
|
290 itm = self.currentItem() |
|
291 if itm is not None and itm.parent() is not None: |
|
292 uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) |
|
293 if uid: |
|
294 self.multiProject.removeProject(uid) |
|
295 |
|
296 def __deleteProject(self): |
|
297 """ |
|
298 Private method to handle the Delete context menu entry. |
|
299 """ |
|
300 itm = self.currentItem() |
|
301 if itm is not None and itm.parent() is not None: |
|
302 projectFile = itm.data(0, MultiProjectBrowser.ProjectFileNameRole) |
|
303 projectPath = os.path.dirname(projectFile) |
|
304 |
|
305 if self.project.getProjectPath() == projectPath: |
|
306 EricMessageBox.warning( |
|
307 self, |
|
308 self.tr("Delete Project"), |
|
309 self.tr("""The current project cannot be deleted.""" |
|
310 """ Please close it first.""")) |
|
311 else: |
|
312 projectFiles = glob.glob(os.path.join(projectPath, "*.epj")) |
|
313 projectFiles += glob.glob(os.path.join(projectPath, "*.e4p")) |
|
314 if not projectFiles: |
|
315 # Oops, that should not happen; play it save |
|
316 res = False |
|
317 elif len(projectFiles) == 1: |
|
318 res = EricMessageBox.yesNo( |
|
319 self, |
|
320 self.tr("Delete Project"), |
|
321 self.tr("""<p>Shall the project <b>{0}</b> (Path:""" |
|
322 """ {1}) really be deleted?</p>""").format( |
|
323 itm.text(0), projectPath)) |
|
324 else: |
|
325 res = EricMessageBox.yesNo( |
|
326 self, |
|
327 self.tr("Delete Project"), |
|
328 self.tr("""<p>Shall the project <b>{0}</b> (Path:""" |
|
329 """ {1}) really be deleted?</p>""" |
|
330 """<p><b>Warning:</b> It contains <b>{2}</b>""" |
|
331 """ sub-projects.</p>""").format( |
|
332 itm.text(0), projectPath, len(projectFiles))) |
|
333 if res: |
|
334 for subprojectFile in projectFiles: |
|
335 # remove all sub-projects before deleting the directory |
|
336 if subprojectFile != projectFile: |
|
337 projectData = { |
|
338 'name': "", |
|
339 'file': subprojectFile, |
|
340 'master': False, |
|
341 'description': "", |
|
342 'category': "", |
|
343 'uid': "", |
|
344 } |
|
345 pitm = self.__findProjectItem(projectData) |
|
346 if pitm: |
|
347 uid = pitm.data( |
|
348 0, MultiProjectBrowser.ProjectUidRole) |
|
349 if uid: |
|
350 self.multiProject.removeProject(uid) |
|
351 |
|
352 uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) |
|
353 if uid: |
|
354 self.multiProject.deleteProject(uid) |
|
355 |
|
356 def __showProjectProperties(self): |
|
357 """ |
|
358 Private method to show the data of a project entry. |
|
359 """ |
|
360 itm = self.currentItem() |
|
361 if itm is not None and itm.parent() is not None: |
|
362 uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) |
|
363 if uid: |
|
364 project = self.multiProject.getProject(uid) |
|
365 if project is not None: |
|
366 from .AddProjectDialog import AddProjectDialog |
|
367 dlg = AddProjectDialog( |
|
368 self, project=project, |
|
369 categories=self.multiProject.getCategories()) |
|
370 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
371 (name, filename, isMaster, description, category, |
|
372 uid) = dlg.getData() |
|
373 project = { |
|
374 'name': name, |
|
375 'file': filename, |
|
376 'master': isMaster, |
|
377 'description': description, |
|
378 'category': category, |
|
379 'uid': uid, |
|
380 } |
|
381 self.multiProject.changeProjectProperties(project) |
|
382 |
|
383 def __addNewProject(self): |
|
384 """ |
|
385 Private method to add a new project entry. |
|
386 """ |
|
387 itm = self.currentItem() |
|
388 if itm is not None: |
|
389 if itm.parent() is None: |
|
390 # current item is a category item |
|
391 category = itm.text(0) |
|
392 else: |
|
393 category = itm.parent().text(0) |
|
394 else: |
|
395 category = "" |
|
396 self.multiProject.addNewProject(category=category) |
|
397 |
|
398 def __copyProject(self): |
|
399 """ |
|
400 Private method to copy the selected project on disk. |
|
401 """ |
|
402 itm = self.currentItem() |
|
403 if itm and itm.parent(): |
|
404 # it is a project item and not a category |
|
405 uid = itm.data(0, MultiProjectBrowser.ProjectUidRole) |
|
406 if uid: |
|
407 self.multiProject.copyProject(uid) |
|
408 |
|
409 def __createPopupMenu(self): |
|
410 """ |
|
411 Private method to create the popup menu. |
|
412 """ |
|
413 self.__menu = QMenu(self) |
|
414 self.__menu.addAction(self.tr("Open"), self.__openItem) |
|
415 self.__menu.addAction(self.tr("Remove from Multi Project"), |
|
416 self.__removeProject) |
|
417 self.__menu.addAction(self.tr("Delete from Disk"), |
|
418 self.__deleteProject) |
|
419 self.__menu.addAction(self.tr("Properties"), |
|
420 self.__showProjectProperties) |
|
421 self.__menu.addSeparator() |
|
422 self.__menu.addAction(self.tr("Add Project..."), |
|
423 self.__addNewProject) |
|
424 self.__menu.addAction(self.tr("Copy Project..."), |
|
425 self.__copyProject) |
|
426 self.__menu.addSeparator() |
|
427 self.__menu.addAction(self.tr("Configure..."), self.__configure) |
|
428 |
|
429 self.__backMenu = QMenu(self) |
|
430 self.__backMenu.addAction(self.tr("Add Project..."), |
|
431 self.__addNewProject) |
|
432 self.__backMenu.addSeparator() |
|
433 self.__backMenu.addAction(self.tr("Configure..."), |
|
434 self.__configure) |
|
435 |
|
436 def __configure(self): |
|
437 """ |
|
438 Private method to open the configuration dialog. |
|
439 """ |
|
440 ericApp().getObject("UserInterface").showPreferences( |
|
441 "multiProjectPage") |