# -*- coding: utf-8 -*-
#
# Copyright © 2012 Pierre Raybaut
# Licensed under the terms of the MIT License
# (see winpython/__init__.py for details)

"""
WinPython Package Manager GUI

Created on Mon Aug 13 11:40:01 2012
"""

from pathlib import Path
import os
import sys
import platform
import locale

# winpython.qt becomes winpython._vendor.qtpy
from winpython._vendor.qtpy.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QLineEdit,
    QHBoxLayout,
    QVBoxLayout,
    QMessageBox,
    QAbstractItemView,
    QProgressDialog,
    QTableView,
    QPushButton,
    QLabel,
    QTabWidget,
    QToolTip,
)

from winpython._vendor.qtpy.QtGui import (
    QColor,
    QDesktopServices,
)

from winpython._vendor.qtpy.QtCore import (
    Qt,
    QAbstractTableModel,
    QModelIndex,
    Signal,
    QThread,
    QTimer,
    QUrl,
)
from winpython._vendor.qtpy.compat import (
    to_qvariant,
    getopenfilenames,
    getexistingdirectory,
)
import winpython._vendor.qtpy

from winpython.qthelpers import (
    get_icon,
    add_actions,
    create_action,
    keybinding,
    get_std_icon,
    action2button,
    mimedata2url,
)

# Local imports
from winpython import __version__, __project_url__
from winpython import wppm, associate, utils

COLUMNS = ACTION, CHECK, NAME, VERSION, DESCRIPTION = list(
    range(5)
)


class PackagesModel(QAbstractTableModel):
    # Signals after PyQt4 old SIGNAL removal
    dataChanged = Signal(QModelIndex, QModelIndex)

    def __init__(self):
        QAbstractTableModel.__init__(self)
        self.packages = []
        self.checked = set()
        self.actions = {}

    def sortByName(self):
        self.packages = sorted(
            self.packages, key=lambda x: x.name
        )
        self.reset()

    def flags(self, index):
        if not index.isValid():
            return Qt.ItemIsEnabled
        column = index.column()
        if column in (NAME, VERSION, ACTION, DESCRIPTION):
            return Qt.ItemFlags(
                QAbstractTableModel.flags(self, index)
            )
        else:
            return Qt.ItemFlags(
                QAbstractTableModel.flags(self, index)
                | Qt.ItemIsUserCheckable
                | Qt.ItemIsEditable
            )

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid() or not (
            0 <= index.row() < len(self.packages)
        ):
            return to_qvariant()
        package = self.packages[index.row()]
        column = index.column()
        if role == Qt.CheckStateRole and column == CHECK:
            return int(package in self.checked)
        elif role == Qt.DisplayRole:
            if column == NAME:
                return to_qvariant(package.name)
            elif column == VERSION:
                return to_qvariant(package.version)
            elif column == ACTION:
                action = self.actions.get(package)
                if action is not None:
                    return to_qvariant(action)
            elif column == DESCRIPTION:
                return to_qvariant(package.description)
        elif role == Qt.TextAlignmentRole:
            if column == ACTION:
                return to_qvariant(
                    int(Qt.AlignRight | Qt.AlignVCenter)
                )
            else:
                return to_qvariant(
                    int(Qt.AlignLeft | Qt.AlignVCenter)
                )
        elif role == Qt.BackgroundColorRole:
            if package in self.checked:
                color = QColor(Qt.darkGreen)
                color.setAlphaF(0.1)
                return to_qvariant(color)
            else:
                color = QColor(Qt.lightGray)
                color.setAlphaF(0.3)
                return to_qvariant(color)
        return to_qvariant()

    def headerData(
        self, section, orientation, role=Qt.DisplayRole
    ):
        if role == Qt.TextAlignmentRole:
            if orientation == Qt.Horizontal:
                return to_qvariant(
                    int(Qt.AlignHCenter | Qt.AlignVCenter)
                )
            return to_qvariant(
                int(Qt.AlignRight | Qt.AlignVCenter)
            )
        if role != Qt.DisplayRole:
            return to_qvariant()
        if orientation == Qt.Horizontal:
            if section == NAME:
                return to_qvariant("Name")
            elif section == VERSION:
                return to_qvariant("Version")
            elif section == ACTION:
                return to_qvariant("Action")
            elif section == DESCRIPTION:
                return to_qvariant("Description")
        return to_qvariant()

    def rowCount(self, index=QModelIndex()):
        return len(self.packages)

    def columnCount(self, index=QModelIndex()):
        return len(COLUMNS)

    def setData(self, index, value, role=Qt.EditRole):
        if (
            index.isValid()
            and 0 <= index.row() < len(self.packages)
            and role == Qt.CheckStateRole
        ):
            package = self.packages[index.row()]
            if package in self.checked:
                self.checked.remove(package)
            else:
                self.checked.add(package)
            # PyQt4 old SIGNAL: self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
            # PyQt4 old SIGNAL:           index, index)
            self.dataChanged.emit(index, index)
            return True
        return False


