1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2011 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing a dialog showing signed changesets.
8"""
9
10import re
11
12from PyQt5.QtCore import pyqtSlot, Qt, QCoreApplication
13from PyQt5.QtWidgets import (
14    QDialog, QDialogButtonBox, QHeaderView, QTreeWidgetItem
15)
16
17from .Ui_HgGpgSignaturesDialog import Ui_HgGpgSignaturesDialog
18
19
20class HgGpgSignaturesDialog(QDialog, Ui_HgGpgSignaturesDialog):
21    """
22    Class implementing a dialog showing signed changesets.
23    """
24    def __init__(self, vcs, parent=None):
25        """
26        Constructor
27
28        @param vcs reference to the vcs object
29        @param parent reference to the parent widget (QWidget)
30        """
31        super().__init__(parent)
32        self.setupUi(self)
33        self.setWindowFlags(Qt.WindowType.Window)
34
35        self.buttonBox.button(
36            QDialogButtonBox.StandardButton.Close).setEnabled(False)
37        self.buttonBox.button(
38            QDialogButtonBox.StandardButton.Cancel).setDefault(True)
39
40        self.vcs = vcs
41        self.__hgClient = vcs.getClient()
42
43        self.show()
44        QCoreApplication.processEvents()
45
46    def closeEvent(self, e):
47        """
48        Protected slot implementing a close event handler.
49
50        @param e close event (QCloseEvent)
51        """
52        if self.__hgClient.isExecuting():
53            self.__hgClient.cancel()
54
55        e.accept()
56
57    def start(self):
58        """
59        Public slot to start the list command.
60        """
61        self.errorGroup.hide()
62
63        self.intercept = False
64        self.activateWindow()
65
66        args = self.vcs.initCommand("sigs")
67
68        out, err = self.__hgClient.runcommand(args)
69        if err:
70            self.__showError(err)
71        if out:
72            for line in out.splitlines():
73                self.__processOutputLine(line)
74                if self.__hgClient.wasCanceled():
75                    break
76        self.__finish()
77
78    def __finish(self):
79        """
80        Private slot called when the process finished or the user pressed
81        the button.
82        """
83        self.buttonBox.button(
84            QDialogButtonBox.StandardButton.Close).setEnabled(True)
85        self.buttonBox.button(
86            QDialogButtonBox.StandardButton.Cancel).setEnabled(False)
87        self.buttonBox.button(
88            QDialogButtonBox.StandardButton.Close).setDefault(True)
89        self.buttonBox.button(
90            QDialogButtonBox.StandardButton.Close).setFocus(
91                Qt.FocusReason.OtherFocusReason)
92
93        if self.signaturesList.topLevelItemCount() == 0:
94            # no patches present
95            self.__generateItem("", "", self.tr("no signatures found"))
96        self.__resizeColumns()
97        self.__resort()
98
99    def on_buttonBox_clicked(self, button):
100        """
101        Private slot called by a button of the button box clicked.
102
103        @param button button that was clicked (QAbstractButton)
104        """
105        if button == self.buttonBox.button(
106            QDialogButtonBox.StandardButton.Close
107        ):
108            self.close()
109        elif button == self.buttonBox.button(
110            QDialogButtonBox.StandardButton.Cancel
111        ):
112            self.__hgClient.cancel()
113
114    def __resort(self):
115        """
116        Private method to resort the tree.
117        """
118        self.signaturesList.sortItems(
119            self.signaturesList.sortColumn(),
120            self.signaturesList.header().sortIndicatorOrder())
121
122    def __resizeColumns(self):
123        """
124        Private method to resize the list columns.
125        """
126        self.signaturesList.header().resizeSections(
127            QHeaderView.ResizeMode.ResizeToContents)
128        self.signaturesList.header().setStretchLastSection(True)
129
130    def __generateItem(self, revision, changeset, signature):
131        """
132        Private method to generate a patch item in the list of patches.
133
134        @param revision revision number (string)
135        @param changeset changeset of the bookmark (string)
136        @param signature signature of the changeset (string)
137        """
138        if revision == "" and changeset == "":
139            QTreeWidgetItem(self.signaturesList, [signature])
140        else:
141            revString = "{0:>7}:{1}".format(revision, changeset)
142            topItems = self.signaturesList.findItems(
143                revString, Qt.MatchFlag.MatchExactly)
144            if len(topItems) == 0:
145                # first signature for this changeset
146                topItm = QTreeWidgetItem(self.signaturesList, [
147                    "{0:>7}:{1}".format(revision, changeset)])
148                topItm.setExpanded(True)
149                font = topItm.font(0)
150                font.setBold(True)
151                topItm.setFont(0, font)
152            else:
153                topItm = topItems[0]
154            QTreeWidgetItem(topItm, [signature])
155
156    def __processOutputLine(self, line):
157        """
158        Private method to process the lines of output.
159
160        @param line output line to be processed (string)
161        """
162        li = line.split()
163        if li[-1][0] in "1234567890":
164            # last element is a rev:changeset
165            rev, changeset = li[-1].split(":", 1)
166            del li[-1]
167            signature = " ".join(li)
168            self.__generateItem(rev, changeset, signature)
169
170    def __showError(self, out):
171        """
172        Private slot to show some error.
173
174        @param out error to be shown (string)
175        """
176        self.errorGroup.show()
177        self.errors.insertPlainText(out)
178        self.errors.ensureCursorVisible()
179
180    @pyqtSlot()
181    def on_signaturesList_itemSelectionChanged(self):
182        """
183        Private slot handling changes of the selection.
184        """
185        selectedItems = self.signaturesList.selectedItems()
186        if (
187            len(selectedItems) == 1 and
188            self.signaturesList.indexOfTopLevelItem(selectedItems[0]) != -1
189        ):
190            self.verifyButton.setEnabled(True)
191        else:
192            self.verifyButton.setEnabled(False)
193
194    @pyqtSlot()
195    def on_verifyButton_clicked(self):
196        """
197        Private slot to verify the signatures of the selected revision.
198        """
199        rev = (
200            self.signaturesList.selectedItems()[0].text(0)
201            .split(":")[0].strip()
202        )
203        self.vcs.getExtensionObject("gpg").hgGpgVerifySignatures(rev)
204
205    @pyqtSlot(int)
206    def on_categoryCombo_activated(self, index):
207        """
208        Private slot called, when a new filter category is selected.
209
210        @param index index of the selected entry
211        @type int
212        """
213        self.__filterSignatures()
214
215    @pyqtSlot(str)
216    def on_rxEdit_textChanged(self, txt):
217        """
218        Private slot called, when a filter expression is entered.
219
220        @param txt filter expression (string)
221        """
222        self.__filterSignatures()
223
224    def __filterSignatures(self):
225        """
226        Private method to filter the log entries.
227        """
228        searchRxText = self.rxEdit.text()
229        filterTop = self.categoryCombo.currentText() == self.tr("Revision")
230        searchRx = (
231            re.compile(
232                r"^\s*{0}".format(searchRxText[1:]), re.IGNORECASE)
233            if filterTop and searchRxText.startswith("^") else
234            re.compile(searchRxText, re.IGNORECASE)
235        )
236        for topIndex in range(self.signaturesList.topLevelItemCount()):
237            topLevelItem = self.signaturesList.topLevelItem(topIndex)
238            if filterTop:
239                topLevelItem.setHidden(
240                    searchRx.search(topLevelItem.text(0)) is None)
241            else:
242                visibleChildren = topLevelItem.childCount()
243                for childIndex in range(topLevelItem.childCount()):
244                    childItem = topLevelItem.child(childIndex)
245                    if searchRx.search(childItem.text(0)) is None:
246                        childItem.setHidden(True)
247                        visibleChildren -= 1
248                    else:
249                        childItem.setHidden(False)
250                topLevelItem.setHidden(visibleChildren == 0)
251