|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2006 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Programs page. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import re |
|
12 |
|
13 from PyQt6.QtCore import pyqtSlot, Qt, QProcess |
|
14 from PyQt6.QtWidgets import ( |
|
15 QApplication, QTreeWidgetItem, QHeaderView, QDialog, QDialogButtonBox |
|
16 ) |
|
17 |
|
18 from EricWidgets.EricApplication import ericApp |
|
19 from EricGui.EricOverrideCursor import EricOverrideCursor |
|
20 |
|
21 from .Ui_ProgramsDialog import Ui_ProgramsDialog |
|
22 |
|
23 import Globals |
|
24 import Preferences |
|
25 import Utilities |
|
26 |
|
27 |
|
28 class ProgramsDialog(QDialog, Ui_ProgramsDialog): |
|
29 """ |
|
30 Class implementing the Programs page. |
|
31 """ |
|
32 ToolAvailableRole = Qt.ItemDataRole.UserRole + 1 |
|
33 |
|
34 def __init__(self, parent=None): |
|
35 """ |
|
36 Constructor |
|
37 |
|
38 @param parent The parent widget of this dialog. (QWidget) |
|
39 """ |
|
40 super().__init__(parent) |
|
41 self.setupUi(self) |
|
42 self.setObjectName("ProgramsDialog") |
|
43 self.setWindowFlags(Qt.WindowType.Window) |
|
44 |
|
45 self.__hasSearched = False |
|
46 |
|
47 self.programsList.headerItem().setText( |
|
48 self.programsList.columnCount(), "") |
|
49 |
|
50 self.searchButton = self.buttonBox.addButton( |
|
51 self.tr("Search"), QDialogButtonBox.ButtonRole.ActionRole) |
|
52 self.searchButton.setToolTip( |
|
53 self.tr("Press to search for programs")) |
|
54 |
|
55 self.showComboBox.addItems([ |
|
56 self.tr("All Supported Tools"), |
|
57 self.tr("Available Tools Only"), |
|
58 self.tr("Unavailable Tools Only"), |
|
59 ]) |
|
60 |
|
61 def show(self): |
|
62 """ |
|
63 Public slot to show the dialog. |
|
64 """ |
|
65 QDialog.show(self) |
|
66 if not self.__hasSearched: |
|
67 self.on_programsSearchButton_clicked() |
|
68 |
|
69 def on_buttonBox_clicked(self, button): |
|
70 """ |
|
71 Private slot called by a button of the button box clicked. |
|
72 |
|
73 @param button button that was clicked (QAbstractButton) |
|
74 """ |
|
75 if button == self.searchButton: |
|
76 self.on_programsSearchButton_clicked() |
|
77 |
|
78 @pyqtSlot() |
|
79 def on_programsSearchButton_clicked(self): |
|
80 """ |
|
81 Private slot to search for all supported/required programs. |
|
82 """ |
|
83 self.programsList.clear() |
|
84 header = self.programsList.header() |
|
85 header.setSortIndicator(0, Qt.SortOrder.AscendingOrder) |
|
86 header.setSortIndicatorShown(False) |
|
87 |
|
88 with EricOverrideCursor(): |
|
89 # 1. do the Qt programs |
|
90 # 1a. Translation Converter |
|
91 exe = ( |
|
92 Utilities.isWindowsPlatform() and |
|
93 "{0}.exe".format(Utilities.generateQtToolName("lrelease")) or |
|
94 Utilities.generateQtToolName("lrelease") |
|
95 ) |
|
96 exe = os.path.join(Utilities.getQtBinariesPath(), exe) |
|
97 version = self.__createProgramEntry( |
|
98 self.tr("Translation Converter (Qt)"), exe, '-version', |
|
99 'lrelease', -1) |
|
100 # 1b. Qt Designer |
|
101 if Utilities.isWindowsPlatform(): |
|
102 exe = os.path.join( |
|
103 Utilities.getQtBinariesPath(), |
|
104 "{0}.exe".format(Utilities.generateQtToolName("designer"))) |
|
105 elif Utilities.isMacPlatform(): |
|
106 exe = Utilities.getQtMacBundle("designer") |
|
107 else: |
|
108 exe = os.path.join( |
|
109 Utilities.getQtBinariesPath(), |
|
110 Utilities.generateQtToolName("designer")) |
|
111 self.__createProgramEntry( |
|
112 self.tr("Qt Designer"), exe, version=version) |
|
113 # 1c. Qt Linguist |
|
114 if Utilities.isWindowsPlatform(): |
|
115 exe = os.path.join( |
|
116 Utilities.getQtBinariesPath(), |
|
117 "{0}.exe".format(Utilities.generateQtToolName("linguist"))) |
|
118 elif Utilities.isMacPlatform(): |
|
119 exe = Utilities.getQtMacBundle("linguist") |
|
120 else: |
|
121 exe = os.path.join( |
|
122 Utilities.getQtBinariesPath(), |
|
123 Utilities.generateQtToolName("linguist")) |
|
124 self.__createProgramEntry( |
|
125 self.tr("Qt Linguist"), exe, version=version) |
|
126 # 1d. Qt Assistant |
|
127 if Utilities.isWindowsPlatform(): |
|
128 exe = os.path.join( |
|
129 Utilities.getQtBinariesPath(), |
|
130 "{0}.exe".format( |
|
131 Utilities.generateQtToolName("assistant"))) |
|
132 elif Utilities.isMacPlatform(): |
|
133 exe = Utilities.getQtMacBundle("assistant") |
|
134 else: |
|
135 exe = os.path.join( |
|
136 Utilities.getQtBinariesPath(), |
|
137 Utilities.generateQtToolName("assistant")) |
|
138 self.__createProgramEntry( |
|
139 self.tr("Qt Assistant"), exe, version=version) |
|
140 |
|
141 # 2. do the PyQt programs |
|
142 # 2.1 do the PyQt5 programs |
|
143 # 2.1a. Translation Extractor PyQt5 |
|
144 self.__createProgramEntry( |
|
145 self.tr("Translation Extractor (Python, PyQt5)"), |
|
146 Utilities.generatePyQtToolPath("pylupdate5"), |
|
147 '-version', 'pylupdate', -1) |
|
148 # 2.1b. Forms Compiler PyQt5 |
|
149 self.__createProgramEntry( |
|
150 self.tr("Forms Compiler (Python, PyQt5)"), |
|
151 Utilities.generatePyQtToolPath("pyuic5", ["py3uic5"]), |
|
152 '--version', 'Python User', 4) |
|
153 # 2.1c. Resource Compiler PyQt5 |
|
154 self.__createProgramEntry( |
|
155 self.tr("Resource Compiler (Python, PyQt5)"), |
|
156 Utilities.generatePyQtToolPath("pyrcc5"), |
|
157 '-version', '', -1, versionRe='Resource Compiler|pyrcc5') |
|
158 |
|
159 # 2.2 do the PyQt6 programs |
|
160 # 2.2a. Translation Extractor PyQt6 |
|
161 self.__createProgramEntry( |
|
162 self.tr("Translation Extractor (Python, PyQt6)"), |
|
163 Utilities.generatePyQtToolPath("pylupdate6"), |
|
164 '--version', versionPosition=0) |
|
165 # 2.2b. Forms Compiler PyQt6 |
|
166 self.__createProgramEntry( |
|
167 self.tr("Forms Compiler (Python, PyQt6)"), |
|
168 Utilities.generatePyQtToolPath("pyuic6"), |
|
169 '--version', versionPosition=0) |
|
170 |
|
171 # 3. do the PySide programs |
|
172 # 3.1 do the PySide2 programs |
|
173 # 3.1a. Translation Extractor PySide2 |
|
174 self.__createProgramEntry( |
|
175 self.tr("Translation Extractor (Python, PySide2)"), |
|
176 Utilities.generatePySideToolPath("pyside2-lupdate", variant=2), |
|
177 '-version', '', -1, versionRe='lupdate') |
|
178 # 3.1b. Forms Compiler PySide2 |
|
179 self.__createProgramEntry( |
|
180 self.tr("Forms Compiler (Python, PySide2)"), |
|
181 Utilities.generatePySideToolPath("pyside2-uic", variant=2), |
|
182 '--version', '', -1, versionRe='uic') |
|
183 # 3.1c Resource Compiler PySide2 |
|
184 self.__createProgramEntry( |
|
185 self.tr("Resource Compiler (Python, PySide2)"), |
|
186 Utilities.generatePySideToolPath("pyside2-rcc", variant=2), |
|
187 '-version', '', -1, versionRe='rcc') |
|
188 # 3.2 do the PySide5 programs |
|
189 # 3.2a. Translation Extractor PySide6 |
|
190 self.__createProgramEntry( |
|
191 self.tr("Translation Extractor (Python, PySide6)"), |
|
192 Utilities.generatePySideToolPath("pyside6-lupdate", variant=6), |
|
193 '-version', '', -1, versionRe='lupdate') |
|
194 # 3.2b. Forms Compiler PySide6 |
|
195 self.__createProgramEntry( |
|
196 self.tr("Forms Compiler (Python, PySide6)"), |
|
197 Utilities.generatePySideToolPath("pyside6-uic", variant=6), |
|
198 '--version', '', -1, versionRe='uic') |
|
199 # 3.2c Resource Compiler PySide6 |
|
200 self.__createProgramEntry( |
|
201 self.tr("Resource Compiler (Python, PySide6)"), |
|
202 Utilities.generatePySideToolPath("pyside6-rcc", variant=6), |
|
203 '--version', '', -1, versionRe='rcc') |
|
204 |
|
205 # 4. do the Conda program(s) |
|
206 exe = Preferences.getConda("CondaExecutable") |
|
207 if not exe: |
|
208 exe = "conda" |
|
209 if Utilities.isWindowsPlatform(): |
|
210 exe += ".exe" |
|
211 self.__createProgramEntry( |
|
212 self.tr("conda Manager"), exe, '--version', 'conda', -1) |
|
213 |
|
214 # 5. do the pip program(s) |
|
215 virtualenvManager = ericApp().getObject("VirtualEnvManager") |
|
216 for venvName in virtualenvManager.getVirtualenvNames(): |
|
217 interpreter = virtualenvManager.getVirtualenvInterpreter( |
|
218 venvName) |
|
219 self.__createProgramEntry( |
|
220 self.tr("PyPI Package Management"), interpreter, |
|
221 '--version', 'pip', 1, exeModule=["-m", "pip"]) |
|
222 |
|
223 # 6. do the CORBA and Protobuf programs |
|
224 # 6a. omniORB |
|
225 exe = Preferences.getCorba("omniidl") |
|
226 if not exe: |
|
227 exe = "omniidl" |
|
228 if Utilities.isWindowsPlatform(): |
|
229 exe += ".exe" |
|
230 self.__createProgramEntry( |
|
231 self.tr("CORBA IDL Compiler"), exe, '-V', 'omniidl', -1) |
|
232 # 6b. protobuf |
|
233 exe = Preferences.getProtobuf("protoc") |
|
234 if not exe: |
|
235 exe = "protoc" |
|
236 if Utilities.isWindowsPlatform(): |
|
237 exe += ".exe" |
|
238 self.__createProgramEntry( |
|
239 self.tr("Protobuf Compiler"), exe, '--version', 'libprotoc', |
|
240 -1) |
|
241 # 6c. grpc |
|
242 exe = Preferences.getProtobuf("grpcPython") |
|
243 if not exe: |
|
244 exe = Globals.getPythonExecutable() |
|
245 self.__createProgramEntry( |
|
246 self.tr("gRPC Compiler"), exe, '--version', 'libprotoc', -1, |
|
247 exeModule=['-m', 'grpc_tools.protoc']) |
|
248 |
|
249 # 7. do the spell checking entry |
|
250 try: |
|
251 import enchant |
|
252 try: |
|
253 text = os.path.dirname(enchant.__file__) |
|
254 except AttributeError: |
|
255 text = "enchant" |
|
256 try: |
|
257 version = enchant.__version__ |
|
258 except AttributeError: |
|
259 version = self.tr("(unknown)") |
|
260 except (ImportError, AttributeError, OSError): |
|
261 text = "enchant" |
|
262 version = "" |
|
263 self.__createEntry( |
|
264 self.tr("Spell Checker - PyEnchant"), text, version) |
|
265 |
|
266 # 8. do the pygments entry |
|
267 try: |
|
268 import pygments |
|
269 try: |
|
270 text = os.path.dirname(pygments.__file__) |
|
271 except AttributeError: |
|
272 text = "pygments" |
|
273 try: |
|
274 version = pygments.__version__ |
|
275 except AttributeError: |
|
276 version = self.tr("(unknown)") |
|
277 except (ImportError, AttributeError, OSError): |
|
278 text = "pygments" |
|
279 version = "" |
|
280 self.__createEntry( |
|
281 self.tr("Source Highlighter - Pygments"), text, version) |
|
282 |
|
283 # 9. do the MicroPython related entries |
|
284 exe = Preferences.getMicroPython("MpyCrossCompiler") |
|
285 if not exe: |
|
286 exe = "mpy-cross" |
|
287 self.__createProgramEntry( |
|
288 self.tr("MicroPython - MPY Cross Compiler"), exe, '--version', |
|
289 'MicroPython', 1) |
|
290 self.__createProgramEntry( |
|
291 self.tr("MicroPython - ESP Tool"), |
|
292 Globals.getPythonExecutable(), 'version', |
|
293 'esptool', -1, exeModule=['-m', 'esptool']) |
|
294 exe = Preferences.getMicroPython("DfuUtilPath") |
|
295 if not exe: |
|
296 exe = "dfu-util" |
|
297 self.__createProgramEntry( |
|
298 self.tr("MicroPython - PyBoard Flasher"), exe, '--version', |
|
299 'dfu-util', -1) |
|
300 |
|
301 # 10. do the jedi related entries |
|
302 try: |
|
303 import jedi |
|
304 try: |
|
305 text = os.path.dirname(jedi.__file__) |
|
306 except AttributeError: |
|
307 text = "jedi" |
|
308 try: |
|
309 version = jedi.__version__ |
|
310 except AttributeError: |
|
311 version = self.tr("(unknown)") |
|
312 except (ImportError, AttributeError, OSError): |
|
313 text = "jedi" |
|
314 version = "" |
|
315 self.__createEntry( |
|
316 self.tr("Code Assistant - Jedi"), text, version) |
|
317 |
|
318 # 11. do the plugin related programs |
|
319 pm = ericApp().getObject("PluginManager") |
|
320 for info in pm.getPluginExeDisplayData(): |
|
321 if info["programEntry"]: |
|
322 if "exeModule" not in info: |
|
323 info["exeModule"] = None |
|
324 if "versionRe" not in info: |
|
325 info["versionRe"] = None |
|
326 self.__createProgramEntry( |
|
327 info["header"], |
|
328 info["exe"], |
|
329 versionCommand=info["versionCommand"], |
|
330 versionStartsWith=info["versionStartsWith"], |
|
331 versionPosition=info["versionPosition"], |
|
332 version=info["version"], |
|
333 versionCleanup=info["versionCleanup"], |
|
334 versionRe=info["versionRe"], |
|
335 exeModule=info["exeModule"], |
|
336 ) |
|
337 else: |
|
338 self.__createEntry( |
|
339 info["header"], |
|
340 info["text"], |
|
341 info["version"] |
|
342 ) |
|
343 |
|
344 self.programsList.sortByColumn(0, Qt.SortOrder.AscendingOrder) |
|
345 self.on_showComboBox_currentIndexChanged( |
|
346 self.showComboBox.currentIndex()) |
|
347 |
|
348 self.__hasSearched = True |
|
349 |
|
350 def __createProgramEntry(self, description, exe, |
|
351 versionCommand="", versionStartsWith="", |
|
352 versionPosition=None, version="", |
|
353 versionCleanup=None, versionRe=None, |
|
354 exeModule=None): |
|
355 """ |
|
356 Private method to generate a program entry. |
|
357 |
|
358 @param description descriptive text (string) |
|
359 @param exe name of the executable program (string) |
|
360 @param versionCommand command line switch to get the version info |
|
361 (str). If this is empty, the given version will be shown. |
|
362 @param versionStartsWith start of line identifying version info |
|
363 (string) |
|
364 @param versionPosition index of part containing the version info |
|
365 (integer) |
|
366 @param version version string to show (string) |
|
367 @param versionCleanup tuple of two integers giving string positions |
|
368 start and stop for the version string (tuple of integers) |
|
369 @param versionRe regexp to determine the line identifying version |
|
370 info (string). Takes precedence over versionStartsWith. |
|
371 @param exeModule list of command line parameters to execute a module |
|
372 with the program given in exe (e.g. to execute a Python module) |
|
373 (list of str) |
|
374 @return version string of detected or given version (string) |
|
375 """ |
|
376 itmList = self.programsList.findItems( |
|
377 description, Qt.MatchFlag.MatchCaseSensitive) |
|
378 itm = ( |
|
379 itmList[0] |
|
380 if itmList else |
|
381 QTreeWidgetItem(self.programsList, [description]) |
|
382 ) |
|
383 font = itm.font(0) |
|
384 font.setBold(True) |
|
385 itm.setFont(0, font) |
|
386 rememberedExe = exe |
|
387 if not exe: |
|
388 itm.setText(1, self.tr("(not configured)")) |
|
389 else: |
|
390 if os.path.isabs(exe): |
|
391 if not Utilities.isExecutable(exe): |
|
392 exe = "" |
|
393 else: |
|
394 exe = Utilities.getExecutablePath(exe) |
|
395 if exe: |
|
396 available = True |
|
397 if versionCommand and versionPosition is not None: |
|
398 proc = QProcess() |
|
399 proc.setProcessChannelMode( |
|
400 QProcess.ProcessChannelMode.MergedChannels) |
|
401 if exeModule: |
|
402 args = exeModule[:] + [versionCommand] |
|
403 else: |
|
404 args = [versionCommand] |
|
405 proc.start(exe, args) |
|
406 finished = proc.waitForFinished(10000) |
|
407 if finished: |
|
408 output = str(proc.readAllStandardOutput(), |
|
409 Preferences.getSystem("IOEncoding"), |
|
410 'replace') |
|
411 if ( |
|
412 exeModule and |
|
413 exeModule[0] == "-m" and |
|
414 ("ImportError:" in output or |
|
415 "ModuleNotFoundError:" in output or |
|
416 proc.exitCode() != 0) |
|
417 ): |
|
418 version = self.tr("(module not found)") |
|
419 available = False |
|
420 elif not versionStartsWith and not versionRe: |
|
421 # assume output is just one line |
|
422 try: |
|
423 version = ( |
|
424 output.strip().split()[versionPosition] |
|
425 ) |
|
426 if versionCleanup: |
|
427 version = version[ |
|
428 versionCleanup[0]: |
|
429 versionCleanup[1] |
|
430 ] |
|
431 except IndexError: |
|
432 version = self.tr("(unknown)") |
|
433 available = False |
|
434 else: |
|
435 if versionRe is None: |
|
436 versionRe = "^{0}".format( |
|
437 re.escape(versionStartsWith)) |
|
438 versionRe = re.compile(versionRe, re.UNICODE) |
|
439 for line in output.splitlines(): |
|
440 if versionRe.search(line): |
|
441 try: |
|
442 version = line.split()[versionPosition] |
|
443 if versionCleanup: |
|
444 version = version[ |
|
445 versionCleanup[0]: |
|
446 versionCleanup[1] |
|
447 ] |
|
448 break |
|
449 except IndexError: |
|
450 version = self.tr("(unknown)") |
|
451 available = False |
|
452 else: |
|
453 version = self.tr("(unknown)") |
|
454 available = False |
|
455 else: |
|
456 version = self.tr("(not executable)") |
|
457 available = False |
|
458 if exeModule: |
|
459 citm = QTreeWidgetItem(itm, [ |
|
460 "{0} {1}".format(exe, " ".join(exeModule)), |
|
461 version]) |
|
462 else: |
|
463 citm = QTreeWidgetItem(itm, [exe, version]) |
|
464 citm.setData(0, self.ToolAvailableRole, available) |
|
465 itm.setExpanded(True) |
|
466 else: |
|
467 if itm.childCount() == 0: |
|
468 itm.setText(1, self.tr("(not found)")) |
|
469 else: |
|
470 citm = QTreeWidgetItem( |
|
471 itm, [rememberedExe, self.tr("(not found)")]) |
|
472 citm.setData(0, self.ToolAvailableRole, False) |
|
473 itm.setExpanded(True) |
|
474 QApplication.processEvents() |
|
475 self.programsList.header().resizeSections( |
|
476 QHeaderView.ResizeMode.ResizeToContents) |
|
477 self.programsList.header().setStretchLastSection(True) |
|
478 return version |
|
479 |
|
480 def __createEntry(self, description, entryText, entryVersion): |
|
481 """ |
|
482 Private method to generate a program entry. |
|
483 |
|
484 @param description descriptive text (string) |
|
485 @param entryText text to show (string) |
|
486 @param entryVersion version string to show (string). |
|
487 """ |
|
488 itm = QTreeWidgetItem(self.programsList, [description]) |
|
489 font = itm.font(0) |
|
490 font.setBold(True) |
|
491 itm.setFont(0, font) |
|
492 |
|
493 if len(entryVersion): |
|
494 citm = QTreeWidgetItem(itm, [entryText, entryVersion]) |
|
495 itm.setExpanded(True) |
|
496 citm.setData(0, self.ToolAvailableRole, |
|
497 not entryVersion.startswith("(")) |
|
498 # assume version starting with '(' is an unavailability |
|
499 else: |
|
500 itm.setText(1, self.tr("(not found)")) |
|
501 QApplication.processEvents() |
|
502 self.programsList.header().resizeSections( |
|
503 QHeaderView.ResizeMode.ResizeToContents) |
|
504 self.programsList.header().setStretchLastSection(True) |
|
505 |
|
506 @pyqtSlot(int) |
|
507 def on_showComboBox_currentIndexChanged(self, index): |
|
508 """ |
|
509 Private slot to apply the selected show criteria. |
|
510 |
|
511 @param index index of the show criterium |
|
512 @type int |
|
513 """ |
|
514 if index == 0: |
|
515 # All Supported Tools |
|
516 for topIndex in range(self.programsList.topLevelItemCount()): |
|
517 topItem = self.programsList.topLevelItem(topIndex) |
|
518 for childIndex in range(topItem.childCount()): |
|
519 topItem.child(childIndex).setHidden(False) |
|
520 topItem.setHidden(False) |
|
521 else: |
|
522 # 1 = Available Tools Only |
|
523 # 2 = Unavailable Tools Only |
|
524 for topIndex in range(self.programsList.topLevelItemCount()): |
|
525 topItem = self.programsList.topLevelItem(topIndex) |
|
526 if topItem.childCount() == 0: |
|
527 topItem.setHidden(index == 1) |
|
528 else: |
|
529 availabilityList = [] |
|
530 for childIndex in range(topItem.childCount()): |
|
531 childItem = topItem.child(childIndex) |
|
532 available = childItem.data(0, self.ToolAvailableRole) |
|
533 if index == 1: |
|
534 childItem.setHidden(not available) |
|
535 else: |
|
536 childItem.setHidden(available) |
|
537 availabilityList.append(available) |
|
538 if index == 1: |
|
539 topItem.setHidden(not any(availabilityList)) |
|
540 else: |
|
541 topItem.setHidden(all(availabilityList)) |