INSTALL_ACTION = 'Install'
REPAIR_ACTION = 'Repair (reinstall)'
NO_REPAIR_ACTION = 'None (Already installed)'
UPGRADE_ACTION = 'Upgrade from v'
NONE_ACTION = '-'


class PackagesTable(QTableView):
    # Signals after PyQt4 old SIGNAL removal, to be emitted after package_added event
    package_added = Signal()

    def __init__(self, parent, process, winname):
        QTableView.__init__(self, parent)
        assert process in ('install', 'uninstall')
        self.process = process
        self.model = PackagesModel()
        self.setModel(self.model)
        self.winname = winname
        self.repair = False
        self.resizeColumnToContents(0)
        self.setAcceptDrops(process == 'install')
        if process == 'uninstall':
            self.hideColumn(0)
        self.distribution = None

        self.setSelectionBehavior(
            QAbstractItemView.SelectRows
        )
        self.verticalHeader().hide()
        self.setShowGrid(False)

    def reset_model(self):
        # self.model.reset() is deprecated in Qt5
        self.model.beginResetModel()
        self.model.endResetModel()
        self.horizontalHeader().setStretchLastSection(True)
        for colnb in (ACTION, CHECK, NAME, VERSION):
            self.resizeColumnToContents(colnb)

    def get_selected_packages(self):
        """Return selected packages"""
        return [
            pack
            for pack in self.model.packages
            if pack in self.model.checked
        ]

    def add_packages(self, fnames):
        """Add packages"""
        notsupported = []
        notcompatible = []
        dist = self.distribution
        for fname in fnames:
            bname = Path(fname).name
            try:
                package = wppm.Package(fname)
                if package.is_compatible_with(dist):
                    self.add_package(package)
                else:
                    notcompatible.append(bname)
            except NotImplementedError:
                notsupported.append(bname)
        # PyQt4 old SIGNAL: self.emit(SIGNAL('package_added()'))
        self.package_added.emit()
        if notsupported:
            QMessageBox.warning(
                self,
                "Warning",
                "The following packages filenaming are <b>not "
                "recognized</b> by %s:\n\n%s"
                % (self.winname, "<br>".join(notsupported)),
                QMessageBox.Ok,
            )
        if notcompatible:
            QMessageBox.warning(
                self,
                "Warning",
                "The following packages "
                "are <b>not compatible</b> with "
                "Python <u>%s %dbit</u>:\n\n%s"
                % (
                    dist.version,
                    dist.architecture,
                    "<br>".join(notcompatible),
                ),
                QMessageBox.Ok,
            )

    def add_package(self, package):
        for pack in self.model.packages:
            if pack.name == package.name:
                return
        self.model.packages.append(package)
        self.model.packages.sort(key=lambda x: x.name)
        self.model.checked.add(package)
        self.reset_model()

    def remove_package(self, package):
        self.model.packages = [
            pack
            for pack in self.model.packages
            if pack.fname != package.fname
        ]
        if package in self.model.checked:
            self.model.checked.remove(package)
        if package in self.model.actions:
            self.model.actions.pop(package)
        self.reset_model()

    def refresh_distribution(self, dist):
        self.distribution = dist
        if self.process == 'install':
            for package in self.model.packages:
                pack = dist.find_package(package.name)
                if pack is None:
                    action = INSTALL_ACTION
                elif pack.version == package.version:
                    if self.repair:
                        action = REPAIR_ACTION
                    else:
                        action = NO_REPAIR_ACTION
                else:
                    action = UPGRADE_ACTION + pack.version
                self.model.actions[package] = action
        else:
            self.model.packages = (
                self.distribution.get_installed_packages()
            )
            for package in self.model.packages:
                self.model.actions[package] = NONE_ACTION
        self.reset_model()

    def select_all(self):
        allpk = set(self.model.packages)
        if self.model.checked == allpk:
            self.model.checked = set()
        else:
            self.model.checked = allpk
        self.model.reset()

    def dragMoveEvent(self, event):
        """Reimplement Qt method, just to avoid default drag'n drop
        implementation of QTableView to handle events"""
        event.acceptProposedAction()

    def dragEnterEvent(self, event):
        """Reimplement Qt method
        Inform Qt about the types of data that the widget accepts"""
        source = event.mimeData()
        if source.hasUrls() and mimedata2url(source):
            event.acceptProposedAction()

    def dropEvent(self, event):
        """Reimplement Qt method
        Unpack dropped data and handle it"""
        source = event.mimeData()
        fnames = [
            path
            for path in mimedata2url(source)
            if Path(path).is_file()
        ]
        self.add_packages(fnames)
        event.acceptProposedAction()


