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