# -*- 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
"""

import os.path as osp
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
from winpython.py3compat import getcwd, to_text_string


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 to_qvariant(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(.1)
                return to_qvariant(color)
            else:
                color = QColor(Qt.lightGray)
                color.setAlphaF(.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 = osp.basename(fname)
            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 osp.isfile(path)]
        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())
        if not osp.isdir(basedir):
            basedir = 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 = osp.abspath(osp.normpath(directory))
            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(osp.exists(self.command_prompt_path))
        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, "About %s..." % 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("Welcome to %s!" % 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",
            "This will associate file extensions, icons and "
            "Windows explorer's context menu entries ('Edit with IDLE', ...) "
            "with selected Python distribution in Windows registry. "
            "<br>Shortcuts for all WinPython launchers will be installed "
            "in <i>WinPython</i> Start menu group (replacing existing "
            "shortcuts)."
            "<br>If <i>pywin32</i> is installed (it should be on any "
            "WinPython distribution), the Python ActiveX Scripting client "
            "will also be registered."
            "<br><br><u>Warning</u>: the only way to undo this change is to "
            "register another Python distribution to Windows registry."
            "<br><br><u>Note</u>: these actions are exactly the same as those "
            "performed when installing Python with the official installer "
            "for Windows.<br><br>Do 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",
            "This will remove file extensions associations, icons and "
            "Windows explorer's context menu entries ('Edit with IDLE', ...) "
            "with selected Python distribution in Windows registry. "
            "<br>Shortcuts for all WinPython launchers will be removed "
            "from <i>WinPython</i> Start menu group."
            "<br>If <i>pywin32</i> is installed (it should be on any "
            "WinPython distribution), the Python ActiveX Scripting client "
            "will also be unregistered."
            "<br><br>Do you want to continue?",
            QMessageBox.Yes | QMessageBox.No)
        if answer == QMessageBox.Yes:
            associate.unregister(self.distribution.target)

    @property
    def command_prompt_path(self):
        return osp.join(self.distribution.target, osp.pardir,
                        "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))
        self.table.refresh_distribution(dist)
        self.untable.refresh_distribution(dist)
        self.distribution = dist
        self.selector.label.setText('Python %s %dbit:'
                                    % (dist.version, dist.architecture))

    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 = osp.dirname(fnames[0])
            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("%s %s %s..."
                                  % (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 = to_text_string(error)
                if error is not None:
                    pstr = package.name + ' ' + package.version
                    QMessageBox.critical(self, "Error",
                                         "<b>Unable to %s <i>%s</i></b>"
                                         "<br><br>Error message:<br>%s"
                                         % (action, pstr, 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 = """\
Python distribution:   %s
Control panel version: %s

Python Version:  %s
Qt Version:      %s, %s %s

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.
""" % (python_distribution_infos(),
       __version__, platform.python_version(),
       winpython._vendor.qtpy.QtCore.__version__, winpython.qt.API_NAME,
       winpython._vendor.qtpy.__version__)

        url = QUrl("%s/issues/entry" % __project_url__)
        url.addQueryItem("comment", issue_template)
        QDesktopServices.openUrl(url)

    def about(self):
        """About this program"""
        QMessageBox.about(self,
            "About %s" % self.NAME,
            """<b>%s %s</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="%s">WinPython at Github.io</a>: downloads, bug reports,
            discussions, etc.</p>
            <p>This program is executed by:<br>
            <b>%s</b><br>
            Python %s, Qt %s, %s qtpy %s"""
            % (self.NAME, __version__, __project_url__,
               python_distribution_infos(),
               platform.python_version(), winpython._vendor.qtpy.QtCore.__version__,
               winpython._vendor.qtpy.API_NAME, 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()