controlpanel.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright © 2012 Pierre Raybaut
  4. # Licensed under the terms of the MIT License
  5. # (see winpython/__init__.py for details)
  6. """
  7. WinPython Package Manager GUI
  8. Created on Mon Aug 13 11:40:01 2012
  9. """
  10. import os.path as osp
  11. import os
  12. import sys
  13. import platform
  14. import locale
  15. # winpython.qt becomes winpython._vendor.qtpy
  16. from winpython._vendor.qtpy.QtWidgets import (
  17. QApplication,
  18. QMainWindow,
  19. QWidget,
  20. QLineEdit,
  21. QHBoxLayout,
  22. QVBoxLayout,
  23. QMessageBox,
  24. QAbstractItemView,
  25. QProgressDialog,
  26. QTableView,
  27. QPushButton,
  28. QLabel,
  29. QTabWidget,
  30. QToolTip,
  31. )
  32. from winpython._vendor.qtpy.QtGui import (
  33. QColor,
  34. QDesktopServices,
  35. )
  36. from winpython._vendor.qtpy.QtCore import (
  37. Qt,
  38. QAbstractTableModel,
  39. QModelIndex,
  40. Signal,
  41. QThread,
  42. QTimer,
  43. QUrl,
  44. )
  45. from winpython._vendor.qtpy.compat import (
  46. to_qvariant,
  47. getopenfilenames,
  48. getexistingdirectory,
  49. )
  50. import winpython._vendor.qtpy
  51. from winpython.qthelpers import (
  52. get_icon,
  53. add_actions,
  54. create_action,
  55. keybinding,
  56. get_std_icon,
  57. action2button,
  58. mimedata2url,
  59. )
  60. # Local imports
  61. from winpython import __version__, __project_url__
  62. from winpython import wppm, associate, utils
  63. from winpython.py3compat import getcwd, to_text_string
  64. COLUMNS = ACTION, CHECK, NAME, VERSION, DESCRIPTION = list(
  65. range(5)
  66. )
  67. class PackagesModel(QAbstractTableModel):
  68. # Signals after PyQt4 old SIGNAL removal
  69. dataChanged = Signal(QModelIndex, QModelIndex)
  70. def __init__(self):
  71. QAbstractTableModel.__init__(self)
  72. self.packages = []
  73. self.checked = set()
  74. self.actions = {}
  75. def sortByName(self):
  76. self.packages = sorted(
  77. self.packages, key=lambda x: x.name
  78. )
  79. self.reset()
  80. def flags(self, index):
  81. if not index.isValid():
  82. return Qt.ItemIsEnabled
  83. column = index.column()
  84. if column in (NAME, VERSION, ACTION, DESCRIPTION):
  85. return Qt.ItemFlags(
  86. QAbstractTableModel.flags(self, index)
  87. )
  88. else:
  89. return Qt.ItemFlags(
  90. QAbstractTableModel.flags(self, index)
  91. | Qt.ItemIsUserCheckable
  92. | Qt.ItemIsEditable
  93. )
  94. def data(self, index, role=Qt.DisplayRole):
  95. if not index.isValid() or not (
  96. 0 <= index.row() < len(self.packages)
  97. ):
  98. return to_qvariant()
  99. package = self.packages[index.row()]
  100. column = index.column()
  101. if role == Qt.CheckStateRole and column == CHECK:
  102. return to_qvariant(package in self.checked)
  103. elif role == Qt.DisplayRole:
  104. if column == NAME:
  105. return to_qvariant(package.name)
  106. elif column == VERSION:
  107. return to_qvariant(package.version)
  108. elif column == ACTION:
  109. action = self.actions.get(package)
  110. if action is not None:
  111. return to_qvariant(action)
  112. elif column == DESCRIPTION:
  113. return to_qvariant(package.description)
  114. elif role == Qt.TextAlignmentRole:
  115. if column == ACTION:
  116. return to_qvariant(
  117. int(Qt.AlignRight | Qt.AlignVCenter)
  118. )
  119. else:
  120. return to_qvariant(
  121. int(Qt.AlignLeft | Qt.AlignVCenter)
  122. )
  123. elif role == Qt.BackgroundColorRole:
  124. if package in self.checked:
  125. color = QColor(Qt.darkGreen)
  126. color.setAlphaF(0.1)
  127. return to_qvariant(color)
  128. else:
  129. color = QColor(Qt.lightGray)
  130. color.setAlphaF(0.3)
  131. return to_qvariant(color)
  132. return to_qvariant()
  133. def headerData(
  134. self, section, orientation, role=Qt.DisplayRole
  135. ):
  136. if role == Qt.TextAlignmentRole:
  137. if orientation == Qt.Horizontal:
  138. return to_qvariant(
  139. int(Qt.AlignHCenter | Qt.AlignVCenter)
  140. )
  141. return to_qvariant(
  142. int(Qt.AlignRight | Qt.AlignVCenter)
  143. )
  144. if role != Qt.DisplayRole:
  145. return to_qvariant()
  146. if orientation == Qt.Horizontal:
  147. if section == NAME:
  148. return to_qvariant("Name")
  149. elif section == VERSION:
  150. return to_qvariant("Version")
  151. elif section == ACTION:
  152. return to_qvariant("Action")
  153. elif section == DESCRIPTION:
  154. return to_qvariant("Description")
  155. return to_qvariant()
  156. def rowCount(self, index=QModelIndex()):
  157. return len(self.packages)
  158. def columnCount(self, index=QModelIndex()):
  159. return len(COLUMNS)
  160. def setData(self, index, value, role=Qt.EditRole):
  161. if (
  162. index.isValid()
  163. and 0 <= index.row() < len(self.packages)
  164. and role == Qt.CheckStateRole
  165. ):
  166. package = self.packages[index.row()]
  167. if package in self.checked:
  168. self.checked.remove(package)
  169. else:
  170. self.checked.add(package)
  171. # PyQt4 old SIGNAL: self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"),
  172. # PyQt4 old SIGNAL: index, index)
  173. self.dataChanged.emit(index, index)
  174. return True
  175. return False
  176. INSTALL_ACTION = 'Install'
  177. REPAIR_ACTION = 'Repair (reinstall)'
  178. NO_REPAIR_ACTION = 'None (Already installed)'
  179. UPGRADE_ACTION = 'Upgrade from v'
  180. NONE_ACTION = '-'
  181. class PackagesTable(QTableView):
  182. # Signals after PyQt4 old SIGNAL removal, to be emitted after package_added event
  183. package_added = Signal()
  184. def __init__(self, parent, process, winname):
  185. QTableView.__init__(self, parent)
  186. assert process in ('install', 'uninstall')
  187. self.process = process
  188. self.model = PackagesModel()
  189. self.setModel(self.model)
  190. self.winname = winname
  191. self.repair = False
  192. self.resizeColumnToContents(0)
  193. self.setAcceptDrops(process == 'install')
  194. if process == 'uninstall':
  195. self.hideColumn(0)
  196. self.distribution = None
  197. self.setSelectionBehavior(
  198. QAbstractItemView.SelectRows
  199. )
  200. self.verticalHeader().hide()
  201. self.setShowGrid(False)
  202. def reset_model(self):
  203. # self.model.reset() is deprecated in Qt5
  204. self.model.beginResetModel()
  205. self.model.endResetModel()
  206. self.horizontalHeader().setStretchLastSection(True)
  207. for colnb in (ACTION, CHECK, NAME, VERSION):
  208. self.resizeColumnToContents(colnb)
  209. def get_selected_packages(self):
  210. """Return selected packages"""
  211. return [
  212. pack
  213. for pack in self.model.packages
  214. if pack in self.model.checked
  215. ]
  216. def add_packages(self, fnames):
  217. """Add packages"""
  218. notsupported = []
  219. notcompatible = []
  220. dist = self.distribution
  221. for fname in fnames:
  222. bname = osp.basename(fname)
  223. try:
  224. package = wppm.Package(fname)
  225. if package.is_compatible_with(dist):
  226. self.add_package(package)
  227. else:
  228. notcompatible.append(bname)
  229. except NotImplementedError:
  230. notsupported.append(bname)
  231. # PyQt4 old SIGNAL: self.emit(SIGNAL('package_added()'))
  232. self.package_added.emit()
  233. if notsupported:
  234. QMessageBox.warning(
  235. self,
  236. "Warning",
  237. "The following packages filenaming are <b>not "
  238. "recognized</b> by %s:\n\n%s"
  239. % (self.winname, "<br>".join(notsupported)),
  240. QMessageBox.Ok,
  241. )
  242. if notcompatible:
  243. QMessageBox.warning(
  244. self,
  245. "Warning",
  246. "The following packages "
  247. "are <b>not compatible</b> with "
  248. "Python <u>%s %dbit</u>:\n\n%s"
  249. % (
  250. dist.version,
  251. dist.architecture,
  252. "<br>".join(notcompatible),
  253. ),
  254. QMessageBox.Ok,
  255. )
  256. def add_package(self, package):
  257. for pack in self.model.packages:
  258. if pack.name == package.name:
  259. return
  260. self.model.packages.append(package)
  261. self.model.packages.sort(key=lambda x: x.name)
  262. self.model.checked.add(package)
  263. self.reset_model()
  264. def remove_package(self, package):
  265. self.model.packages = [
  266. pack
  267. for pack in self.model.packages
  268. if pack.fname != package.fname
  269. ]
  270. if package in self.model.checked:
  271. self.model.checked.remove(package)
  272. if package in self.model.actions:
  273. self.model.actions.pop(package)
  274. self.reset_model()
  275. def refresh_distribution(self, dist):
  276. self.distribution = dist
  277. if self.process == 'install':
  278. for package in self.model.packages:
  279. pack = dist.find_package(package.name)
  280. if pack is None:
  281. action = INSTALL_ACTION
  282. elif pack.version == package.version:
  283. if self.repair:
  284. action = REPAIR_ACTION
  285. else:
  286. action = NO_REPAIR_ACTION
  287. else:
  288. action = UPGRADE_ACTION + pack.version
  289. self.model.actions[package] = action
  290. else:
  291. self.model.packages = (
  292. self.distribution.get_installed_packages()
  293. )
  294. for package in self.model.packages:
  295. self.model.actions[package] = NONE_ACTION
  296. self.reset_model()
  297. def select_all(self):
  298. allpk = set(self.model.packages)
  299. if self.model.checked == allpk:
  300. self.model.checked = set()
  301. else:
  302. self.model.checked = allpk
  303. self.model.reset()
  304. def dragMoveEvent(self, event):
  305. """Reimplement Qt method, just to avoid default drag'n drop
  306. implementation of QTableView to handle events"""
  307. event.acceptProposedAction()
  308. def dragEnterEvent(self, event):
  309. """Reimplement Qt method
  310. Inform Qt about the types of data that the widget accepts"""
  311. source = event.mimeData()
  312. if source.hasUrls() and mimedata2url(source):
  313. event.acceptProposedAction()
  314. def dropEvent(self, event):
  315. """Reimplement Qt method
  316. Unpack dropped data and handle it"""
  317. source = event.mimeData()
  318. fnames = [
  319. path
  320. for path in mimedata2url(source)
  321. if osp.isfile(path)
  322. ]
  323. self.add_packages(fnames)
  324. event.acceptProposedAction()
  325. class DistributionSelector(QWidget):
  326. """Python distribution selector widget"""
  327. TITLE = 'Select a Python distribution path'
  328. # Signals after PyQt4 old SIGNAL removal
  329. selected_distribution = Signal(str)
  330. def __init__(self, parent):
  331. super(DistributionSelector, self).__init__(parent)
  332. self.browse_btn = None
  333. self.label = None
  334. self.line_edit = None
  335. self.setup_widget()
  336. def set_distribution(self, path):
  337. """Set distribution directory"""
  338. self.line_edit.setText(path)
  339. def setup_widget(self):
  340. """Setup workspace selector widget"""
  341. self.label = QLabel()
  342. self.line_edit = QLineEdit()
  343. self.line_edit.setAlignment(Qt.AlignRight)
  344. self.line_edit.setReadOnly(True)
  345. # self.line_edit.setDisabled(True)
  346. self.browse_btn = QPushButton(
  347. get_std_icon('DirOpenIcon'), "", self
  348. )
  349. self.browse_btn.setToolTip(self.TITLE)
  350. # PyQt4 old SIGNAL:self.connect(self.browse_btn, SIGNAL("clicked()"),
  351. # PyQt4 old SIGNAL: self.select_directory)
  352. self.browse_btn.clicked.connect(
  353. self.select_directory
  354. )
  355. layout = QHBoxLayout()
  356. layout.addWidget(self.label)
  357. layout.addWidget(self.line_edit)
  358. layout.addWidget(self.browse_btn)
  359. layout.setContentsMargins(0, 0, 0, 0)
  360. self.setLayout(layout)
  361. def select_directory(self):
  362. """Select directory"""
  363. basedir = to_text_string(self.line_edit.text())
  364. if not osp.isdir(basedir):
  365. basedir = getcwd()
  366. while True:
  367. directory = getexistingdirectory(
  368. self, self.TITLE, basedir
  369. )
  370. if not directory:
  371. break
  372. if not utils.is_python_distribution(directory):
  373. QMessageBox.warning(
  374. self,
  375. self.TITLE,
  376. "The following directory is not a Python distribution.",
  377. QMessageBox.Ok,
  378. )
  379. basedir = directory
  380. continue
  381. directory = osp.abspath(osp.normpath(directory))
  382. self.set_distribution(directory)
  383. # PyQt4 old SIGNAL: self.emit(SIGNAL('selected_distribution(QString)'), directory)
  384. self.selected_distribution.emit(directory)
  385. break
  386. class Thread(QThread):
  387. """Installation/Uninstallation thread"""
  388. def __init__(self, parent):
  389. QThread.__init__(self, parent)
  390. self.callback = None
  391. self.error = None
  392. def run(self):
  393. try:
  394. self.callback()
  395. except Exception as error:
  396. error_str = str(error)
  397. fs_encoding = (
  398. sys.getfilesystemencoding()
  399. or locale.getpreferredencoding()
  400. )
  401. try:
  402. error_str = error_str.decode(fs_encoding)
  403. except (
  404. UnicodeError,
  405. TypeError,
  406. AttributeError,
  407. ):
  408. pass
  409. self.error = error_str
  410. def python_distribution_infos():
  411. """Return Python distribution infos (not selected distribution but
  412. the one used to run this script)"""
  413. winpyver = os.environ.get('WINPYVER')
  414. if winpyver is None:
  415. return 'Unknown Python distribution'
  416. else:
  417. return 'WinPython ' + winpyver
  418. class PMWindow(QMainWindow):
  419. NAME = 'WinPython Control Panel'
  420. def __init__(self):
  421. QMainWindow.__init__(self)
  422. self.setAttribute(Qt.WA_DeleteOnClose)
  423. self.distribution = None
  424. self.tabwidget = None
  425. self.selector = None
  426. self.table = None
  427. self.untable = None
  428. self.basedir = None
  429. self.select_all_action = None
  430. self.install_action = None
  431. self.uninstall_action = None
  432. self.remove_action = None
  433. self.packages_icon = get_std_icon(
  434. 'FileDialogContentsView'
  435. )
  436. self.setup_window()
  437. def _add_table(self, table, title, icon):
  438. """Add table tab to main tab widget, return button layout"""
  439. widget = QWidget()
  440. tabvlayout = QVBoxLayout()
  441. widget.setLayout(tabvlayout)
  442. tabvlayout.addWidget(table)
  443. btn_layout = QHBoxLayout()
  444. tabvlayout.addLayout(btn_layout)
  445. self.tabwidget.addTab(widget, icon, title)
  446. return btn_layout
  447. def setup_window(self):
  448. """Setup main window"""
  449. self.setWindowTitle(self.NAME)
  450. self.setWindowIcon(get_icon('winpython.svg'))
  451. self.selector = DistributionSelector(self)
  452. # PyQt4 old SIGNAL: self.connect(self.selector, SIGNAL('selected_distribution(QString)'),
  453. # PyQt4 old SIGNAL: self.distribution_changed)
  454. self.selector.selected_distribution.connect(
  455. self.distribution_changed
  456. )
  457. self.table = PackagesTable(
  458. self, 'install', self.NAME
  459. )
  460. # PyQt4 old SIGNAL:self.connect(self.table, SIGNAL('package_added()'),
  461. # PyQt4 old SIGNAL: self.refresh_install_button)
  462. self.table.package_added.connect(
  463. self.refresh_install_button
  464. )
  465. # PyQt4 old SIGNAL: self.connect(self.table, SIGNAL("clicked(QModelIndex)"),
  466. # PyQt4 old SIGNAL: lambda index: self.refresh_install_button())
  467. self.table.clicked.connect(
  468. lambda index: self.refresh_install_button()
  469. )
  470. self.untable = PackagesTable(
  471. self, 'uninstall', self.NAME
  472. )
  473. # PyQt4 old SIGNAL:self.connect(self.untable, SIGNAL("clicked(QModelIndex)"),
  474. # PyQt4 old SIGNAL: lambda index: self.refresh_uninstall_button())
  475. self.untable.clicked.connect(
  476. lambda index: self.refresh_uninstall_button()
  477. )
  478. self.selector.set_distribution(sys.prefix)
  479. self.distribution_changed(sys.prefix)
  480. self.tabwidget = QTabWidget()
  481. # PyQt4 old SIGNAL:self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
  482. # PyQt4 old SIGNAL: self.current_tab_changed)
  483. self.tabwidget.currentChanged.connect(
  484. self.current_tab_changed
  485. )
  486. btn_layout = self._add_table(
  487. self.table,
  488. "Install/upgrade packages",
  489. get_std_icon("ArrowDown"),
  490. )
  491. unbtn_layout = self._add_table(
  492. self.untable,
  493. "Uninstall packages",
  494. get_std_icon("DialogResetButton"),
  495. )
  496. central_widget = QWidget()
  497. vlayout = QVBoxLayout()
  498. vlayout.addWidget(self.selector)
  499. vlayout.addWidget(self.tabwidget)
  500. central_widget.setLayout(vlayout)
  501. self.setCentralWidget(central_widget)
  502. # Install tab
  503. add_action = create_action(
  504. self,
  505. "&Add packages...",
  506. icon=get_std_icon('DialogOpenButton'),
  507. triggered=self.add_packages,
  508. )
  509. self.remove_action = create_action(
  510. self,
  511. "Remove",
  512. shortcut=keybinding('Delete'),
  513. icon=get_std_icon('TrashIcon'),
  514. triggered=self.remove_packages,
  515. )
  516. self.remove_action.setEnabled(False)
  517. self.select_all_action = create_action(
  518. self,
  519. "(Un)Select all",
  520. shortcut=keybinding('SelectAll'),
  521. icon=get_std_icon('DialogYesButton'),
  522. triggered=self.table.select_all,
  523. )
  524. self.install_action = create_action(
  525. self,
  526. "&Install packages",
  527. icon=get_std_icon('DialogApplyButton'),
  528. triggered=lambda: self.process_packages(
  529. 'install'
  530. ),
  531. )
  532. self.install_action.setEnabled(False)
  533. quit_action = create_action(
  534. self,
  535. "&Quit",
  536. icon=get_std_icon('DialogCloseButton'),
  537. triggered=self.close,
  538. )
  539. packages_menu = self.menuBar().addMenu("&Packages")
  540. add_actions(
  541. packages_menu,
  542. [
  543. add_action,
  544. self.remove_action,
  545. self.install_action,
  546. None,
  547. quit_action,
  548. ],
  549. )
  550. # Uninstall tab
  551. self.uninstall_action = create_action(
  552. self,
  553. "&Uninstall packages",
  554. icon=get_std_icon('DialogCancelButton'),
  555. triggered=lambda: self.process_packages(
  556. 'uninstall'
  557. ),
  558. )
  559. self.uninstall_action.setEnabled(False)
  560. uninstall_btn = action2button(
  561. self.uninstall_action,
  562. autoraise=False,
  563. text_beside_icon=True,
  564. )
  565. # Option menu
  566. option_menu = self.menuBar().addMenu("&Options")
  567. repair_action = create_action(
  568. self,
  569. "Repair packages",
  570. tip="Reinstall packages even if version is unchanged",
  571. toggled=self.toggle_repair,
  572. )
  573. add_actions(option_menu, (repair_action,))
  574. # Advanced menu
  575. option_menu = self.menuBar().addMenu("&Advanced")
  576. register_action = create_action(
  577. self,
  578. "Register distribution...",
  579. tip="Register file extensions, icons and context menu",
  580. triggered=self.register_distribution,
  581. )
  582. unregister_action = create_action(
  583. self,
  584. "Unregister distribution...",
  585. tip="Unregister file extensions, icons and context menu",
  586. triggered=self.unregister_distribution,
  587. )
  588. open_console_action = create_action(
  589. self,
  590. "Open console here",
  591. triggered=lambda: os.startfile(
  592. self.command_prompt_path
  593. ),
  594. )
  595. open_console_action.setEnabled(
  596. osp.exists(self.command_prompt_path)
  597. )
  598. add_actions(
  599. option_menu,
  600. (
  601. register_action,
  602. unregister_action,
  603. None,
  604. open_console_action,
  605. ),
  606. )
  607. # # View menu
  608. # view_menu = self.menuBar().addMenu("&View")
  609. # popmenu = self.createPopupMenu()
  610. # add_actions(view_menu, popmenu.actions())
  611. # Help menu
  612. about_action = create_action(
  613. self,
  614. "About %s..." % self.NAME,
  615. icon=get_std_icon('MessageBoxInformation'),
  616. triggered=self.about,
  617. )
  618. report_action = create_action(
  619. self,
  620. "Report issue...",
  621. icon=get_icon('bug.png'),
  622. triggered=self.report_issue,
  623. )
  624. help_menu = self.menuBar().addMenu("?")
  625. add_actions(
  626. help_menu, [about_action, None, report_action]
  627. )
  628. # Status bar
  629. status = self.statusBar()
  630. status.setObjectName("StatusBar")
  631. status.showMessage(
  632. "Welcome to %s!" % self.NAME, 5000
  633. )
  634. # Button layouts
  635. for act in (
  636. add_action,
  637. self.remove_action,
  638. None,
  639. self.select_all_action,
  640. self.install_action,
  641. ):
  642. if act is None:
  643. btn_layout.addStretch()
  644. else:
  645. btn_layout.addWidget(
  646. action2button(
  647. act,
  648. autoraise=False,
  649. text_beside_icon=True,
  650. )
  651. )
  652. unbtn_layout.addWidget(uninstall_btn)
  653. unbtn_layout.addStretch()
  654. self.resize(400, 500)
  655. def current_tab_changed(self, index):
  656. """Current tab has just changed"""
  657. if index == 0:
  658. self.show_drop_tip()
  659. def refresh_install_button(self):
  660. """Refresh install button enable state"""
  661. self.table.refresh_distribution(self.distribution)
  662. self.install_action.setEnabled(
  663. len(self.get_packages_to_be_installed()) > 0
  664. )
  665. nbp = len(self.table.get_selected_packages())
  666. for act in (
  667. self.remove_action,
  668. self.select_all_action,
  669. ):
  670. act.setEnabled(nbp > 0)
  671. self.show_drop_tip()
  672. def show_drop_tip(self):
  673. """Show drop tip on install table"""
  674. callback = lambda: QToolTip.showText(
  675. self.table.mapToGlobal(self.table.pos()),
  676. '<b>Drop files here</b><br>'
  677. 'Executable installers (distutils) or source packages',
  678. self,
  679. )
  680. QTimer.singleShot(500, callback)
  681. def refresh_uninstall_button(self):
  682. """Refresh uninstall button enable state"""
  683. nbp = len(self.untable.get_selected_packages())
  684. self.uninstall_action.setEnabled(nbp > 0)
  685. def toggle_repair(self, state):
  686. """Toggle repair mode"""
  687. self.table.repair = state
  688. self.refresh_install_button()
  689. def register_distribution(self):
  690. """Register distribution"""
  691. answer = QMessageBox.warning(
  692. self,
  693. "Register distribution",
  694. "This will associate file extensions, icons and "
  695. "Windows explorer's context menu entries ('Edit with IDLE', ...) "
  696. "with selected Python distribution in Windows registry. "
  697. "<br>Shortcuts for all WinPython launchers will be installed "
  698. "in <i>WinPython</i> Start menu group (replacing existing "
  699. "shortcuts)."
  700. "<br>If <i>pywin32</i> is installed (it should be on any "
  701. "WinPython distribution), the Python ActiveX Scripting client "
  702. "will also be registered."
  703. "<br><br><u>Warning</u>: the only way to undo this change is to "
  704. "register another Python distribution to Windows registry."
  705. "<br><br><u>Note</u>: these actions are exactly the same as those "
  706. "performed when installing Python with the official installer "
  707. "for Windows.<br><br>Do you want to continue?",
  708. QMessageBox.Yes | QMessageBox.No,
  709. )
  710. if answer == QMessageBox.Yes:
  711. associate.register(self.distribution.target)
  712. def unregister_distribution(self):
  713. """Unregister distribution"""
  714. answer = QMessageBox.warning(
  715. self,
  716. "Unregister distribution",
  717. "This will remove file extensions associations, icons and "
  718. "Windows explorer's context menu entries ('Edit with IDLE', ...) "
  719. "with selected Python distribution in Windows registry. "
  720. "<br>Shortcuts for all WinPython launchers will be removed "
  721. "from <i>WinPython</i> Start menu group."
  722. "<br>If <i>pywin32</i> is installed (it should be on any "
  723. "WinPython distribution), the Python ActiveX Scripting client "
  724. "will also be unregistered."
  725. "<br><br>Do you want to continue?",
  726. QMessageBox.Yes | QMessageBox.No,
  727. )
  728. if answer == QMessageBox.Yes:
  729. associate.unregister(self.distribution.target)
  730. @property
  731. def command_prompt_path(self):
  732. return osp.join(
  733. self.distribution.target,
  734. osp.pardir,
  735. "WinPython Command Prompt.exe",
  736. )
  737. def distribution_changed(self, path):
  738. """Distribution path has just changed"""
  739. for package in self.table.model.packages:
  740. self.table.remove_package(package)
  741. dist = wppm.Distribution(to_text_string(path))
  742. self.table.refresh_distribution(dist)
  743. self.untable.refresh_distribution(dist)
  744. self.distribution = dist
  745. self.selector.label.setText(
  746. 'Python %s %dbit:'
  747. % (dist.version, dist.architecture)
  748. )
  749. def add_packages(self):
  750. """Add packages"""
  751. basedir = (
  752. self.basedir if self.basedir is not None else ''
  753. )
  754. fnames, _selfilter = getopenfilenames(
  755. parent=self,
  756. basedir=basedir,
  757. caption='Add packages',
  758. filters='*.exe *.zip *.tar.gz *.whl',
  759. )
  760. if fnames:
  761. self.basedir = osp.dirname(fnames[0])
  762. self.table.add_packages(fnames)
  763. def get_packages_to_be_installed(self):
  764. """Return packages to be installed"""
  765. return [
  766. pack
  767. for pack in self.table.get_selected_packages()
  768. if self.table.model.actions[pack]
  769. not in (NO_REPAIR_ACTION, NONE_ACTION)
  770. ]
  771. def remove_packages(self):
  772. """Remove selected packages"""
  773. for package in self.table.get_selected_packages():
  774. self.table.remove_package(package)
  775. def process_packages(self, action):
  776. """Install/uninstall packages"""
  777. if action == 'install':
  778. text, table = 'Installing', self.table
  779. if not self.get_packages_to_be_installed():
  780. return
  781. elif action == 'uninstall':
  782. text, table = 'Uninstalling', self.untable
  783. else:
  784. raise AssertionError
  785. packages = table.get_selected_packages()
  786. if not packages:
  787. return
  788. func = getattr(self.distribution, action)
  789. thread = Thread(self)
  790. for widget in self.children():
  791. if isinstance(widget, QWidget):
  792. widget.setEnabled(False)
  793. try:
  794. status = self.statusBar()
  795. except AttributeError:
  796. status = self.parent().statusBar()
  797. progress = QProgressDialog(
  798. self, Qt.FramelessWindowHint
  799. )
  800. progress.setMaximum(
  801. len(packages)
  802. ) # old vicious bug:len(packages)-1
  803. for index, package in enumerate(packages):
  804. progress.setValue(index)
  805. progress.setLabelText(
  806. "%s %s %s..."
  807. % (text, package.name, package.version)
  808. )
  809. QApplication.processEvents()
  810. if progress.wasCanceled():
  811. break
  812. if package in table.model.actions:
  813. try:
  814. thread.callback = lambda: func(package)
  815. thread.start()
  816. while thread.isRunning():
  817. QApplication.processEvents()
  818. if progress.wasCanceled():
  819. status.setEnabled(True)
  820. status.showMessage(
  821. "Cancelling operation..."
  822. )
  823. table.remove_package(package)
  824. error = thread.error
  825. except Exception as error:
  826. error = to_text_string(error)
  827. if error is not None:
  828. pstr = (
  829. package.name + ' ' + package.version
  830. )
  831. QMessageBox.critical(
  832. self,
  833. "Error",
  834. "<b>Unable to %s <i>%s</i></b>"
  835. "<br><br>Error message:<br>%s"
  836. % (action, pstr, error),
  837. )
  838. progress.setValue(progress.maximum())
  839. status.clearMessage()
  840. for widget in self.children():
  841. if isinstance(widget, QWidget):
  842. widget.setEnabled(True)
  843. thread = None
  844. for table in (self.table, self.untable):
  845. table.refresh_distribution(self.distribution)
  846. def report_issue(self):
  847. issue_template = """\
  848. Python distribution: %s
  849. Control panel version: %s
  850. Python Version: %s
  851. Qt Version: %s, %s %s
  852. What steps will reproduce the problem?
  853. 1.
  854. 2.
  855. 3.
  856. What is the expected output? What do you see instead?
  857. Please provide any additional information below.
  858. """ % (
  859. python_distribution_infos(),
  860. __version__,
  861. platform.python_version(),
  862. winpython._vendor.qtpy.QtCore.__version__,
  863. winpython.qt.API_NAME,
  864. winpython._vendor.qtpy.__version__,
  865. )
  866. url = QUrl("%s/issues/entry" % __project_url__)
  867. url.addQueryItem("comment", issue_template)
  868. QDesktopServices.openUrl(url)
  869. def about(self):
  870. """About this program"""
  871. QMessageBox.about(
  872. self,
  873. "About %s" % self.NAME,
  874. """<b>%s %s</b>
  875. <br>Package Manager and Advanced Tasks
  876. <p>Copyright &copy; 2012 Pierre Raybaut
  877. <br>Licensed under the terms of the MIT License
  878. <p>Created, developed and maintained by Pierre Raybaut
  879. <p><a href="%s">WinPython at Github.io</a>: downloads, bug reports,
  880. discussions, etc.</p>
  881. <p>This program is executed by:<br>
  882. <b>%s</b><br>
  883. Python %s, Qt %s, %s qtpy %s"""
  884. % (
  885. self.NAME,
  886. __version__,
  887. __project_url__,
  888. python_distribution_infos(),
  889. platform.python_version(),
  890. winpython._vendor.qtpy.QtCore.__version__,
  891. winpython._vendor.qtpy.API_NAME,
  892. winpython._vendor.qtpy.__version__,
  893. ),
  894. )
  895. def main(test=False):
  896. app = QApplication([])
  897. win = PMWindow()
  898. win.show()
  899. if test:
  900. return app, win
  901. else:
  902. app.exec_()
  903. def test():
  904. app, win = main(test=True)
  905. print(sys.modules)
  906. app.exec_()
  907. if __name__ == "__main__":
  908. main()