|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing Qt/PyQt/PySide related utility functions. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import functools |
|
12 import os |
|
13 import sys |
|
14 import sysconfig |
|
15 |
|
16 from PyQt6.QtCore import QT_VERSION, QDir, QLibraryInfo, QProcess |
|
17 |
|
18 from eric7.EricWidgets.EricApplication import ericApp |
|
19 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities |
|
20 |
|
21 try: |
|
22 from eric7.eric7config import getConfig |
|
23 except ImportError: |
|
24 from eric7config import getConfig |
|
25 |
|
26 ############################################################################### |
|
27 ## Qt utility functions below |
|
28 ############################################################################### |
|
29 |
|
30 |
|
31 def qVersionTuple(): |
|
32 """ |
|
33 Module function to get the Qt version as a tuple. |
|
34 |
|
35 @return Qt version as a tuple |
|
36 @rtype tuple of int |
|
37 """ |
|
38 return ( |
|
39 (QT_VERSION & 0xFF0000) >> 16, |
|
40 (QT_VERSION & 0xFF00) >> 8, |
|
41 QT_VERSION & 0xFF, |
|
42 ) |
|
43 |
|
44 |
|
45 def generateQtToolName(toolname): |
|
46 """ |
|
47 Module function to generate the executable name for a Qt tool like |
|
48 designer. |
|
49 |
|
50 @param toolname base name of the tool (string) |
|
51 @return the Qt tool name without extension (string) |
|
52 """ |
|
53 from eric7 import Preferences |
|
54 |
|
55 return "{0}{1}{2}".format( |
|
56 Preferences.getQt("QtToolsPrefix"), |
|
57 toolname, |
|
58 Preferences.getQt("QtToolsPostfix"), |
|
59 ) |
|
60 |
|
61 |
|
62 def getQtBinariesPath(libexec=False): |
|
63 """ |
|
64 Module function to get the path of the Qt binaries. |
|
65 |
|
66 @param libexec flag indicating to get the path of the executable library |
|
67 (defaults to False) |
|
68 @type bool (optional) |
|
69 @return path of the Qt binaries |
|
70 @rtype str |
|
71 """ |
|
72 from eric7 import Preferences |
|
73 |
|
74 binPath = "" |
|
75 |
|
76 # step 1: check, if the user has configured a tools path |
|
77 qtToolsDir = Preferences.getQt("QtToolsDir") |
|
78 if qtToolsDir: |
|
79 if libexec: |
|
80 binPath = os.path.join(qtToolsDir, "..", "libexec") |
|
81 if not os.path.exists(binPath): |
|
82 binPath = qtToolsDir |
|
83 else: |
|
84 binPath = Preferences.getQt("QtToolsDir") |
|
85 if not os.path.exists(binPath): |
|
86 binPath = "" |
|
87 |
|
88 # step 2: try the qt6_applications package |
|
89 if not binPath: |
|
90 with contextlib.suppress(ImportError): |
|
91 # if qt6-applications is not installed just go to the next step |
|
92 import qt6_applications # __IGNORE_WARNING_I10__ |
|
93 |
|
94 if libexec: |
|
95 binPath = os.path.join( |
|
96 os.path.dirname(qt6_applications.__file__), "Qt", "libexec" |
|
97 ) |
|
98 if not os.path.exists(binPath): |
|
99 binPath = os.path.join( |
|
100 os.path.dirname(qt6_applications.__file__), "Qt", "bin" |
|
101 ) |
|
102 else: |
|
103 binPath = os.path.join( |
|
104 os.path.dirname(qt6_applications.__file__), "Qt", "bin" |
|
105 ) |
|
106 if not os.path.exists(binPath): |
|
107 binPath = "" |
|
108 |
|
109 # step3: determine via QLibraryInfo |
|
110 if not binPath: |
|
111 binPath = ( |
|
112 QLibraryInfo.path(QLibraryInfo.LibraryPath.LibraryExecutablesPath) |
|
113 if libexec |
|
114 else QLibraryInfo.path(QLibraryInfo.LibraryPath.BinariesPath) |
|
115 ) |
|
116 |
|
117 # step 4: determine from used Python interpreter (designer is test object) |
|
118 if not binPath: |
|
119 program = "designer" |
|
120 if OSUtilities.isWindowsPlatform(): |
|
121 program += ".exe" |
|
122 |
|
123 progPath = os.path.join(PythonUtilities.getPythonScriptsDirectory(), program) |
|
124 if os.path.exists(progPath): |
|
125 binPath = PythonUtilities.getPythonScriptsDirectory() |
|
126 |
|
127 return QDir.toNativeSeparators(binPath) |
|
128 |
|
129 |
|
130 def getQtMacBundle(toolname): |
|
131 """ |
|
132 Module function to determine the correct Mac OS X bundle name for Qt tools. |
|
133 |
|
134 @param toolname plain name of the tool (e.g. "designer") (string) |
|
135 @return bundle name of the Qt tool (string) |
|
136 """ |
|
137 qtDir = getQtBinariesPath() |
|
138 bundles = [ |
|
139 os.path.join(qtDir, "bin", generateQtToolName(toolname.capitalize())) + ".app", |
|
140 os.path.join(qtDir, "bin", generateQtToolName(toolname)) + ".app", |
|
141 os.path.join(qtDir, generateQtToolName(toolname.capitalize())) + ".app", |
|
142 os.path.join(qtDir, generateQtToolName(toolname)) + ".app", |
|
143 ] |
|
144 if toolname == "designer": |
|
145 # support the standalone Qt Designer installer from |
|
146 # https://build-system.fman.io/qt-designer-download |
|
147 designer = "Qt Designer.app" |
|
148 bundles.extend( |
|
149 [ |
|
150 os.path.join(qtDir, "bin", designer), |
|
151 os.path.join(qtDir, designer), |
|
152 ] |
|
153 ) |
|
154 for bundle in bundles: |
|
155 if os.path.exists(bundle): |
|
156 return bundle |
|
157 return "" |
|
158 |
|
159 |
|
160 def prepareQtMacBundle(toolname, args): |
|
161 """ |
|
162 Module function for starting Qt tools that are Mac OS X bundles. |
|
163 |
|
164 @param toolname plain name of the tool (e.g. "designer") |
|
165 @type str |
|
166 @param args name of input file for tool, if any |
|
167 @type list of str |
|
168 @return command-name and args for QProcess |
|
169 @rtype tuple of (str, list of str) |
|
170 """ |
|
171 fullBundle = getQtMacBundle(toolname) |
|
172 if fullBundle == "": |
|
173 return ("", []) |
|
174 |
|
175 newArgs = [] |
|
176 newArgs.append("-a") |
|
177 newArgs.append(fullBundle) |
|
178 if args: |
|
179 newArgs.append("--args") |
|
180 newArgs += args |
|
181 |
|
182 return ("open", newArgs) |
|
183 |
|
184 |
|
185 ############################################################################### |
|
186 ## PyQt utility functions below |
|
187 ############################################################################### |
|
188 |
|
189 |
|
190 def getPyQt6ModulesDirectory(): |
|
191 """ |
|
192 Function to determine the path to PyQt6 modules directory. |
|
193 |
|
194 @return path to the PyQt6 modules directory |
|
195 @rtype str |
|
196 """ |
|
197 pyqtPath = os.path.join(sysconfig.get_path("platlib"), "PyQt6") |
|
198 if os.path.exists(pyqtPath): |
|
199 return pyqtPath |
|
200 |
|
201 return "" |
|
202 |
|
203 |
|
204 def getPyQtToolsPath(version=5): |
|
205 """ |
|
206 Module function to get the path of the PyQt tools. |
|
207 |
|
208 @param version PyQt major version |
|
209 @type int |
|
210 @return path to the PyQt tools |
|
211 @rtype str |
|
212 """ |
|
213 from eric7 import Preferences |
|
214 from eric7.EricWidgets.EricApplication import ericApp |
|
215 |
|
216 toolsPath = "" |
|
217 |
|
218 # step 1: check, if the user has configured a tools path |
|
219 if version == 5: |
|
220 toolsPath = Preferences.getQt("PyQtToolsDir") |
|
221 venvName = Preferences.getQt("PyQtVenvName") |
|
222 elif version == 6: |
|
223 toolsPath = Preferences.getQt("PyQt6ToolsDir") |
|
224 venvName = Preferences.getQt("PyQt6VenvName") |
|
225 |
|
226 # step 2: determine from used Python interpreter (pylupdate is test object) |
|
227 if not toolsPath: |
|
228 program = "pylupdate{0}".format(version) |
|
229 if venvName: |
|
230 venvManager = ericApp().getObject("VirtualEnvManager") |
|
231 dirName = venvManager.getVirtualenvDirectory(venvName) |
|
232 else: |
|
233 dirName = os.path.dirname(sys.executable) |
|
234 |
|
235 if OSUtilities.isWindowsPlatform(): |
|
236 program += ".exe" |
|
237 if os.path.exists(os.path.join(dirName, program)): |
|
238 toolsPath = dirName |
|
239 elif os.path.exists(os.path.join(dirName, "Scripts", program)): |
|
240 toolsPath = os.path.join(dirName, "Scripts") |
|
241 else: |
|
242 if os.path.exists(os.path.join(dirName, program)): |
|
243 toolsPath = dirName |
|
244 elif os.path.exists(os.path.join(dirName, "bin", program)): |
|
245 toolsPath = os.path.join(dirName, "bin") |
|
246 |
|
247 return toolsPath |
|
248 |
|
249 |
|
250 def generatePyQtToolPath(toolname, alternatives=None): |
|
251 """ |
|
252 Module function to generate the executable path for a PyQt tool. |
|
253 |
|
254 @param toolname base name of the tool |
|
255 @type str |
|
256 @param alternatives list of alternative tool names to try |
|
257 @type list of str |
|
258 @return executable path name of the tool |
|
259 @rtype str |
|
260 """ |
|
261 pyqtVariant = int(toolname[-1]) |
|
262 pyqtToolsPath = getPyQtToolsPath(pyqtVariant) |
|
263 if pyqtToolsPath: |
|
264 exe = os.path.join(pyqtToolsPath, toolname) |
|
265 if OSUtilities.isWindowsPlatform(): |
|
266 exe += ".exe" |
|
267 else: |
|
268 if OSUtilities.isWindowsPlatform(): |
|
269 exe = OSUtilities.getWindowsExecutablePath(toolname) |
|
270 else: |
|
271 exe = toolname |
|
272 |
|
273 if not FileSystemUtilities.isinpath(exe) and alternatives: |
|
274 ex_ = generatePyQtToolPath(alternatives[0], alternatives[1:]) |
|
275 if FileSystemUtilities.isinpath(ex_): |
|
276 exe = ex_ |
|
277 |
|
278 return exe |
|
279 |
|
280 |
|
281 ############################################################################### |
|
282 ## PySide2/PySide6 utility functions below |
|
283 ############################################################################### |
|
284 |
|
285 |
|
286 def generatePySideToolPath(toolname, variant=2): |
|
287 """ |
|
288 Module function to generate the executable path for a PySide2/PySide6 tool. |
|
289 |
|
290 @param toolname base name of the tool |
|
291 @type str |
|
292 @param variant indicator for the PySide variant |
|
293 @type int or str |
|
294 @return the PySide2/PySide6 tool path with extension |
|
295 @rtype str |
|
296 """ |
|
297 from eric7 import Preferences |
|
298 |
|
299 if OSUtilities.isWindowsPlatform(): |
|
300 hasPyside = checkPyside(variant) |
|
301 if not hasPyside: |
|
302 return "" |
|
303 |
|
304 venvName = Preferences.getQt("PySide{0}VenvName".format(variant)) |
|
305 if not venvName: |
|
306 venvName = Preferences.getDebugger("Python3VirtualEnv") |
|
307 interpreter = ( |
|
308 ericApp().getObject("VirtualEnvManager").getVirtualenvInterpreter(venvName) |
|
309 ) |
|
310 if interpreter == "" or not FileSystemUtilities.isinpath(interpreter): |
|
311 interpreter = PythonUtilities.getPythonExecutable() |
|
312 prefix = os.path.dirname(interpreter) |
|
313 if not prefix.endswith("Scripts"): |
|
314 prefix = os.path.join(prefix, "Scripts") |
|
315 return os.path.join(prefix, toolname + ".exe") |
|
316 else: |
|
317 # step 1: check, if the user has configured a tools path |
|
318 path = Preferences.getQt("PySide{0}ToolsDir".format(variant)) |
|
319 if path: |
|
320 return os.path.join(path, toolname) |
|
321 |
|
322 # step 2: determine from used Python interpreter |
|
323 dirName = os.path.dirname(sys.executable) |
|
324 if os.path.exists(os.path.join(dirName, toolname)): |
|
325 return os.path.join(dirName, toolname) |
|
326 |
|
327 return toolname |
|
328 |
|
329 |
|
330 @functools.lru_cache() |
|
331 def checkPyside(variant=2): |
|
332 """ |
|
333 Module function to check the presence of PySide2/PySide6. |
|
334 |
|
335 @param variant indicator for the PySide variant |
|
336 @type int or str |
|
337 @return flags indicating the presence of PySide2/PySide6 |
|
338 @rtype bool |
|
339 """ |
|
340 from eric7 import Preferences |
|
341 |
|
342 venvName = Preferences.getQt("PySide{0}VenvName".format(variant)) |
|
343 if not venvName: |
|
344 venvName = Preferences.getDebugger("Python3VirtualEnv") |
|
345 interpreter = ( |
|
346 ericApp().getObject("VirtualEnvManager").getVirtualenvInterpreter(venvName) |
|
347 ) |
|
348 if interpreter == "" or not FileSystemUtilities.isinpath(interpreter): |
|
349 interpreter = PythonUtilities.getPythonExecutable() |
|
350 |
|
351 checker = os.path.join(getConfig("ericDir"), "SystemUtilities", "PySideImporter.py") |
|
352 args = [checker, "--variant={0}".format(variant)] |
|
353 proc = QProcess() |
|
354 proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels) |
|
355 proc.start(interpreter, args) |
|
356 finished = proc.waitForFinished(30000) |
|
357 return finished and proc.exitCode() == 0 |