--- a/eric7/VCS/StatusWidget.py Mon Sep 20 19:47:18 2021 +0200 +++ b/eric7/VCS/StatusWidget.py Tue Sep 21 19:11:31 2021 +0200 @@ -13,7 +13,7 @@ from PyQt6.QtCore import pyqtSlot, Qt from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QListView, - QListWidget, QListWidgetItem, QToolButton, QAbstractItemView + QListWidget, QListWidgetItem, QToolButton, QAbstractItemView, QMenu ) from EricWidgets.EricApplication import ericApp @@ -21,6 +21,7 @@ import Preferences import UI.PixmapCache +import Utilities class StatusWidget(QWidget): @@ -29,12 +30,14 @@ """ StatusDataRole = Qt.ItemDataRole.UserRole + 1 - def __init__(self, project, parent=None): + def __init__(self, project, viewmanager, parent=None): """ Constructor @param project reference to the project object @type Project + @param viewmanager reference to the viewmanager object + @type ViewManager @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ @@ -42,6 +45,7 @@ self.setObjectName("VcsStatusWidget") self.__project = project + self.__vm = viewmanager self.__layout = QVBoxLayout() self.__layout.setObjectName("MainLayout") @@ -83,6 +87,15 @@ self.__reloadButton.clicked.connect(self.__reload) self.__topLayout.addWidget(self.__reloadButton) + self.__actionsButton = QToolButton(self) + self.__actionsButton.setIcon( + UI.PixmapCache.getIcon("actionsToolButton")) + self.__actionsButton.setToolTip( + self.tr("Select action from menu")) + self.__actionsButton.setPopupMode( + QToolButton.ToolButtonPopupMode.InstantPopup) + self.__topLayout.addWidget(self.__actionsButton) + self.__layout.addLayout(self.__topLayout) self.__statusList = QListWidget(self) @@ -92,6 +105,8 @@ self.__statusList.setTextElideMode(Qt.TextElideMode.ElideLeft) self.__statusList.setSelectionMode( QAbstractItemView.SelectionMode.ExtendedSelection) + self.__statusList.itemSelectionChanged.connect( + self.__updateButtonStates) self.__layout.addWidget(self.__statusList) self.setLayout(self.__layout) @@ -117,6 +132,10 @@ "!": self.tr("missing"), } + self.__initActionsMenu() + + self.__reset() + if self.__project.isOpen(): self.__projectOpened() else: @@ -129,6 +148,73 @@ self.__project.vcsStatusMonitorAllData.connect( self.__processStatusData) + def __initActionsMenu(self): + """ + Private method to initialize the actions menu. + """ + self.__actionsMenu = QMenu() + self.__actionsMenu.setToolTipsVisible(True) + self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu) + + self.__commitAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("vcsCommit"), + self.tr("Commit"), self.__commit) + self.__commitAct.setToolTip(self.tr("Commit the selected changes")) + self.__commitSelectAct = self.__actionsMenu.addAction( + self.tr("Select all for commit"), self.__commitSelectAll) + self.__commitDeselectAct = self.__actionsMenu.addAction( + self.tr("Unselect all from commit"), self.__commitDeselectAll) + + self.__actionsMenu.addSeparator() + + self.__addAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("vcsAdd"), + self.tr("Add"), self.__addUntracked) + + self.__actionsMenu.addSeparator() + + self.__diffAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("vcsDiff"), + self.tr("Differences"), self.__diff) + self.__diffAct.setToolTip(self.tr( + "Shows the differences of the selected entry in a" + " separate dialog")) + self.__sbsDiffAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("vcsSbsDiff"), + self.tr("Differences Side-By-Side"), self.__sbsDiff) + self.__sbsDiffAct.setToolTip(self.tr( + "Shows the differences of the selected entry side-by-side in" + " a separate dialog")) + + self.__actionsMenu.addSeparator() + + self.__revertAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("vcsRevert"), + self.tr("Revert"), self.__revert) + self.__revertAct.setToolTip(self.tr( + "Reverts the changes of the selected files")) + + self.__actionsMenu.addSeparator() + + self.__forgetAct = self.__actionsMenu.addAction( + self.tr("Forget Missing"), self.__forgetMissing) + self.__forgetAct.setToolTip(self.tr( + "Forgets about the selected missing files")) + self.__restoreAct = self.__actionsMenu.addAction( + self.tr("Restore Missing"), self.__restoreMissing) + self.__restoreAct.setToolTip(self.tr( + "Restores the selected missing files")) + self.__actionsMenu.addSeparator() + + self.__editAct = self.__actionsMenu.addAction( + UI.PixmapCache.getIcon("open"), + self.tr("Edit Conflict"), self.__editConflict) + self.__editAct.setToolTip(self.tr( + "Edit the selected conflicting file")) + # TODO: add menu entry for 'Conflict Resolved' + + self.__actionsButton.setMenu(self.__actionsMenu) + @pyqtSlot() def __projectOpened(self): """ @@ -145,7 +231,7 @@ self.__reloadButton.setEnabled(False) - self.__statusList.clear() + self.__reset() @pyqtSlot(str) def __setInfoText(self, info): @@ -164,6 +250,28 @@ """ self.__project.checkVCSStatus() + def __reset(self): + """ + Private method to reset the widget to default. + """ + self.__statusList.clear() + + self.__commitToggleButton.setEnabled(False) + self.__commitButton.setEnabled(False) + self.__addButton.setEnabled(False) + + def __updateButtonStates(self): + """ + Private method to set the button states depending on the list state. + """ + modified = len(self.__getModifiedItems()) + unversioned = len(self.__getUnversionedItems()) + commitable = len(self.__getCommitableItems()) + + self.__commitToggleButton.setEnabled(modified) + self.__commitButton.setEnabled(commitable) + self.__addButton.setEnabled(unversioned) + @pyqtSlot(dict) def __processStatusData(self, data): """ @@ -187,7 +295,7 @@ @param data dictionary containing the status data @type dict """ - self.__statusList.clear() + self.__reset() for name, status in data.items(): if status: @@ -206,6 +314,8 @@ itm.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) self.__statusList.sortItems(Qt.SortOrder.AscendingOrder) + + self.__updateButtonStates() @pyqtSlot() def __toggleCheckMark(self): @@ -223,6 +333,24 @@ else: itm.setCheckState(Qt.CheckState.Unchecked) + def __setCheckMark(self, checked): + """ + Private method to set or unset all check marks. + + @param checked check mark state to be set + @type bool + """ + for row in range(self.__statusList.count()): + itm = self.__statusList.item(row) + if ( + itm.flags() & Qt.ItemFlag.ItemIsUserCheckable == + Qt.ItemFlag.ItemIsUserCheckable + ): + if checked: + itm.setCheckState(Qt.CheckState.Checked) + else: + itm.setCheckState(Qt.CheckState.Unchecked) + @pyqtSlot() def __commit(self): """ @@ -259,17 +387,28 @@ self.__reload() @pyqtSlot() + def __commitSelectAll(self): + """ + Private slot to select all entries for commit. + """ + self.__setCheckMark(True) + + @pyqtSlot() + def __commitDeselectAll(self): + """ + Private slot to deselect all entries from commit. + """ + self.__setCheckMark(False) + + @pyqtSlot() def __addUntracked(self): """ Private slot to add the selected untracked entries. """ projectPath = self.__project.getProjectPath() - names = [ - os.path.join(projectPath, itm.text()) - for itm in self.__statusList.selectedItems() - if itm.data(self.StatusDataRole) == "?" - ] + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getUnversionedItems()] if not names: EricMessageBox.information( @@ -282,3 +421,239 @@ vcs = self.__project.getVcs() vcs and vcs.vcsAdd(names) self.__reload() + + ########################################################################### + ## Menu handling methods + ########################################################################### + + def __showActionsMenu(self): + """ + Private slot to prepare the actions button menu before it is shown. + """ + modified = len(self.__getSelectedModifiedItems()) + unversioned = len(self.__getUnversionedItems()) + missing = len(self.__getMissingItems()) + commitable = len(self.__getCommitableItems()) + commitableUnselected = len(self.__getCommitableUnselectedItems()) + conflicting = len(self.__getSelectedConflictingItems()) + + self.__addAct.setEnabled(unversioned) + self.__diffAct.setEnabled(modified) + self.__sbsDiffAct.setEnabled(modified == 1) + self.__revertAct.setEnabled(modified) + self.__forgetAct.setEnabled(missing) + self.__restoreAct.setEnabled(missing) + self.__commitAct.setEnabled(commitable) + self.__commitSelectAct.setEnabled(commitableUnselected) + self.__commitDeselectAct.setEnabled(commitable) + self.__editAct.setEnabled(conflicting == 1) + + def __getCommitableItems(self): + """ + Private method to retrieve all entries the user wants to commit. + + @return list of all items, the user has checked + @rtype list of QListWidgetItem + """ + commitableItems = [] + for row in range(self.__statusList.count()): + itm = self.__statusList.item(row) + if ( + itm.checkState() == Qt.CheckState.Checked + ): + commitableItems.append(itm) + return commitableItems + + def __getCommitableUnselectedItems(self): + """ + Private method to retrieve all entries the user may commit but hasn't + selected. + + @return list of all items, the user has checked + @rtype list of QListWidgetItem + """ + items = [] + for row in range(self.__statusList.count()): + itm = self.__statusList.item(row) + if ( + (itm.flags() & Qt.ItemFlag.ItemIsUserCheckable == + Qt.ItemFlag.ItemIsUserCheckable) and + itm.checkState() == Qt.CheckState.Unchecked + ): + items.append(itm) + return items + + def __getModifiedItems(self): + """ + Private method to retrieve all entries, that have a modified status. + + @return list of all items with a modified status + @rtype list of QListWidgetItem + """ + items = [] + for row in range(self.__statusList.count()): + itm = self.__statusList.item(row) + if itm.data(self.StatusDataRole) in "AMOR": + items.append(itm) + return items + + def __getSelectedModifiedItems(self): + """ + Private method to retrieve all selected entries, that have a modified + status. + + @return list of all selected entries with a modified status + @rtype list of QListWidgetItem + """ + return [itm for itm in self.__statusList.selectedItems() + if itm.data(self.StatusDataRole) in "AMOR"] + + def __getUnversionedItems(self): + """ + Private method to retrieve all entries, that have an unversioned + status. + + @return list of all items with an unversioned status + @rtype list of QListWidgetItem + """ + return [itm for itm in self.__statusList.selectedItems() + if itm.data(self.StatusDataRole) == "?"] + + def __getMissingItems(self): + """ + Private method to retrieve all entries, that have a missing status. + + @return list of all items with a missing status + @rtype list of QListWidgetItem + """ + return [itm for itm in self.__statusList.selectedItems() + if itm.data(self.StatusDataRole) == "!"] + + def __getSelectedConflictingItems(self): + """ + Private method to retrieve all selected entries, that have a conflict + status. + + @return list of all selected entries with a conflict status + @rtype list of QListWidgetItem + """ + return [itm for itm in self.__statusList.selectedItems() + if itm.data(self.StatusDataRole) == "Z"] + + @pyqtSlot() + def __diff(self): + """ + Private slot to handle the Diff action menu entry. + """ + projectPath = self.__project.getProjectPath() + + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getSelectedModifiedItems()] + if not names: + EricMessageBox.information( + self, + self.tr("Differences"), + self.tr("""There are no uncommitted changes""" + """ available/selected.""")) + return + + vcs = self.__project.getVcs() + vcs and vcs.vcsDiff(names) + + @pyqtSlot() + def __sbsDiff(self): + """ + Private slot to handle the Side-By-Side Diff action menu entry. + """ + projectPath = self.__project.getProjectPath() + + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getSelectedModifiedItems()] + if not names: + EricMessageBox.information( + self, + self.tr("Differences Side-By-Side"), + self.tr("""There are no uncommitted changes""" + """ available/selected.""")) + return + elif len(names) > 1: + EricMessageBox.information( + self, + self.tr("Differences Side-By-Side"), + self.tr("""Only one file with uncommitted changes""" + """ must be selected.""")) + return + + vcs = self.__project.getVcs() + vcs and vcs.vcsSbsDiff(names[0]) + + def __revert(self): + """ + Private slot to handle the Revert action menu entry. + """ + projectPath = self.__project.getProjectPath() + + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getSelectedModifiedItems()] + if not names: + EricMessageBox.information( + self, + self.tr("Revert"), + self.tr("""There are no uncommitted changes""" + """ available/selected.""")) + return + + vcs = self.__project.getVcs() + vcs and vcs.vcsRevert(names) + self.__reload() + + def __forgetMissing(self): + """ + Private slot to handle the Forget action menu entry. + """ + projectPath = self.__project.getProjectPath() + + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getMissingItems()] + if not names: + EricMessageBox.information( + self, + self.tr("Forget Missing"), + self.tr("""There are no missing entries""" + """ available/selected.""")) + return + + vcs = self.__project.getVcs() + vcs and vcs.vcsForget(names) + self.__reload() + + def __restoreMissing(self): + """ + Private slot to handle the Restore Missing context menu entry. + """ + projectPath = self.__project.getProjectPath() + + names = [os.path.join(projectPath, itm.text()) + for itm in self.__getMissingItems()] + if not names: + EricMessageBox.information( + self, + self.tr("Revert Missing"), + self.tr("""There are no missing entries""" + """ available/selected.""")) + return + + vcs = self.__project.getVcs() + vcs and vcs.vcsRevert(names) + self.__reload() + + def __editConflict(self): + """ + Private slot to handle the Edit Conflict action menu entry. + """ + projectPath = self.__project.getProjectPath() + + itm = self.__getSelectedConflictingItems()[0] + filename = os.path.join(projectPath, itm.text()) + if Utilities.MimeTypes.isTextFile(filename): + self.__vm.getEditor(filename)