Mon, 07 Nov 2022 17:19:58 +0100
Corrected/acknowledged some bad import style and removed some obsolete code.
# -*- coding: utf-8 -*- # Copyright (c) 2002 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the baseclass for the various project browsers. """ import contextlib import os from PyQt6.QtCore import ( QCoreApplication, QElapsedTimer, QItemSelection, QItemSelectionModel, QModelIndex, Qt, pyqtSignal, ) from PyQt6.QtWidgets import QAbstractItemView, QApplication, QDialog, QMenu, QTreeView from eric7.EricGui.EricOverrideCursor import EricOverrideCursor from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.UI.Browser import Browser from eric7.UI.BrowserModel import BrowserDirectoryItem, BrowserFileItem from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog from .ProjectBrowserModel import ( ProjectBrowserDirectoryItem, ProjectBrowserFileItem, ProjectBrowserSimpleDirectoryItem, ) from .ProjectBrowserSortFilterProxyModel import ProjectBrowserSortFilterProxyModel class ProjectBaseBrowser(Browser): """ Baseclass implementing common functionality for the various project browsers. @signal closeSourceWindow(str) emitted to close a source file """ closeSourceWindow = pyqtSignal(str) def __init__(self, project, type_, parent=None): """ Constructor @param project reference to the project object @param type_ project browser type (string) @param parent parent widget of this browser """ QTreeView.__init__(self, parent) self.project = project self._model = project.getModel() self._sortModel = ProjectBrowserSortFilterProxyModel(type_) self._sortModel.setSourceModel(self._model) self.setModel(self._sortModel) self.selectedItemsFilter = [ProjectBrowserFileItem] # contains codes for special menu entries # 1 = specials for Others browser self.specialMenuEntries = [] self.isTranslationsBrowser = False self.expandedNames = [] self.SelectFlags = ( QItemSelectionModel.SelectionFlag.Select | QItemSelectionModel.SelectionFlag.Rows ) self.DeselectFlags = ( QItemSelectionModel.SelectionFlag.Deselect | QItemSelectionModel.SelectionFlag.Rows ) self._activating = False self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self._contextMenuRequested) self.activated.connect(self._openItem) self._model.rowsInserted.connect(self.__modelRowsInserted) self._connectExpandedCollapsed() self._createPopupMenus() self.currentItemName = None self._init() # perform common initialization tasks self._keyboardSearchString = "" self._keyboardSearchTimer = QElapsedTimer() self._keyboardSearchTimer.invalidate() self._initHookMethods() # perform initialization of the hooks self.hooksMenuEntries = {} def _connectExpandedCollapsed(self): """ Protected method to connect the expanded and collapsed signals. """ self.expanded.connect(self._resizeColumns) self.collapsed.connect(self._resizeColumns) def _disconnectExpandedCollapsed(self): """ Protected method to disconnect the expanded and collapsed signals. """ self.expanded.disconnect(self._resizeColumns) self.collapsed.disconnect(self._resizeColumns) def _createPopupMenus(self): """ Protected overloaded method to generate the popup menus. """ # create the popup menu for source files self.sourceMenu = QMenu(self) self.sourceMenu.addAction( QCoreApplication.translate("ProjectBaseBrowser", "Open"), self._openItem ) # create the popup menu for general use self.menu = QMenu(self) self.menu.addAction( QCoreApplication.translate("ProjectBaseBrowser", "Open"), self._openItem ) # create the menu for multiple selected files self.multiMenu = QMenu(self) self.multiMenu.addAction( QCoreApplication.translate("ProjectBaseBrowser", "Open"), self._openItem ) # create the background menu self.backMenu = None # create the directories menu self.dirMenu = None # create the directory for multiple selected directories self.dirMultiMenu = None self.menuActions = [] self.multiMenuActions = [] self.dirMenuActions = [] self.dirMultiMenuActions = [] self.mainMenu = None def _contextMenuRequested(self, coord): """ Protected slot to show the context menu. @param coord the position of the mouse pointer (QPoint) """ if not self.project.isOpen(): return cnt = self.getSelectedItemsCount() if cnt > 1: self.multiMenu.popup(self.mapToGlobal(coord)) else: index = self.indexAt(coord) if index.isValid(): self.menu.popup(self.mapToGlobal(coord)) else: self.backMenu and self.backMenu.popup(self.mapToGlobal(coord)) def _selectSingleItem(self, index): """ Protected method to select a single item. @param index index of item to be selected (QModelIndex) """ if index.isValid(): self.setCurrentIndex(index) self.selectionModel().select( index, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows, ) def _setItemSelected(self, index, selected): """ Protected method to set the selection status of an item. @param index index of item to set (QModelIndex) @param selected flag giving the new selection status (boolean) """ if index.isValid(): self.selectionModel().select( index, selected and self.SelectFlags or self.DeselectFlags ) def _setItemRangeSelected(self, startIndex, endIndex, selected): """ Protected method to set the selection status of a range of items. @param startIndex start index of range of items to set (QModelIndex) @param endIndex end index of range of items to set (QModelIndex) @param selected flag giving the new selection status (boolean) """ selection = QItemSelection(startIndex, endIndex) self.selectionModel().select( selection, selected and self.SelectFlags or self.DeselectFlags ) def __modelRowsInserted(self, parent, start, end): """ Private slot called after rows have been inserted into the model. @param parent parent index of inserted rows (QModelIndex) @param start start row number (integer) @param end end row number (integer) """ self._resizeColumns(parent) def _projectClosed(self): """ Protected slot to handle the projectClosed signal. """ self.layoutDisplay() if self.backMenu is not None: self.backMenu.setEnabled(False) self._createPopupMenus() def _projectOpened(self): """ Protected slot to handle the projectOpened signal. """ self.layoutDisplay() self.sortByColumn(0, Qt.SortOrder.DescendingOrder) self.sortByColumn(0, Qt.SortOrder.AscendingOrder) self._initMenusAndVcs() def _initMenusAndVcs(self): """ Protected slot to initialize the menus and the Vcs interface. """ self._createPopupMenus() if self.backMenu is not None: self.backMenu.setEnabled(True) if self.project.vcs is not None: self.vcsHelper = self.project.vcs.vcsGetProjectBrowserHelper( self, self.project, self.isTranslationsBrowser ) self.vcsHelper.addVCSMenus( self.mainMenu, self.multiMenu, self.backMenu, self.dirMenu, self.dirMultiMenu, ) def _newProject(self): """ Protected slot to handle the newProject signal. """ # default to perform same actions as opening a project self._projectOpened() def _removeFile(self): """ Protected method to remove a file or files from the project. """ itmList = self.getSelectedItems() for itm in itmList[:]: fn = itm.fileName() self.closeSourceWindow.emit(fn) self.project.removeFile(fn) def _removeDir(self): """ Protected method to remove a (single) directory from the project. """ itmList = self.getSelectedItems( [ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem] ) for itm in itmList[:]: dn = itm.dirName() self.project.removeDirectory(dn) def _deleteDirectory(self): """ Protected method to delete the selected directory from the project data area. """ itmList = self.getSelectedItems() dirs = [] fullNames = [] for itm in itmList: dn = itm.dirName() fullNames.append(dn) dn = self.project.getRelativePath(dn) dirs.append(dn) dlg = DeleteFilesConfirmationDialog( self.parent(), QCoreApplication.translate("ProjectBaseBrowser", "Delete directories"), QCoreApplication.translate( "ProjectBaseBrowser", "Do you really want to delete these directories from" " the project?", ), dirs, ) if dlg.exec() == QDialog.DialogCode.Accepted: for dn in fullNames: self.project.deleteDirectory(dn) def _renameFile(self): """ Protected method to rename a file of the project. """ itm = self.model().item(self.currentIndex()) fn = itm.fileName() self.project.renameFile(fn) def _copyToClipboard(self): """ Protected method to copy the path of an entry to the clipboard. """ itm = self.model().item(self.currentIndex()) try: fn = itm.fileName() except AttributeError: try: fn = itm.dirName() except AttributeError: fn = "" cb = QApplication.clipboard() cb.setText(fn) def selectFile(self, fn): """ Public method to highlight a node given its filename. @param fn filename of file to be highlighted (string) """ newfn = os.path.abspath(fn) newfn = self.project.getRelativePath(newfn) sindex = self._model.itemIndexByName(newfn) if sindex.isValid(): index = self.model().mapFromSource(sindex) if index.isValid(): self._selectSingleItem(index) self.scrollTo(index, QAbstractItemView.ScrollHint.PositionAtTop) def selectFileLine(self, fn, lineno): """ Public method to highlight a node given its filename. @param fn filename of file to be highlighted (string) @param lineno one based line number of the item (integer) """ newfn = os.path.abspath(fn) newfn = self.project.getRelativePath(newfn) sindex = self._model.itemIndexByNameAndLine(newfn, lineno) if sindex.isValid(): index = self.model().mapFromSource(sindex) if index.isValid(): self._selectSingleItem(index) self.scrollTo(index) def _expandAllDirs(self): """ Protected slot to handle the 'Expand all directories' menu action. """ self._disconnectExpandedCollapsed() with EricOverrideCursor(): index = self.model().index(0, 0) while index.isValid(): itm = self.model().item(index) if isinstance( itm, (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem), ) and not self.isExpanded(index): self.expand(index) index = self.indexBelow(index) self.layoutDisplay() self._connectExpandedCollapsed() def _collapseAllDirs(self): """ Protected slot to handle the 'Collapse all directories' menu action. """ self._disconnectExpandedCollapsed() with EricOverrideCursor(): # step 1: find last valid index vindex = QModelIndex() index = self.model().index(0, 0) while index.isValid(): vindex = index index = self.indexBelow(index) # step 2: go up collapsing all directory items index = vindex while index.isValid(): itm = self.model().item(index) if isinstance( itm, (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem), ) and self.isExpanded(index): self.collapse(index) index = self.indexAbove(index) self.layoutDisplay() self._connectExpandedCollapsed() def _showContextMenu(self, menu): """ Protected slot called before the context menu is shown. It enables/disables the VCS menu entries depending on the overall VCS status and the file status. @param menu reference to the menu to be shown (QMenu) """ if self.project.vcs is None: for act in self.menuActions: act.setEnabled(True) else: self.vcsHelper.showContextMenu(menu, self.menuActions) def _showContextMenuMulti(self, menu): """ Protected slot called before the context menu (multiple selections) is shown. It enables/disables the VCS menu entries depending on the overall VCS status and the files status. @param menu reference to the menu to be shown (QMenu) """ if self.project.vcs is None: for act in self.multiMenuActions: act.setEnabled(True) else: self.vcsHelper.showContextMenuMulti(menu, self.multiMenuActions) def _showContextMenuDir(self, menu): """ Protected slot called before the context menu is shown. It enables/disables the VCS menu entries depending on the overall VCS status and the directory status. @param menu reference to the menu to be shown (QMenu) """ if self.project.vcs is None: for act in self.dirMenuActions: act.setEnabled(True) else: self.vcsHelper.showContextMenuDir(menu, self.dirMenuActions) def _showContextMenuDirMulti(self, menu): """ Protected slot called before the context menu is shown. It enables/disables the VCS menu entries depending on the overall VCS status and the directory status. @param menu reference to the menu to be shown (QMenu) """ if self.project.vcs is None: for act in self.dirMultiMenuActions: act.setEnabled(True) else: self.vcsHelper.showContextMenuDirMulti(menu, self.dirMultiMenuActions) def _showContextMenuBack(self, menu): """ Protected slot called before the context menu is shown. @param menu reference to the menu to be shown (QMenu) """ # nothing to do for now return def _selectEntries(self, local=True, filterList=None): """ Protected method to select entries based on their VCS status. @param local flag indicating local (i.e. non VCS controlled) file/directory entries should be selected (boolean) @param filterList list of classes to check against """ if self.project.vcs is None: return compareString = ( QCoreApplication.translate("ProjectBaseBrowser", "local") if local else self.project.vcs.vcsName() ) # expand all directories in order to iterate over all entries self._expandAllDirs() self.selectionModel().clear() with EricOverrideCursor(): # now iterate over all entries startIndex = None endIndex = None selectedEntries = 0 index = self.model().index(0, 0) while index.isValid(): itm = self.model().item(index) if self.wantedItem(itm, filterList) and compareString == itm.data(1): if startIndex is not None and startIndex.parent() != index.parent(): self._setItemRangeSelected(startIndex, endIndex, True) startIndex = None selectedEntries += 1 if startIndex is None: startIndex = index endIndex = index else: if startIndex is not None: self._setItemRangeSelected(startIndex, endIndex, True) startIndex = None index = self.indexBelow(index) if startIndex is not None: self._setItemRangeSelected(startIndex, endIndex, True) if selectedEntries == 0: EricMessageBox.information( self, QCoreApplication.translate("ProjectBaseBrowser", "Select entries"), QCoreApplication.translate( "ProjectBaseBrowser", """There were no matching entries found.""" ), ) def selectLocalEntries(self): """ Public slot to handle the select local files context menu entries. """ self._selectEntries(local=True, filterList=[ProjectBrowserFileItem]) def selectVCSEntries(self): """ Public slot to handle the select VCS files context menu entries. """ self._selectEntries(local=False, filterList=[ProjectBrowserFileItem]) def selectLocalDirEntries(self): """ Public slot to handle the select local directories context menu entries. """ self._selectEntries( local=True, filterList=[ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem], ) def selectVCSDirEntries(self): """ Public slot to handle the select VCS directories context menu entries. """ self._selectEntries( local=False, filterList=[ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem], ) def getExpandedItemNames(self): """ Public method to get the file/directory names of all expanded items. @return list of expanded items names (list of string) """ expandedNames = [] childIndex = self.model().index(0, 0) while childIndex.isValid(): if self.isExpanded(childIndex): with contextlib.suppress(AttributeError): expandedNames.append(self.model().item(childIndex).name()) # only items defining the name() method are returned childIndex = self.indexBelow(childIndex) return expandedNames def expandItemsByName(self, names): """ Public method to expand items given their names. @param names list of item names to be expanded (list of string) """ model = self.model() for name in names: childIndex = model.index(0, 0) while childIndex.isValid(): with contextlib.suppress(AttributeError): if model.item(childIndex).name() == name: self.setExpanded(childIndex, True) break # ignore items not supporting this method childIndex = self.indexBelow(childIndex) def _prepareRepopulateItem(self, name): """ Protected slot to handle the prepareRepopulateItem signal. @param name relative name of file item to be repopulated (string) """ itm = self.currentItem() if itm is not None: self.currentItemName = itm.data(0) self.expandedNames = [] sindex = self._model.itemIndexByName(name) if not sindex.isValid(): return index = self.model().mapFromSource(sindex) if not index.isValid(): return childIndex = self.indexBelow(index) while childIndex.isValid(): if childIndex.parent() == index.parent(): break if self.isExpanded(childIndex): self.expandedNames.append(self.model().item(childIndex).data(0)) childIndex = self.indexBelow(childIndex) def _completeRepopulateItem(self, name): """ Protected slot to handle the completeRepopulateItem signal. @param name relative name of file item to be repopulated (string) """ sindex = self._model.itemIndexByName(name) if sindex.isValid(): index = self.model().mapFromSource(sindex) if index.isValid(): if self.isExpanded(index): childIndex = self.indexBelow(index) while childIndex.isValid(): if ( not childIndex.isValid() or childIndex.parent() == index.parent() ): break itm = self.model().item(childIndex) if itm is not None: itemData = itm.data(0) if ( self.currentItemName and self.currentItemName == itemData ): self._selectSingleItem(childIndex) if itemData in self.expandedNames: self.setExpanded(childIndex, True) childIndex = self.indexBelow(childIndex) else: self._selectSingleItem(index) self.expandedNames = [] self.currentItemName = None self._resort() def currentItem(self): """ Public method to get a reference to the current item. @return reference to the current item """ itm = self.model().item(self.currentIndex()) return itm def _keyboardSearchType(self, item): """ Protected method to check, if the item is of the correct type. @param item reference to the item @type BrowserItem @return flag indicating a correct type @rtype bool """ return isinstance( item, ( BrowserDirectoryItem, BrowserFileItem, ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem, ProjectBrowserFileItem, ), ) ########################################################################### ## Support for hooks below ########################################################################### def _initHookMethods(self): """ Protected method to initialize the hooks dictionary. This method should be overridden by subclasses. All supported hook methods should be initialized with a None value. The keys must be strings. """ self.hooks = {} def __checkHookKey(self, key): """ Private method to check a hook key. @param key key of the hook to check (string) @exception KeyError raised to indicate an invalid hook """ if len(self.hooks) == 0: raise KeyError("Hooks are not initialized.") if key not in self.hooks: raise KeyError(key) def addHookMethod(self, key, method): """ Public method to add a hook method to the dictionary. @param key for the hook method (string) @param method reference to the hook method (method object) """ self.__checkHookKey(key) self.hooks[key] = method def addHookMethodAndMenuEntry(self, key, method, menuEntry): """ Public method to add a hook method to the dictionary. @param key for the hook method (string) @param method reference to the hook method (method object) @param menuEntry entry to be shown in the context menu (string) """ self.addHookMethod(key, method) self.hooksMenuEntries[key] = menuEntry def removeHookMethod(self, key): """ Public method to remove a hook method from the dictionary. @param key for the hook method (string) """ self.__checkHookKey(key) self.hooks[key] = None if key in self.hooksMenuEntries: del self.hooksMenuEntries[key] ################################################################## ## Configure method below ################################################################## def _configure(self): """ Protected method to open the configuration dialog. """ ericApp().getObject("UserInterface").showPreferences("projectBrowserPage")