eric6/Project/ProjectBaseBrowser.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7229
53054eb5b15a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2002 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the baseclass for the various project browsers.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13
14 from PyQt5.QtCore import QModelIndex, pyqtSignal, Qt, QCoreApplication, \
15 QItemSelectionModel, QItemSelection, QElapsedTimer
16 from PyQt5.QtGui import QCursor
17 from PyQt5.QtWidgets import QTreeView, QApplication, QMenu, QDialog, \
18 QAbstractItemView
19
20 from E5Gui.E5Application import e5App
21 from E5Gui import E5MessageBox
22
23 from UI.Browser import Browser
24 from UI.BrowserModel import BrowserDirectoryItem, BrowserFileItem
25
26 from .ProjectBrowserModel import ProjectBrowserSimpleDirectoryItem, \
27 ProjectBrowserDirectoryItem, ProjectBrowserFileItem
28 from .ProjectBrowserSortFilterProxyModel import \
29 ProjectBrowserSortFilterProxyModel
30
31
32 class ProjectBaseBrowser(Browser):
33 """
34 Baseclass implementing common functionality for the various project
35 browsers.
36
37 @signal closeSourceWindow(str) emitted to close a source file
38 """
39 closeSourceWindow = pyqtSignal(str)
40
41 def __init__(self, project, type_, parent=None):
42 """
43 Constructor
44
45 @param project reference to the project object
46 @param type_ project browser type (string)
47 @param parent parent widget of this browser
48 """
49 QTreeView.__init__(self, parent)
50
51 self.project = project
52
53 self._model = project.getModel()
54 self._sortModel = ProjectBrowserSortFilterProxyModel(type_)
55 self._sortModel.setSourceModel(self._model)
56 self.setModel(self._sortModel)
57
58 self.selectedItemsFilter = [ProjectBrowserFileItem]
59
60 # contains codes for special menu entries
61 # 1 = specials for Others browser
62 self.specialMenuEntries = []
63 self.isTranslationsBrowser = False
64 self.expandedNames = []
65
66 self.SelectFlags = QItemSelectionModel.SelectionFlags(
67 QItemSelectionModel.Select | QItemSelectionModel.Rows)
68 self.DeselectFlags = QItemSelectionModel.SelectionFlags(
69 QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
70
71 self._activating = False
72
73 self.setContextMenuPolicy(Qt.CustomContextMenu)
74 self.customContextMenuRequested.connect(self._contextMenuRequested)
75 self.activated.connect(self._openItem)
76 self._model.rowsInserted.connect(self.__modelRowsInserted)
77 self._connectExpandedCollapsed()
78
79 self._createPopupMenus()
80
81 self.currentItemName = None
82
83 self._init() # perform common initialization tasks
84
85 self._keyboardSearchString = ""
86 self._keyboardSearchTimer = QElapsedTimer()
87 self._keyboardSearchTimer.invalidate()
88
89 self._initHookMethods() # perform initialization of the hooks
90 self.hooksMenuEntries = {}
91
92 def _connectExpandedCollapsed(self):
93 """
94 Protected method to connect the expanded and collapsed signals.
95 """
96 self.expanded.connect(self._resizeColumns)
97 self.collapsed.connect(self._resizeColumns)
98
99 def _disconnectExpandedCollapsed(self):
100 """
101 Protected method to disconnect the expanded and collapsed signals.
102 """
103 self.expanded.disconnect(self._resizeColumns)
104 self.collapsed.disconnect(self._resizeColumns)
105
106 def _createPopupMenus(self):
107 """
108 Protected overloaded method to generate the popup menus.
109 """
110 # create the popup menu for source files
111 self.sourceMenu = QMenu(self)
112 self.sourceMenu.addAction(
113 QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
114 self._openItem)
115
116 # create the popup menu for general use
117 self.menu = QMenu(self)
118 self.menu.addAction(
119 QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
120 self._openItem)
121
122 # create the menu for multiple selected files
123 self.multiMenu = QMenu(self)
124 self.multiMenu.addAction(
125 QCoreApplication.translate('ProjectBaseBrowser', 'Open'),
126 self._openItem)
127
128 # create the background menu
129 self.backMenu = None
130
131 # create the directories menu
132 self.dirMenu = None
133
134 # create the directory for multiple selected directories
135 self.dirMultiMenu = None
136
137 self.menuActions = []
138 self.multiMenuActions = []
139 self.dirMenuActions = []
140 self.dirMultiMenuActions = []
141
142 self.mainMenu = None
143
144 def _contextMenuRequested(self, coord):
145 """
146 Protected slot to show the context menu.
147
148 @param coord the position of the mouse pointer (QPoint)
149 """
150 if not self.project.isOpen():
151 return
152
153 cnt = self.getSelectedItemsCount()
154 if cnt > 1:
155 self.multiMenu.popup(self.mapToGlobal(coord))
156 else:
157 index = self.indexAt(coord)
158
159 if index.isValid():
160 self.menu.popup(self.mapToGlobal(coord))
161 else:
162 self.backMenu and self.backMenu.popup(self.mapToGlobal(coord))
163
164 def _selectSingleItem(self, index):
165 """
166 Protected method to select a single item.
167
168 @param index index of item to be selected (QModelIndex)
169 """
170 if index.isValid():
171 self.setCurrentIndex(index)
172 flags = QItemSelectionModel.SelectionFlags(
173 QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
174 self.selectionModel().select(index, flags)
175
176 def _setItemSelected(self, index, selected):
177 """
178 Protected method to set the selection status of an item.
179
180 @param index index of item to set (QModelIndex)
181 @param selected flag giving the new selection status (boolean)
182 """
183 if index.isValid():
184 self.selectionModel().select(
185 index, selected and self.SelectFlags or self.DeselectFlags)
186
187 def _setItemRangeSelected(self, startIndex, endIndex, selected):
188 """
189 Protected method to set the selection status of a range of items.
190
191 @param startIndex start index of range of items to set (QModelIndex)
192 @param endIndex end index of range of items to set (QModelIndex)
193 @param selected flag giving the new selection status (boolean)
194 """
195 selection = QItemSelection(startIndex, endIndex)
196 self.selectionModel().select(
197 selection, selected and self.SelectFlags or self.DeselectFlags)
198
199 def __modelRowsInserted(self, parent, start, end):
200 """
201 Private slot called after rows have been inserted into the model.
202
203 @param parent parent index of inserted rows (QModelIndex)
204 @param start start row number (integer)
205 @param end end row number (integer)
206 """
207 self._resizeColumns(parent)
208
209 def _projectClosed(self):
210 """
211 Protected slot to handle the projectClosed signal.
212 """
213 self.layoutDisplay()
214 if self.backMenu is not None:
215 self.backMenu.setEnabled(False)
216
217 self._createPopupMenus()
218
219 def _projectOpened(self):
220 """
221 Protected slot to handle the projectOpened signal.
222 """
223 self.layoutDisplay()
224 self.sortByColumn(0, Qt.DescendingOrder)
225 self.sortByColumn(0, Qt.AscendingOrder)
226 self._initMenusAndVcs()
227
228 def _initMenusAndVcs(self):
229 """
230 Protected slot to initialize the menus and the Vcs interface.
231 """
232 self._createPopupMenus()
233
234 if self.backMenu is not None:
235 self.backMenu.setEnabled(True)
236
237 if self.project.vcs is not None:
238 self.vcsHelper = self.project.vcs.vcsGetProjectBrowserHelper(
239 self, self.project, self.isTranslationsBrowser)
240 self.vcsHelper.addVCSMenus(
241 self.mainMenu, self.multiMenu, self.backMenu,
242 self.dirMenu, self.dirMultiMenu)
243
244 def _newProject(self):
245 """
246 Protected slot to handle the newProject signal.
247 """
248 # default to perform same actions as opening a project
249 self._projectOpened()
250
251 def _removeFile(self):
252 """
253 Protected method to remove a file or files from the project.
254 """
255 itmList = self.getSelectedItems()
256
257 for itm in itmList[:]:
258 fn = itm.fileName()
259 self.closeSourceWindow.emit(fn)
260 self.project.removeFile(fn)
261
262 def _removeDir(self):
263 """
264 Protected method to remove a (single) directory from the project.
265 """
266 itmList = self.getSelectedItems(
267 [ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem])
268 for itm in itmList[:]:
269 dn = itm.dirName()
270 self.project.removeDirectory(dn)
271
272 def _deleteDirectory(self):
273 """
274 Protected method to delete the selected directory from the project
275 data area.
276 """
277 itmList = self.getSelectedItems()
278
279 dirs = []
280 fullNames = []
281 for itm in itmList:
282 dn = itm.dirName()
283 fullNames.append(dn)
284 dn = self.project.getRelativePath(dn)
285 dirs.append(dn)
286
287 from UI.DeleteFilesConfirmationDialog import \
288 DeleteFilesConfirmationDialog
289 dlg = DeleteFilesConfirmationDialog(
290 self.parent(),
291 QCoreApplication.translate(
292 "ProjectBaseBrowser", "Delete directories"),
293 QCoreApplication.translate(
294 "ProjectBaseBrowser",
295 "Do you really want to delete these directories from"
296 " the project?"),
297 dirs)
298
299 if dlg.exec_() == QDialog.Accepted:
300 for dn in fullNames:
301 self.project.deleteDirectory(dn)
302
303 def _renameFile(self):
304 """
305 Protected method to rename a file of the project.
306 """
307 itm = self.model().item(self.currentIndex())
308 fn = itm.fileName()
309 self.project.renameFile(fn)
310
311 def _copyToClipboard(self):
312 """
313 Protected method to copy the path of an entry to the clipboard.
314 """
315 itm = self.model().item(self.currentIndex())
316 try:
317 fn = itm.fileName()
318 except AttributeError:
319 try:
320 fn = itm.dirName()
321 except AttributeError:
322 fn = ""
323
324 cb = QApplication.clipboard()
325 cb.setText(fn)
326
327 def selectFile(self, fn):
328 """
329 Public method to highlight a node given its filename.
330
331 @param fn filename of file to be highlighted (string)
332 """
333 newfn = os.path.abspath(fn)
334 newfn = self.project.getRelativePath(newfn)
335 sindex = self._model.itemIndexByName(newfn)
336 if sindex.isValid():
337 index = self.model().mapFromSource(sindex)
338 if index.isValid():
339 self._selectSingleItem(index)
340 self.scrollTo(index, QAbstractItemView.PositionAtTop)
341
342 def selectFileLine(self, fn, lineno):
343 """
344 Public method to highlight a node given its filename.
345
346 @param fn filename of file to be highlighted (string)
347 @param lineno one based line number of the item (integer)
348 """
349 newfn = os.path.abspath(fn)
350 newfn = self.project.getRelativePath(newfn)
351 sindex = self._model.itemIndexByNameAndLine(newfn, lineno)
352 if sindex.isValid():
353 index = self.model().mapFromSource(sindex)
354 if index.isValid():
355 self._selectSingleItem(index)
356 self.scrollTo(index)
357
358 def _expandAllDirs(self):
359 """
360 Protected slot to handle the 'Expand all directories' menu action.
361 """
362 self._disconnectExpandedCollapsed()
363 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
364 QApplication.processEvents()
365 index = self.model().index(0, 0)
366 while index.isValid():
367 itm = self.model().item(index)
368 if (isinstance(itm, ProjectBrowserSimpleDirectoryItem) or
369 isinstance(itm, ProjectBrowserDirectoryItem)) and \
370 not self.isExpanded(index):
371 self.expand(index)
372 index = self.indexBelow(index)
373 self.layoutDisplay()
374 self._connectExpandedCollapsed()
375 QApplication.restoreOverrideCursor()
376
377 def _collapseAllDirs(self):
378 """
379 Protected slot to handle the 'Collapse all directories' menu action.
380 """
381 self._disconnectExpandedCollapsed()
382 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
383 QApplication.processEvents()
384 # step 1: find last valid index
385 vindex = QModelIndex()
386 index = self.model().index(0, 0)
387 while index.isValid():
388 vindex = index
389 index = self.indexBelow(index)
390
391 # step 2: go up collapsing all directory items
392 index = vindex
393 while index.isValid():
394 itm = self.model().item(index)
395 if (isinstance(itm, ProjectBrowserSimpleDirectoryItem) or
396 isinstance(itm, ProjectBrowserDirectoryItem)) and \
397 self.isExpanded(index):
398 self.collapse(index)
399 index = self.indexAbove(index)
400 self.layoutDisplay()
401 self._connectExpandedCollapsed()
402 QApplication.restoreOverrideCursor()
403
404 def _showContextMenu(self, menu):
405 """
406 Protected slot called before the context menu is shown.
407
408 It enables/disables the VCS menu entries depending on the overall
409 VCS status and the file status.
410
411 @param menu reference to the menu to be shown (QMenu)
412 """
413 if self.project.vcs is None:
414 for act in self.menuActions:
415 act.setEnabled(True)
416 else:
417 self.vcsHelper.showContextMenu(menu, self.menuActions)
418
419 def _showContextMenuMulti(self, menu):
420 """
421 Protected slot called before the context menu (multiple selections) is
422 shown.
423
424 It enables/disables the VCS menu entries depending on the overall
425 VCS status and the files status.
426
427 @param menu reference to the menu to be shown (QMenu)
428 """
429 if self.project.vcs is None:
430 for act in self.multiMenuActions:
431 act.setEnabled(True)
432 else:
433 self.vcsHelper.showContextMenuMulti(menu, self.multiMenuActions)
434
435 def _showContextMenuDir(self, menu):
436 """
437 Protected slot called before the context menu is shown.
438
439 It enables/disables the VCS menu entries depending on the overall
440 VCS status and the directory status.
441
442 @param menu reference to the menu to be shown (QMenu)
443 """
444 if self.project.vcs is None:
445 for act in self.dirMenuActions:
446 act.setEnabled(True)
447 else:
448 self.vcsHelper.showContextMenuDir(menu, self.dirMenuActions)
449
450 def _showContextMenuDirMulti(self, menu):
451 """
452 Protected slot called before the context menu is shown.
453
454 It enables/disables the VCS menu entries depending on the overall
455 VCS status and the directory status.
456
457 @param menu reference to the menu to be shown (QMenu)
458 """
459 if self.project.vcs is None:
460 for act in self.dirMultiMenuActions:
461 act.setEnabled(True)
462 else:
463 self.vcsHelper.showContextMenuDirMulti(
464 menu, self.dirMultiMenuActions)
465
466 def _showContextMenuBack(self, menu):
467 """
468 Protected slot called before the context menu is shown.
469
470 @param menu reference to the menu to be shown (QMenu)
471 """
472 # nothing to do for now
473 return
474
475 def _selectEntries(self, local=True, filterList=None):
476 """
477 Protected method to select entries based on their VCS status.
478
479 @param local flag indicating local (i.e. non VCS controlled)
480 file/directory entries should be selected (boolean)
481 @param filterList list of classes to check against
482 """
483 if self.project.vcs is None:
484 return
485
486 if local:
487 compareString = \
488 QCoreApplication.translate('ProjectBaseBrowser', "local")
489 else:
490 compareString = self.project.vcs.vcsName()
491
492 # expand all directories in order to iterate over all entries
493 self._expandAllDirs()
494
495 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
496 QApplication.processEvents()
497 self.selectionModel().clear()
498 QApplication.processEvents()
499
500 # now iterate over all entries
501 startIndex = None
502 endIndex = None
503 selectedEntries = 0
504 index = self.model().index(0, 0)
505 while index.isValid():
506 itm = self.model().item(index)
507 if self.wantedItem(itm, filterList) and \
508 compareString == itm.data(1):
509 if startIndex is not None and \
510 startIndex.parent() != index.parent():
511 self._setItemRangeSelected(startIndex, endIndex, True)
512 startIndex = None
513 selectedEntries += 1
514 if startIndex is None:
515 startIndex = index
516 endIndex = index
517 else:
518 if startIndex is not None:
519 self._setItemRangeSelected(startIndex, endIndex, True)
520 startIndex = None
521 index = self.indexBelow(index)
522 if startIndex is not None:
523 self._setItemRangeSelected(startIndex, endIndex, True)
524 QApplication.restoreOverrideCursor()
525 QApplication.processEvents()
526
527 if selectedEntries == 0:
528 E5MessageBox.information(
529 self,
530 QCoreApplication.translate(
531 'ProjectBaseBrowser', "Select entries"),
532 QCoreApplication.translate(
533 'ProjectBaseBrowser',
534 """There were no matching entries found."""))
535
536 def selectLocalEntries(self):
537 """
538 Public slot to handle the select local files context menu entries.
539 """
540 self._selectEntries(local=True, filterList=[ProjectBrowserFileItem])
541
542 def selectVCSEntries(self):
543 """
544 Public slot to handle the select VCS files context menu entries.
545 """
546 self._selectEntries(local=False, filterList=[ProjectBrowserFileItem])
547
548 def selectLocalDirEntries(self):
549 """
550 Public slot to handle the select local directories context menu
551 entries.
552 """
553 self._selectEntries(
554 local=True,
555 filterList=[ProjectBrowserSimpleDirectoryItem,
556 ProjectBrowserDirectoryItem])
557
558 def selectVCSDirEntries(self):
559 """
560 Public slot to handle the select VCS directories context menu entries.
561 """
562 self._selectEntries(
563 local=False,
564 filterList=[ProjectBrowserSimpleDirectoryItem,
565 ProjectBrowserDirectoryItem])
566
567 def getExpandedItemNames(self):
568 """
569 Public method to get the file/directory names of all expanded items.
570
571 @return list of expanded items names (list of string)
572 """
573 expandedNames = []
574
575 childIndex = self.model().index(0, 0)
576 while childIndex.isValid():
577 if self.isExpanded(childIndex):
578 try:
579 expandedNames.append(
580 self.model().item(childIndex).name())
581 except AttributeError:
582 # only items defining the name() method are returned
583 pass
584 childIndex = self.indexBelow(childIndex)
585
586 return expandedNames
587
588 def expandItemsByName(self, names):
589 """
590 Public method to expand items given their names.
591
592 @param names list of item names to be expanded (list of string)
593 """
594 model = self.model()
595 for name in names:
596 childIndex = model.index(0, 0)
597 while childIndex.isValid():
598 try:
599 if model.item(childIndex).name() == name:
600 self.setExpanded(childIndex, True)
601 break
602 except AttributeError:
603 # ignore items not supporting this method
604 pass
605 childIndex = self.indexBelow(childIndex)
606
607 def _prepareRepopulateItem(self, name):
608 """
609 Protected slot to handle the prepareRepopulateItem signal.
610
611 @param name relative name of file item to be repopulated (string)
612 """
613 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
614 QApplication.processEvents()
615 itm = self.currentItem()
616 if itm is not None:
617 self.currentItemName = itm.data(0)
618 self.expandedNames = []
619 sindex = self._model.itemIndexByName(name)
620 if not sindex.isValid():
621 return
622
623 index = self.model().mapFromSource(sindex)
624 if not index.isValid():
625 return
626
627 childIndex = self.indexBelow(index)
628 while childIndex.isValid():
629 if childIndex.parent() == index.parent():
630 break
631 if self.isExpanded(childIndex):
632 self.expandedNames.append(
633 self.model().item(childIndex).data(0))
634 childIndex = self.indexBelow(childIndex)
635
636 def _completeRepopulateItem(self, name):
637 """
638 Protected slot to handle the completeRepopulateItem signal.
639
640 @param name relative name of file item to be repopulated (string)
641 """
642 sindex = self._model.itemIndexByName(name)
643 if sindex.isValid():
644 index = self.model().mapFromSource(sindex)
645 if index.isValid():
646 if self.isExpanded(index):
647 childIndex = self.indexBelow(index)
648 while childIndex.isValid():
649 if not childIndex.isValid() or \
650 childIndex.parent() == index.parent():
651 break
652 itm = self.model().item(childIndex)
653 if itm is not None:
654 itemData = itm.data(0)
655 if self.currentItemName and \
656 self.currentItemName == itemData:
657 self._selectSingleItem(childIndex)
658 if itemData in self.expandedNames:
659 self.setExpanded(childIndex, True)
660 childIndex = self.indexBelow(childIndex)
661 else:
662 self._selectSingleItem(index)
663 self.expandedNames = []
664 self.currentItemName = None
665 QApplication.restoreOverrideCursor()
666 QApplication.processEvents()
667 self._resort()
668
669 def currentItem(self):
670 """
671 Public method to get a reference to the current item.
672
673 @return reference to the current item
674 """
675 itm = self.model().item(self.currentIndex())
676 return itm
677
678 def _keyboardSearchType(self, item):
679 """
680 Protected method to check, if the item is of the correct type.
681
682 @param item reference to the item
683 @type BrowserItem
684 @return flag indicating a correct type
685 @rtype bool
686 """
687 return isinstance(
688 item, (BrowserDirectoryItem, BrowserFileItem,
689 ProjectBrowserSimpleDirectoryItem,
690 ProjectBrowserDirectoryItem, ProjectBrowserFileItem))
691
692 ###########################################################################
693 ## Support for hooks below
694 ###########################################################################
695
696 def _initHookMethods(self):
697 """
698 Protected method to initialize the hooks dictionary.
699
700 This method should be overridden by subclasses. All supported
701 hook methods should be initialized with a None value. The keys
702 must be strings.
703 """
704 self.hooks = {}
705
706 def __checkHookKey(self, key):
707 """
708 Private method to check a hook key.
709
710 @param key key of the hook to check (string)
711 @exception KeyError raised to indicate an invalid hook
712 """
713 if len(self.hooks) == 0:
714 raise KeyError("Hooks are not initialized.")
715
716 if key not in self.hooks:
717 raise KeyError(key)
718
719 def addHookMethod(self, key, method):
720 """
721 Public method to add a hook method to the dictionary.
722
723 @param key for the hook method (string)
724 @param method reference to the hook method (method object)
725 """
726 self.__checkHookKey(key)
727 self.hooks[key] = method
728
729 def addHookMethodAndMenuEntry(self, key, method, menuEntry):
730 """
731 Public method to add a hook method to the dictionary.
732
733 @param key for the hook method (string)
734 @param method reference to the hook method (method object)
735 @param menuEntry entry to be shown in the context menu (string)
736 """
737 self.addHookMethod(key, method)
738 self.hooksMenuEntries[key] = menuEntry
739
740 def removeHookMethod(self, key):
741 """
742 Public method to remove a hook method from the dictionary.
743
744 @param key for the hook method (string)
745 """
746 self.__checkHookKey(key)
747 self.hooks[key] = None
748 if key in self.hooksMenuEntries:
749 del self.hooksMenuEntries[key]
750
751 ##################################################################
752 ## Configure method below
753 ##################################################################
754
755 def _configure(self):
756 """
757 Protected method to open the configuration dialog.
758 """
759 e5App().getObject("UserInterface")\
760 .showPreferences("projectBrowserPage")

eric ide

mercurial