text: # -*- 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()
  