class DistributionSelector(QWidget):
    """Python distribution selector widget"""

    TITLE = 'Select a Python distribution path'

    # Signals after PyQt4 old SIGNAL removal
    selected_distribution = Signal(str)

    def __init__(self, parent):
        super(DistributionSelector, self).__init__(parent)
        self.browse_btn = None
        self.label = None
        self.line_edit = None
        self.setup_widget()

    def set_distribution(self, path):
        """Set distribution directory"""
        self.line_edit.setText(path)

    def setup_widget(self):
        """Setup workspace selector widget"""
        self.label = QLabel()
        self.line_edit = QLineEdit()
        self.line_edit.setAlignment(Qt.AlignRight)
        self.line_edit.setReadOnly(True)
        # self.line_edit.setDisabled(True)
        self.browse_btn = QPushButton(
            get_std_icon('DirOpenIcon'), "", self
        )
        self.browse_btn.setToolTip(self.TITLE)
        # PyQt4 old SIGNAL:self.connect(self.browse_btn, SIGNAL("clicked()"),
        # PyQt4 old SIGNAL:             self.select_directory)
        self.browse_btn.clicked.connect(
            self.select_directory
        )
        layout = QHBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.line_edit)
        layout.addWidget(self.browse_btn)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(layout)

    def select_directory(self):
        """Select directory"""
        #  basedir = to_text_string(self.line_edit.text())
        basedir = str(self.line_edit.text())
        if not Path(basedir).is_dir():
            basedir = str(Path.cwd())  # getcwd()
        while True:
            directory = getexistingdirectory(
                self, self.TITLE, basedir
            )
            if not directory:
                break
            if not utils.is_python_distribution(directory):
                QMessageBox.warning(
                    self,
                    self.TITLE,
                    "The following directory is not a Python distribution.",
                    QMessageBox.Ok,
                )
                basedir = directory
                continue
            directory = str(Path(directory).resolve(strict=False))
            self.set_distribution(directory)
            # PyQt4 old SIGNAL: self.emit(SIGNAL('selected_distribution(QString)'), directory)
            self.selected_distribution.emit(directory)
            break


