eric7/VCS/StatusWidget.py

branch
eric7
changeset 8621
8c9f41115c04
parent 8620
84f7f7867b5f
child 8622
149d51870ce8
--- 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)

eric ide

mercurial