src/eric7/Project/ProjectBaseBrowser.py

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

eric ide

mercurial