src/eric7/Plugins/VcsPlugins/vcsSubversion/SvnRepoBrowserDialog.py

Wed, 04 Oct 2023 17:50:59 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 04 Oct 2023 17:50:59 +0200
branch
eric7
changeset 10217
7888177e7463
parent 10069
435cc5875135
child 10438
4cd7e5a8b3cf
permissions
-rw-r--r--

Fixed in issue with several editable combo box selectors, that the value could not be changed if the edited text differed by case only. This was caused by the standard QComboBox completer.

# -*- coding: utf-8 -*-

# Copyright (c) 2007 - 2023 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the subversion repository browser dialog.
"""

import os
import re

from PyQt6.QtCore import QProcess, Qt, QTimer, pyqtSlot
from PyQt6.QtWidgets import (
    QDialog,
    QDialogButtonBox,
    QHeaderView,
    QLineEdit,
    QTreeWidgetItem,
)

from eric7 import Preferences
from eric7.EricGui import EricPixmapCache
from eric7.EricGui.EricOverrideCursor import EricOverrideCursorProcess
from eric7.EricWidgets import EricMessageBox
from eric7.Globals import strToQByteArray

from .Ui_SvnRepoBrowserDialog import Ui_SvnRepoBrowserDialog


class SvnRepoBrowserDialog(QDialog, Ui_SvnRepoBrowserDialog):
    """
    Class implementing the subversion repository browser dialog.
    """

    def __init__(self, vcs, mode="browse", parent=None):
        """
        Constructor

        @param vcs reference to the vcs object
        @param mode mode of the dialog (string, "browse" or "select")
        @param parent parent widget (QWidget)
        """
        super().__init__(parent)
        self.setupUi(self)
        self.setWindowFlags(Qt.WindowType.Window)

        self.repoTree.headerItem().setText(self.repoTree.columnCount(), "")
        self.repoTree.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder)

        self.vcs = vcs
        self.mode = mode

        self.__process = EricOverrideCursorProcess()
        self.__process.finished.connect(self.__procFinished)
        self.__process.readyReadStandardOutput.connect(self.__readStdout)
        self.__process.readyReadStandardError.connect(self.__readStderr)

        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
            self.buttonBox.button(QDialogButtonBox.StandardButton.Close).hide()
        else:
            self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).hide()
            self.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()

        self.__dirIcon = EricPixmapCache.getIcon("dirClosed")
        self.__fileIcon = EricPixmapCache.getIcon("fileMisc")

        self.__urlRole = Qt.ItemDataRole.UserRole
        self.__ignoreExpand = False
        self.intercept = False

        self.__rx_dir = re.compile(
            r"""\s*([0-9]+)\s+(\w+)\s+"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*"""
        )
        self.__rx_file = re.compile(
            r"""\s*([0-9]+)\s+(\w+)\s+([0-9]+)\s"""
            r"""((?:\w+\s+\d+|[0-9.]+\s+\w+)\s+[0-9:]+)\s+(.+)\s*"""
        )

    def closeEvent(self, e):
        """
        Protected slot implementing a close event handler.

        @param e close event (QCloseEvent)
        """
        if (
            self.__process is not None
            and self.__process.state() != QProcess.ProcessState.NotRunning
        ):
            self.__process.terminate()
            QTimer.singleShot(2000, self.__process.kill)
            self.__process.waitForFinished(3000)

        e.accept()

    def __resort(self):
        """
        Private method to resort the tree.
        """
        self.repoTree.sortItems(
            self.repoTree.sortColumn(), self.repoTree.header().sortIndicatorOrder()
        )

    def __resizeColumns(self):
        """
        Private method to resize the tree columns.
        """
        self.repoTree.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
        self.repoTree.header().setStretchLastSection(True)

    def __generateItem(self, repopath, revision, author, size, date, nodekind, url):
        """
        Private method to generate a tree item in the repository tree.

        @param repopath path of the item (string)
        @param revision revision info (string)
        @param author author info (string)
        @param size size info (string)
        @param date date info (string)
        @param nodekind node kind info (string, "dir" or "file")
        @param url url of the entry (string)
        @return reference to the generated item (QTreeWidgetItem)
        """
        rev = "" if revision == "" else int(revision)
        sz = "" if size == "" else int(size)

        itm = QTreeWidgetItem(self.parentItem)
        itm.setData(0, Qt.ItemDataRole.DisplayRole, repopath)
        itm.setData(1, Qt.ItemDataRole.DisplayRole, rev)
        itm.setData(2, Qt.ItemDataRole.DisplayRole, author)
        itm.setData(3, Qt.ItemDataRole.DisplayRole, sz)
        itm.setData(4, Qt.ItemDataRole.DisplayRole, date)

        if nodekind == "dir":
            itm.setIcon(0, self.__dirIcon)
            itm.setChildIndicatorPolicy(
                QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator
            )
        elif nodekind == "file":
            itm.setIcon(0, self.__fileIcon)

        itm.setData(0, self.__urlRole, url)

        itm.setTextAlignment(0, Qt.AlignmentFlag.AlignLeft)
        itm.setTextAlignment(1, Qt.AlignmentFlag.AlignRight)
        itm.setTextAlignment(2, Qt.AlignmentFlag.AlignLeft)
        itm.setTextAlignment(3, Qt.AlignmentFlag.AlignRight)
        itm.setTextAlignment(4, Qt.AlignmentFlag.AlignLeft)

        return itm

    def __repoRoot(self, url):
        """
        Private method to get the repository root using the svn info command.

        @param url the repository URL to browser (string)
        @return repository root (string)
        """
        ioEncoding = Preferences.getSystem("IOEncoding")
        repoRoot = None

        process = QProcess()

        args = []
        args.append("info")
        self.vcs.addArguments(args, self.vcs.options["global"])
        args.append("--xml")
        args.append(url)

        process.start("svn", args)
        procStarted = process.waitForStarted(5000)
        if procStarted:
            finished = process.waitForFinished(30000)
            if finished:
                if process.exitCode() == 0:
                    output = str(process.readAllStandardOutput(), ioEncoding, "replace")
                    for line in output.splitlines():
                        line = line.strip()
                        if line.startswith("<root>"):
                            repoRoot = line.replace("<root>", "").replace("</root>", "")
                            break
                else:
                    error = str(
                        process.readAllStandardError(),
                        Preferences.getSystem("IOEncoding"),
                        "replace",
                    )
                    self.errors.insertPlainText(error)
                    self.errors.ensureCursorVisible()
        else:
            EricMessageBox.critical(
                self,
                self.tr("Process Generation Error"),
                self.tr(
                    "The process {0} could not be started. "
                    "Ensure, that it is in the search path."
                ).format("svn"),
            )
        return repoRoot

    def __listRepo(self, url, parent=None):
        """
        Private method to perform the svn list command.

        @param url the repository URL to browse (string)
        @param parent reference to the item, the data should be appended to
            (QTreeWidget or QTreeWidgetItem)
        """
        self.errorGroup.hide()

        self.repoUrl = url

        if parent is None:
            self.parentItem = self.repoTree
        else:
            self.parentItem = parent

        if self.parentItem == self.repoTree:
            repoRoot = self.__repoRoot(url)
            if repoRoot is None:
                self.__finish()
                return
            self.__ignoreExpand = True
            itm = self.__generateItem(repoRoot, "", "", "", "", "dir", repoRoot)
            itm.setExpanded(True)
            self.parentItem = itm
            urlPart = repoRoot
            for element in url.replace(repoRoot, "").split("/"):
                if element:
                    urlPart = "{0}/{1}".format(urlPart, element)
                    itm = self.__generateItem(element, "", "", "", "", "dir", urlPart)
                    itm.setExpanded(True)
                    self.parentItem = itm
            itm.setExpanded(False)
            self.__ignoreExpand = False
            self.__finish()
            return

        self.intercept = False

        self.__process.kill()

        args = []
        args.append("list")
        self.vcs.addArguments(args, self.vcs.options["global"])
        if "--verbose" not in self.vcs.options["global"]:
            args.append("--verbose")
        args.append(url)

        self.__process.start("svn", args)
        procStarted = self.__process.waitForStarted(5000)
        if not procStarted:
            self.__finish()
            self.inputGroup.setEnabled(False)
            self.inputGroup.hide()
            EricMessageBox.critical(
                self,
                self.tr("Process Generation Error"),
                self.tr(
                    "The process {0} could not be started. "
                    "Ensure, that it is in the search path."
                ).format("svn"),
            )
        else:
            self.inputGroup.setEnabled(True)
            self.inputGroup.show()

    def __normalizeUrl(self, url):
        """
        Private method to normalite the url.

        @param url the url to normalize (string)
        @return normalized URL (string)
        """
        if url.endswith("/"):
            return url[:-1]
        return url

    def start(self, url):
        """
        Public slot to start the svn info command.

        @param url the repository URL to browser (string)
        """
        self.repoTree.clear()

        self.url = ""

        url = self.__normalizeUrl(url)
        if self.urlCombo.findText(url) == -1:
            self.urlCombo.addItem(url)

    @pyqtSlot(int)
    def on_urlCombo_currentIndexChanged(self, index):
        """
        Private slot called, when a new repository URL is entered or selected.

        @param index index of the current item
        @type int
        """
        text = self.urlCombo.itemText(index)
        url = self.__normalizeUrl(text)
        if url != self.url:
            self.url = url
            self.repoTree.clear()
            self.__listRepo(url)

    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemExpanded(self, item):
        """
        Private slot called when an item is expanded.

        @param item reference to the item to be expanded (QTreeWidgetItem)
        """
        if not self.__ignoreExpand:
            url = item.data(0, self.__urlRole)
            self.__listRepo(url, item)

    @pyqtSlot(QTreeWidgetItem)
    def on_repoTree_itemCollapsed(self, item):
        """
        Private slot called when an item is collapsed.

        @param item reference to the item to be collapsed (QTreeWidgetItem)
        """
        for child in item.takeChildren():
            del child

    @pyqtSlot()
    def on_repoTree_itemSelectionChanged(self):
        """
        Private slot called when the selection changes.
        """
        if self.mode == "select":
            self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(True)

    def accept(self):
        """
        Public slot called when the dialog is accepted.
        """
        if self.focusWidget() == self.urlCombo:
            return

        super().accept()

    def getSelectedUrl(self):
        """
        Public method to retrieve the selected repository URL.

        @return the selected repository URL (string)
        """
        items = self.repoTree.selectedItems()
        if len(items) == 1:
            return items[0].data(0, self.__urlRole)
        else:
            return ""

    def __finish(self):
        """
        Private slot called when the process finished or the user pressed the
        button.
        """
        if (
            self.__process is not None
            and self.__process.state() != QProcess.ProcessState.NotRunning
        ):
            self.__process.terminate()
            QTimer.singleShot(2000, self.__process.kill)
            self.__process.waitForFinished(3000)

        self.inputGroup.setEnabled(False)
        self.inputGroup.hide()

        self.__resizeColumns()
        self.__resort()

    @pyqtSlot(int, QProcess.ExitStatus)
    def __procFinished(self, exitCode, exitStatus):
        """
        Private slot connected to the finished signal.

        @param exitCode exit code of the process (integer)
        @param exitStatus exit status of the process (QProcess.ExitStatus)
        """
        self.__finish()

    def __readStdout(self):
        """
        Private slot to handle the readyReadStandardOutput signal.

        It reads the output of the process, formats it and inserts it into
        the contents pane.
        """
        if self.__process is not None:
            self.__process.setReadChannel(QProcess.ProcessChannel.StandardOutput)

            while self.__process.canReadLine():
                s = str(
                    self.__process.readLine(),
                    Preferences.getSystem("IOEncoding"),
                    "replace",
                )
                match = self.__rx_dir.fullmatch(s) or self.__rx_file.fullmatch(s)
                if match is None:
                    continue
                elif match.re is self.__rx_dir:
                    revision = match.group(1)
                    author = match.group(2)
                    date = match.group(3)
                    name = match.group(4).strip()
                    if name.endswith("/"):
                        name = name[:-1]
                    size = ""
                    nodekind = "dir"
                    if name == ".":
                        continue
                elif match.re is self.__rx_file:
                    revision = match.group(1)
                    author = match.group(2)
                    size = match.group(3)
                    date = match.group(4)
                    name = match.group(5).strip()
                    nodekind = "file"

                url = "{0}/{1}".format(self.repoUrl, name)
                self.__generateItem(name, revision, author, size, date, nodekind, url)

    def __readStderr(self):
        """
        Private slot to handle the readyReadStandardError signal.

        It reads the error output of the process and inserts it into the
        error pane.
        """
        if self.__process is not None:
            s = str(
                self.__process.readAllStandardError(),
                Preferences.getSystem("IOEncoding"),
                "replace",
            )
            self.errors.insertPlainText(s)
            self.errors.ensureCursorVisible()
            self.errorGroup.show()

    def on_passwordCheckBox_toggled(self, isOn):
        """
        Private slot to handle the password checkbox toggled.

        @param isOn flag indicating the status of the check box (boolean)
        """
        if isOn:
            self.input.setEchoMode(QLineEdit.EchoMode.Password)
        else:
            self.input.setEchoMode(QLineEdit.EchoMode.Normal)

    @pyqtSlot()
    def on_sendButton_clicked(self):
        """
        Private slot to send the input to the subversion process.
        """
        inputTxt = self.input.text()
        inputTxt += os.linesep

        if self.passwordCheckBox.isChecked():
            self.errors.insertPlainText(os.linesep)
            self.errors.ensureCursorVisible()
        else:
            self.errors.insertPlainText(inputTxt)
            self.errors.ensureCursorVisible()

        self.__process.write(strToQByteArray(inputTxt))

        self.passwordCheckBox.setChecked(False)
        self.input.clear()

    def on_input_returnPressed(self):
        """
        Private slot to handle the press of the return key in the input field.
        """
        self.intercept = True
        self.on_sendButton_clicked()

    def keyPressEvent(self, evt):
        """
        Protected slot to handle a key press event.

        @param evt the key press event (QKeyEvent)
        """
        if self.intercept:
            self.intercept = False
            evt.accept()
            return
        super().keyPressEvent(evt)

eric ide

mercurial