You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
773 lines
30 KiB
773 lines
30 KiB
# -*- 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 © 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()
|