|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the standalone MicroPython window. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 |
|
12 from PyQt6.QtCore import QSize, Qt, QUrl, pyqtSignal, pyqtSlot |
|
13 from PyQt6.QtGui import QDesktopServices |
|
14 from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkProxyFactory |
|
15 from PyQt6.QtWidgets import QDialog, QSplitter, QWidget |
|
16 |
|
17 from eric7 import Preferences |
|
18 from eric7.EricNetwork.EricNetworkProxyFactory import ( |
|
19 EricNetworkProxyFactory, |
|
20 proxyAuthenticationRequired, |
|
21 ) |
|
22 from eric7.EricWidgets import EricMessageBox |
|
23 from eric7.EricWidgets.EricApplication import ericApp |
|
24 from eric7.EricWidgets.EricMainWindow import EricMainWindow |
|
25 from eric7.EricWidgets.EricSideBar import EricSideBar, EricSideBarSide |
|
26 from eric7.MicroPython.MicroPythonWidget import MicroPythonWidget |
|
27 from eric7.PipInterface.Pip import Pip |
|
28 from eric7.QScintilla.MiniEditor import MiniEditor |
|
29 from eric7.SystemUtilities import FileSystemUtilities |
|
30 |
|
31 try: |
|
32 from eric7.EricNetwork.EricSslErrorHandler import ( |
|
33 EricSslErrorHandler, |
|
34 EricSslErrorState, |
|
35 ) |
|
36 |
|
37 SSL_AVAILABLE = True |
|
38 except ImportError: |
|
39 SSL_AVAILABLE = False |
|
40 |
|
41 |
|
42 class MicroPythonWindow(EricMainWindow): |
|
43 """ |
|
44 Class implementing the standalone MicroPython window. |
|
45 |
|
46 @signal preferencesChanged() emitted after the preferences were changed |
|
47 """ |
|
48 |
|
49 preferencesChanged = pyqtSignal() |
|
50 |
|
51 def __init__(self, parent=None): |
|
52 """ |
|
53 Constructor |
|
54 |
|
55 @param parent reference to the parent widget |
|
56 @type QWidget |
|
57 """ |
|
58 super().__init__(parent) |
|
59 |
|
60 self.__pip = Pip(self) |
|
61 |
|
62 # create the window layout |
|
63 self.__mpyWidget = MicroPythonWidget(parent=self, forMPyWindow=True) |
|
64 self.__mpyWidget.aboutToDisconnect.connect(self.__deviceDisconnect) |
|
65 |
|
66 self.__bottomSidebar = EricSideBar( |
|
67 EricSideBarSide.SOUTH, Preferences.getUI("IconBarSize") |
|
68 ) |
|
69 self.__bottomSidebar.setIconBarColor(Preferences.getUI("IconBarColor")) |
|
70 |
|
71 self.__verticalSplitter = QSplitter(Qt.Orientation.Vertical) |
|
72 self.__verticalSplitter.setChildrenCollapsible(False) |
|
73 self.__verticalSplitter.addWidget(self.__mpyWidget) |
|
74 self.__verticalSplitter.addWidget(self.__bottomSidebar) |
|
75 self.setCentralWidget(self.__verticalSplitter) |
|
76 |
|
77 self.setWindowTitle(self.tr("MicroPython / CircuitPython Devices")) |
|
78 |
|
79 g = Preferences.getGeometry("MPyWindowGeometry") |
|
80 if g.isEmpty(): |
|
81 s = QSize(800, 1000) |
|
82 self.resize(s) |
|
83 else: |
|
84 self.restoreGeometry(g) |
|
85 self.__verticalSplitter.setSizes( |
|
86 Preferences.getMicroPython("MPyWindowSplitterSizes") |
|
87 ) |
|
88 |
|
89 # register the objects |
|
90 ericApp().registerObject("UserInterface", self) |
|
91 ericApp().registerObject("ViewManager", self) |
|
92 ericApp().registerObject("Pip", self.__pip) |
|
93 ericApp().registerObject("MicroPython", self.__mpyWidget) |
|
94 |
|
95 # attributes to manage the open editors |
|
96 self.__editors = [] |
|
97 self.__activeEditor = None |
|
98 |
|
99 ericApp().focusChanged.connect(self.__appFocusChanged) |
|
100 |
|
101 # network related setup |
|
102 if Preferences.getUI("UseSystemProxy"): |
|
103 QNetworkProxyFactory.setUseSystemConfiguration(True) |
|
104 else: |
|
105 self.__proxyFactory = EricNetworkProxyFactory() |
|
106 QNetworkProxyFactory.setApplicationProxyFactory(self.__proxyFactory) |
|
107 QNetworkProxyFactory.setUseSystemConfiguration(False) |
|
108 |
|
109 self.__networkManager = QNetworkAccessManager(self) |
|
110 self.__networkManager.proxyAuthenticationRequired.connect( |
|
111 proxyAuthenticationRequired |
|
112 ) |
|
113 if SSL_AVAILABLE: |
|
114 self.__sslErrorHandler = EricSslErrorHandler(self) |
|
115 self.__networkManager.sslErrors.connect(self.__sslErrors) |
|
116 self.__replies = [] |
|
117 |
|
118 self.__bottomSidebar.setVisible(False) |
|
119 |
|
120 def closeEvent(self, evt): |
|
121 """ |
|
122 Protected event handler for the close event. |
|
123 |
|
124 @param evt reference to the close event |
|
125 <br />This event is simply accepted after the history has been |
|
126 saved and all window references have been deleted. |
|
127 @type QCloseEvent |
|
128 """ |
|
129 Preferences.setGeometry("MPyWindowGeometry", self.saveGeometry()) |
|
130 Preferences.setMicroPython( |
|
131 "MPyWindowSplitterSizes", self.__verticalSplitter.sizes() |
|
132 ) |
|
133 |
|
134 for editor in self.__editors[:]: |
|
135 with contextlib.suppress(RuntimeError): |
|
136 editor.close() |
|
137 |
|
138 evt.accept() |
|
139 |
|
140 def __sslErrors(self, reply, errors): |
|
141 """ |
|
142 Private slot to handle SSL errors. |
|
143 |
|
144 @param reply reference to the reply object |
|
145 @type QNetworkReply |
|
146 @param errors list of SSL errors |
|
147 @type list of QSslError |
|
148 """ |
|
149 ignored = self.__sslErrorHandler.sslErrorsReply(reply, errors)[0] |
|
150 if ignored == EricSslErrorState.NOT_IGNORED: |
|
151 self.__downloadCancelled = True |
|
152 |
|
153 ####################################################################### |
|
154 ## Methods below implement user interface methods needed by the |
|
155 ## MicroPython widget. |
|
156 ####################################################################### |
|
157 |
|
158 def addSideWidget( |
|
159 self, |
|
160 side, # noqa U100 |
|
161 widget, |
|
162 icon, |
|
163 label, |
|
164 ): |
|
165 """ |
|
166 Public method to add a widget to the sides. |
|
167 |
|
168 @param side side to add the widget to |
|
169 @type UserInterfaceSide |
|
170 @param widget reference to the widget to add |
|
171 @type QWidget |
|
172 @param icon icon to be used |
|
173 @type QIcon |
|
174 @param label label text to be shown |
|
175 @type str |
|
176 """ |
|
177 self.__bottomSidebar.addTab(widget, icon, label) |
|
178 self.__bottomSidebar.setVisible(True) |
|
179 |
|
180 def showSideWidget(self, widget): |
|
181 """ |
|
182 Public method to show a specific widget placed in the side widgets. |
|
183 |
|
184 @param widget reference to the widget to be shown |
|
185 @type QWidget |
|
186 """ |
|
187 index = self.__bottomSidebar.indexOf(widget) |
|
188 if index != -1: |
|
189 self.__bottomSidebar.show() |
|
190 self.__bottomSidebar.setCurrentIndex(index) |
|
191 self.__bottomSidebar.raise_() |
|
192 |
|
193 def removeSideWidget(self, widget): |
|
194 """ |
|
195 Public method to remove a widget added using addSideWidget(). |
|
196 |
|
197 @param widget reference to the widget to remove |
|
198 @type QWidget |
|
199 """ |
|
200 index = self.__bottomSidebar.indexOf(widget) |
|
201 if index != -1: |
|
202 self.__bottomSidebar.removeTab(index) |
|
203 |
|
204 self.__bottomSidebar.setVisible(self.__bottomSidebar.count() > 0) |
|
205 |
|
206 def networkAccessManager(self): |
|
207 """ |
|
208 Public method to get a reference to the network access manager object. |
|
209 |
|
210 @return reference to the network access manager object |
|
211 @rtype QNetworkAccessManager |
|
212 """ |
|
213 return self.__networkManager |
|
214 |
|
215 def launchHelpViewer(self, url): |
|
216 """ |
|
217 Public slot to start the help viewer/web browser. |
|
218 |
|
219 @param url URL to be opened |
|
220 @type str or QUrl |
|
221 """ |
|
222 started = QDesktopServices.openUrl(QUrl(url)) |
|
223 if not started: |
|
224 EricMessageBox.critical( |
|
225 self, self.tr("Open Browser"), self.tr("Could not start a web browser") |
|
226 ) |
|
227 |
|
228 @pyqtSlot() |
|
229 @pyqtSlot(str) |
|
230 def showPreferences(self, pageName=None): |
|
231 """ |
|
232 Public slot to set the preferences. |
|
233 |
|
234 @param pageName name of the configuration page to show |
|
235 @type str |
|
236 """ |
|
237 from eric7.Preferences.ConfigurationDialog import ( |
|
238 ConfigurationDialog, |
|
239 ConfigurationMode, |
|
240 ) |
|
241 |
|
242 dlg = ConfigurationDialog( |
|
243 None, |
|
244 "Configuration", |
|
245 True, |
|
246 fromEric=True, |
|
247 displayMode=ConfigurationMode.MICROPYTHONMODE, |
|
248 ) |
|
249 dlg.show() |
|
250 if pageName is not None: |
|
251 dlg.showConfigurationPageByName(pageName) |
|
252 else: |
|
253 dlg.showConfigurationPageByName("empty") |
|
254 dlg.exec() |
|
255 if dlg.result() == QDialog.DialogCode.Accepted: |
|
256 dlg.setPreferences() |
|
257 Preferences.syncPreferences() |
|
258 self.__preferencesChanged() |
|
259 |
|
260 @pyqtSlot() |
|
261 def __preferencesChanged(self): |
|
262 """ |
|
263 Private slot to handle a change of the preferences. |
|
264 """ |
|
265 self.__bottomSidebar.setIconBarColor(Preferences.getUI("IconBarColor")) |
|
266 self.__bottomSidebar.setIconBarSize(Preferences.getUI("IconBarSize")) |
|
267 |
|
268 if Preferences.getUI("UseSystemProxy"): |
|
269 QNetworkProxyFactory.setUseSystemConfiguration(True) |
|
270 else: |
|
271 self.__proxyFactory = EricNetworkProxyFactory() |
|
272 QNetworkProxyFactory.setApplicationProxyFactory(self.__proxyFactory) |
|
273 QNetworkProxyFactory.setUseSystemConfiguration(False) |
|
274 |
|
275 self.preferencesChanged.emit() |
|
276 |
|
277 ####################################################################### |
|
278 ## Methods below implement view manager methods needed by the |
|
279 ## MicroPython widget. |
|
280 ####################################################################### |
|
281 |
|
282 def activeWindow(self): |
|
283 """ |
|
284 Public method to get a reference to the active editor. |
|
285 |
|
286 @return reference to the active editor |
|
287 @rtype MiniEditor |
|
288 """ |
|
289 return self.__activeEditor |
|
290 |
|
291 def getEditor(self, fn): |
|
292 """ |
|
293 Public method to return the editor displaying the given file. |
|
294 |
|
295 @param fn filename to look for |
|
296 @type str |
|
297 """ |
|
298 for editor in self.__editors: |
|
299 if editor.getFileName() == fn: |
|
300 editor.raise_() |
|
301 break |
|
302 else: |
|
303 editor = MiniEditor(filename=fn, parent=self) |
|
304 editor.closing.connect(lambda: self.__editorClosing(editor)) |
|
305 editor.show() |
|
306 |
|
307 self.__editors.append(editor) |
|
308 |
|
309 def newEditorWithText(self, text, language="", fileName=""): |
|
310 """ |
|
311 Public method to generate a new editor with a given text and associated file |
|
312 name. |
|
313 |
|
314 @param text text for the editor |
|
315 @type str |
|
316 @param language source language (defaults to "") |
|
317 @type str (optional) |
|
318 @param fileName associated file name (defaults to "") |
|
319 @type str (optional) |
|
320 """ |
|
321 editor = MiniEditor(filename=fileName, parent=self) |
|
322 editor.closing.connect(lambda: self.__editorClosing(editor)) |
|
323 editor.setText(text, filetype=language) |
|
324 editor.setLanguage(fileName) |
|
325 editor.show() |
|
326 |
|
327 self.__editors.append(editor) |
|
328 |
|
329 def __editorClosing(self, editor): |
|
330 """ |
|
331 Private method called, when an editor is closing. |
|
332 |
|
333 @param editor reference to the closing editor |
|
334 @type MiniEditor |
|
335 """ |
|
336 with contextlib.suppress(ValueError): |
|
337 self.__editors.remove(editor) |
|
338 del editor |
|
339 |
|
340 if self.__editors: |
|
341 # make the last one (i.e. most recently opened one) the active editor |
|
342 self.__activeEditor = self.__editors[-1] |
|
343 else: |
|
344 self.__activeEditor = None |
|
345 |
|
346 @pyqtSlot(QWidget, QWidget) |
|
347 def __appFocusChanged(self, old, now): |
|
348 """ |
|
349 Private slot to track the application focus. |
|
350 |
|
351 @param old reference to the widget loosing focus |
|
352 @type QWidget |
|
353 @param now reference to the widget gaining focus |
|
354 @type QWidget |
|
355 """ |
|
356 if now is None: |
|
357 return |
|
358 |
|
359 for editor in self.__editors: |
|
360 if now in editor.findChildren(QWidget): |
|
361 self.__activeEditor = editor |
|
362 break |
|
363 |
|
364 @pyqtSlot() |
|
365 def __deviceDisconnect(self): |
|
366 """ |
|
367 Private slot handling the device being disconnected. |
|
368 |
|
369 This closes all editors directly connected to the device about to |
|
370 be disconnected. |
|
371 """ |
|
372 for editor in self.__editors[:]: |
|
373 if FileSystemUtilities.isDeviceFileName(editor.getFileName()): |
|
374 editor.close() |