class Thread(QThread):
    """Installation/Uninstallation thread"""

    def __init__(self, parent):
        QThread.__init__(self, parent)
        self.callback = None
        self.error = None

    def run(self):
        try:
            self.callback()
        except Exception as error:
            error_str = str(error)
            fs_encoding = (
                sys.getfilesystemencoding()
                or locale.getpreferredencoding()
            )
            try:
                error_str = error_str.decode(fs_encoding)
            except (
                UnicodeError,
                TypeError,
                AttributeError,
            ):
                pass
            self.error = error_str


def python_distribution_infos():
    """Return Python distribution infos (not selected distribution but
    the one used to run this script)"""
    winpyver = os.environ.get('WINPYVER')
    if winpyver is None:
        return 'Unknown Python distribution'
    else:
        return 'WinPython ' + winpyver


class PMWindow(QMainWindow):
    NAME = 'WinPython Control Panel'

    def __init__(self):
        QMainWindow.__init__(self)
        self.setAttribute(Qt.WA_DeleteOnClose)

        self.distribution = None

        self.tabwidget = None
        self.selector = None
        self.table = None
        self.untable = None

        self.basedir = None

        self.select_all_action = None
        self.install_action = None
        self.uninstall_action = None
        self.remove_action = None
        self.packages_icon = get_std_icon(
            'FileDialogContentsView'
        )

        self.setup_window()

    def _add_table(self, table, title, icon):
        """Add table tab to main tab widget, return button layout"""
        widget = QWidget()
        tabvlayout = QVBoxLayout()
        widget.setLayout(tabvlayout)
        tabvlayout.addWidget(table)
        btn_layout = QHBoxLayout()
        tabvlayout.addLayout(btn_layout)
        self.tabwidget.addTab(widget, icon, title)
        return btn_layout

    def setup_window(self):
        """Setup main window"""
        self.setWindowTitle(self.NAME)
        self.setWindowIcon(get_icon('winpython.svg'))

        self.selector = DistributionSelector(self)
        # PyQt4 old SIGNAL: self.connect(self.selector, SIGNAL('selected_distribution(QString)'),
        # PyQt4 old SIGNAL:              self.distribution_changed)
        self.selector.selected_distribution.connect(
            self.distribution_changed
        )

        self.table = PackagesTable(
            self, 'install', self.NAME
        )
        # PyQt4 old SIGNAL:self.connect(self.table, SIGNAL('package_added()'),
        # PyQt4 old SIGNAL:             self.refresh_install_button)
        self.table.package_added.connect(
            self.refresh_install_button
        )

        # PyQt4 old SIGNAL: self.connect(self.table, SIGNAL("clicked(QModelIndex)"),
        # PyQt4 old SIGNAL:              lambda index: self.refresh_install_button())
        self.table.clicked.connect(
            lambda index: self.refresh_install_button()
        )

        self.untable = PackagesTable(
            self, 'uninstall', self.NAME
        )
        # PyQt4 old SIGNAL:self.connect(self.untable, SIGNAL("clicked(QModelIndex)"),
        # PyQt4 old SIGNAL:             lambda index: self.refresh_uninstall_button())
        self.untable.clicked.connect(
            lambda index: self.refresh_uninstall_button()
        )

        self.selector.set_distribution(sys.prefix)
        self.distribution_changed(sys.prefix)

        self.tabwidget = QTabWidget()
        # PyQt4 old SIGNAL:self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
        # PyQt4 old SIGNAL:             self.current_tab_changed)
        self.tabwidget.currentChanged.connect(
            self.current_tab_changed
        )

        btn_layout = self._add_table(
            self.table,
            "Install/upgrade packages",
            get_std_icon("ArrowDown"),
        )
        unbtn_layout = self._add_table(
            self.untable,
            "Uninstall packages",
            get_std_icon("DialogResetButton"),
        )

        central_widget = QWidget()
        vlayout = QVBoxLayout()
        vlayout.addWidget(self.selector)
        vlayout.addWidget(self.tabwidget)
        central_widget.setLayout(vlayout)
        self.setCentralWidget(central_widget)

        # Install tab
        add_action = create_action(
            self,
            "&Add packages...",
            icon=get_std_icon('DialogOpenButton'),
            triggered=self.add_packages,
        )
        self.remove_action = create_action(
            self,
            "Remove",
            shortcut=keybinding('Delete'),
            icon=get_std_icon('TrashIcon'),
            triggered=self.remove_packages,
        )
        self.remove_action.setEnabled(False)
        self.select_all_action = create_action(
            self,
            "(Un)Select all",
            shortcut=keybinding('SelectAll'),
            icon=get_std_icon('DialogYesButton'),
            triggered=self.table.select_all,
        )
        self.install_action = create_action(
            self,
            "&Install packages",
            icon=get_std_icon('DialogApplyButton'),
            triggered=lambda: self.process_packages(
                'install'
            ),
        )
        self.install_action.setEnabled(False)
        quit_action = create_action(
            self,
            "&Quit",
            icon=get_std_icon('DialogCloseButton'),
            triggered=self.close,
        )
        packages_menu = self.menuBar().addMenu("&Packages")
        add_actions(
            packages_menu,
            [
                add_action,
                self.remove_action,
                self.install_action,
                None,
                quit_action,
            ],
        )

        # Uninstall tab
        self.uninstall_action = create_action(
            self,
            "&Uninstall packages",
            icon=get_std_icon('DialogCancelButton'),
            triggered=lambda: self.process_packages(
                'uninstall'
            ),
        )
        self.uninstall_action.setEnabled(False)

        uninstall_btn = action2button(
            self.uninstall_action,
            autoraise=False,
            text_beside_icon=True,
        )

        # Option menu
        option_menu = self.menuBar().addMenu("&Options")
        repair_action = create_action(
            self,
            "Repair packages",
            tip="Reinstall packages even if version is unchanged",
            toggled=self.toggle_repair,
        )
        add_actions(option_menu, (repair_action,))

        # Advanced menu
        option_menu = self.menuBar().addMenu("&Advanced")
        register_action = create_action(
            self,
            "Register distribution...",
            tip="Register file extensions, icons and context menu",
            triggered=self.register_distribution,
        )
        unregister_action = create_action(
            self,
            "Unregister distribution...",
            tip="Unregister file extensions, icons and context menu",
            triggered=self.unregister_distribution,
        )
        open_console_action = create_action(
            self,
            "Open console here",
            triggered=lambda: os.startfile(
                self.command_prompt_path
            ),
        )
        open_console_action.setEnabled(
            Path(self.command_prompt_path).exists()
        )
        add_actions(
            option_menu,
            (
                register_action,
                unregister_action,
                None,
                open_console_action,
            ),
        )

        # # View menu
        # view_menu = self.menuBar().addMenu("&View")
        # popmenu = self.createPopupMenu()
        # add_actions(view_menu, popmenu.actions())

        # Help menu
        about_action = create_action(
            self,
            f"About {self.NAME}...",
            icon=get_std_icon('MessageBoxInformation'),
            triggered=self.about,
        )
        report_action = create_action(
            self,
            "Report issue...",
            icon=get_icon('bug.png'),
            triggered=self.report_issue,
        )
        help_menu = self.menuBar().addMenu("?")
        add_actions(
            help_menu, [about_action, None, report_action]
        )

        # Status bar
        status = self.statusBar()
        status.setObjectName("StatusBar")
        status.showMessage(
            f"Welcome to {self.NAME}!", 5000
        )

        # Button layouts
        for act in (
            add_action,
            self.remove_action,
            None,
            self.select_all_action,
            self.install_action,
        ):
            if act is None:
                btn_layout.addStretch()
            else:
                btn_layout.addWidget(
                    action2button(
                        act,
                        autoraise=False,
                        text_beside_icon=True,
                    )
                )
        unbtn_layout.addWidget(uninstall_btn)
        unbtn_layout.addStretch()

        self.resize(400, 500)

    def current_tab_changed(self, index):
        """Current tab has just changed"""
        if index == 0:
            self.show_drop_tip()

    def refresh_install_button(self):
        """Refresh install button enable state"""
        self.table.refresh_distribution(self.distribution)
        self.install_action.setEnabled(
            len(self.get_packages_to_be_installed()) > 0
        )
        nbp = len(self.table.get_selected_packages())
        for act in (
            self.remove_action,
            self.select_all_action,
        ):
            act.setEnabled(nbp > 0)
        self.show_drop_tip()

    def show_drop_tip(self):
        """Show drop tip on install table"""
        callback = lambda: QToolTip.showText(
            self.table.mapToGlobal(self.table.pos()),
            '<b>Drop files here</b><br>'
            'Executable installers (distutils) or source packages',
            self,
        )
        QTimer.singleShot(500, callback)

    def refresh_uninstall_button(self):
        """Refresh uninstall button enable state"""
        nbp = len(self.untable.get_selected_packages())
        self.uninstall_action.setEnabled(nbp > 0)

    def toggle_repair(self, state):
        """Toggle repair mode"""
        self.table.repair = state
        self.refresh_install_button()

    def register_distribution(self):
        """Register distribution"""
        answer = QMessageBox.warning(
            self,
            "Register distribution",
            "(experimental)\n"
            "This will associate file extensions, icons and "
            "Windows explorer's context menu entries ('Edit with IDLE', ...) "
            "with selected Python distribution in Windows registry. "
            "\n\nShortcuts for all WinPython launchers will be installed "
            "in WinPython Start menu group (replacing existing "
            "shortcuts)."
            "\n\nNote: these actions are similar to those performed"
            "when installing old Pythons with the official installer before 'py' "
            "for Windows.\n\nDo you want to continue? ",
            QMessageBox.Yes | QMessageBox.No,
        )
        if answer == QMessageBox.Yes:
            associate.register(self.distribution.target)

    def unregister_distribution(self):
        """Unregister distribution"""
        answer = QMessageBox.warning(
            self,
            "Unregister distribution",
            "(experimental)\n"
            "This will remove file extensions associations, icons and "
            "Windows explorer's context menu entries ('Edit with IDLE', ...) "
            "with selected Python distribution in Windows registry. "
            "\n\nShortcuts for all WinPython launchers will be removed "
            "from WinPython Start menu group."
            "\n\nDo you want to continue? ",
            QMessageBox.Yes | QMessageBox.No,
        )
        if answer == QMessageBox.Yes:
            associate.unregister(self.distribution.target)

    @property
    def command_prompt_path(self):
        return str(Path(self.distribution.target).parent /
                   "WinPython Command Prompt.exe")

    def distribution_changed(self, path):
        """Distribution path has just changed"""
        for package in self.table.model.packages:
            self.table.remove_package(package)
        # dist = wppm.Distribution(to_text_string(path))
        dist = wppm.Distribution(str(path))
        self.table.refresh_distribution(dist)
        self.untable.refresh_distribution(dist)
        self.distribution = dist
        self.selector.label.setText(
            f'Python {dist.version} {dist.architecture}bit:'
        )

    def add_packages(self):
        """Add packages"""
        basedir = (
            self.basedir if self.basedir is not None else ''
        )
        fnames, _selfilter = getopenfilenames(
            parent=self,
            basedir=basedir,
            caption='Add packages',
            filters='*.exe *.zip *.tar.gz *.whl',
        )
        if fnames:
            self.basedir = str(Path(fnames[0]).parent)
            self.table.add_packages(fnames)

    def get_packages_to_be_installed(self):
        """Return packages to be installed"""
        return [
            pack
            for pack in self.table.get_selected_packages()
            if self.table.model.actions[pack]
            not in (NO_REPAIR_ACTION, NONE_ACTION)
        ]

    def remove_packages(self):
        """Remove selected packages"""
        for package in self.table.get_selected_packages():
            self.table.remove_package(package)

    def process_packages(self, action):
        """Install/uninstall packages"""
        if action == 'install':
            text, table = 'Installing', self.table
            if not self.get_packages_to_be_installed():
                return
        elif action == 'uninstall':
            text, table = 'Uninstalling', self.untable
        else:
            raise AssertionError
        packages = table.get_selected_packages()
        if not packages:
            return
        func = getattr(self.distribution, action)
        thread = Thread(self)
        for widget in self.children():
            if isinstance(widget, QWidget):
                widget.setEnabled(False)
        try:
            status = self.statusBar()
        except AttributeError:
            status = self.parent().statusBar()
        progress = QProgressDialog(
            self, Qt.FramelessWindowHint
        )
        progress.setMaximum(
            len(packages)
        )  #  old vicious bug:len(packages)-1
        for index, package in enumerate(packages):
            progress.setValue(index)
            progress.setLabelText(
                f"{text} {package.name} {package.version}..."
            )
            QApplication.processEvents()
            if progress.wasCanceled():
                break
            if package in table.model.actions:
                try:
                    thread.callback = lambda: func(package)
                    thread.start()
                    while thread.isRunning():
                        QApplication.processEvents()
                        if progress.wasCanceled():
                            status.setEnabled(True)
                            status.showMessage(
                                "Cancelling operation..."
                            )
                    table.remove_package(package)
                    error = thread.error
                except Exception as error:
                    error = str(error)  # to_text_string(error)
                if error is not None:
                    pstr = (
                        package.name + ' ' + package.version
                    )
                    QMessageBox.critical(
                        self,
                        "Error",
                        f"<b>Unable to {action} <i>{pstr}/i></b>"
                        f"<br><br>Error message:<br>{error}"
                        ,
                    )
        progress.setValue(progress.maximum())
        status.clearMessage()
        for widget in self.children():
            if isinstance(widget, QWidget):
                widget.setEnabled(True)
        thread = None
        for table in (self.table, self.untable):
            table.refresh_distribution(self.distribution)

    def report_issue(self):

        issue_template = f"""\
Python distribution:   {python_distribution_infos()}
Control panel version: {__version__}

Python Version:  {platform.python_version()}
Qt Version:      {winpython._vendor.qtpy.QtCore.__version__}, {winpython.qt.API_NAME} {winpython._vendor.qtpy.__version__}

What steps will reproduce the problem?
1.
2.
3.

What is the expected output? What do you see instead?


Please provide any additional information below.
"""

        url = QUrl(f"{__project_url__}/issues/entry")
        url.addQueryItem("comment", issue_template)
        QDesktopServices.openUrl(url)

    def about(self):
        """About this program"""
        QMessageBox.about(
            self,
            f"About {self.NAME}",
            f"""<b>{self.NAME} {__version__}</b>
            <br>Package Manager and Advanced Tasks
            <p>Copyright &copy; 2012 Pierre Raybaut
            <br>Licensed under the terms of the MIT License
            <p>Created, developed and maintained by Pierre Raybaut
            <p><a href="{__project_url__}">WinPython at Github.io</a>: downloads, bug reports,
            discussions, etc.</p>
            <p>This program is executed by:<br>
            <b>{python_distribution_infos()}</b><br>
            Python {platform.python_version()}, Qt {winpython._vendor.qtpy.QtCore.__version__}, {winpython._vendor.qtpy.API_NAME} qtpy {winpython._vendor.qtpy.__version__}"""
            ,
        )


def main(test=False):
    app = QApplication([])
    win = PMWindow()
    win.show()
    if test:
        return app, win
    else:
        app.exec_()


def test():
    app, win = main(test=True)
    print(sys.modules)
    app.exec_()


if __name__ == "__main__":
    main()
