eric7/MicroPython/MicroPythonWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the MicroPython REPL widget.
8 """
9
10 import re
11 import time
12 import os
13 import functools
14
15 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent
16 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush, QClipboard
17 from PyQt5.QtWidgets import (
18 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy,
19 QTextEdit, QToolButton, QDialog
20 )
21
22 from E5Gui.E5ZoomWidget import E5ZoomWidget
23 from E5Gui import E5MessageBox, E5FileDialog
24 from E5Gui.E5Application import e5App
25 from E5Gui.E5ProcessDialog import E5ProcessDialog
26 from E5Gui.E5OverrideCursor import E5OverrideCursor, E5OverridenCursor
27
28 from .Ui_MicroPythonWidget import Ui_MicroPythonWidget
29
30 from . import MicroPythonDevices
31 from . import UF2FlashDialog
32 try:
33 from .MicroPythonGraphWidget import MicroPythonGraphWidget
34 HAS_QTCHART = True
35 except ImportError:
36 HAS_QTCHART = False
37 from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget
38 try:
39 from .MicroPythonCommandsInterface import MicroPythonCommandsInterface
40 HAS_QTSERIALPORT = True
41 except ImportError:
42 HAS_QTSERIALPORT = False
43
44 import Globals
45 import UI.PixmapCache
46 import Preferences
47 import Utilities
48
49 from UI.Info import BugAddress
50
51 # ANSI Colors (see https://en.wikipedia.org/wiki/ANSI_escape_code)
52 AnsiColorSchemes = {
53 "Windows 7": {
54 0: QBrush(QColor(0, 0, 0)),
55 1: QBrush(QColor(128, 0, 0)),
56 2: QBrush(QColor(0, 128, 0)),
57 3: QBrush(QColor(128, 128, 0)),
58 4: QBrush(QColor(0, 0, 128)),
59 5: QBrush(QColor(128, 0, 128)),
60 6: QBrush(QColor(0, 128, 128)),
61 7: QBrush(QColor(192, 192, 192)),
62 10: QBrush(QColor(128, 128, 128)),
63 11: QBrush(QColor(255, 0, 0)),
64 12: QBrush(QColor(0, 255, 0)),
65 13: QBrush(QColor(255, 255, 0)),
66 14: QBrush(QColor(0, 0, 255)),
67 15: QBrush(QColor(255, 0, 255)),
68 16: QBrush(QColor(0, 255, 255)),
69 17: QBrush(QColor(255, 255, 255)),
70 },
71 "Windows 10": {
72 0: QBrush(QColor(12, 12, 12)),
73 1: QBrush(QColor(197, 15, 31)),
74 2: QBrush(QColor(19, 161, 14)),
75 3: QBrush(QColor(193, 156, 0)),
76 4: QBrush(QColor(0, 55, 218)),
77 5: QBrush(QColor(136, 23, 152)),
78 6: QBrush(QColor(58, 150, 221)),
79 7: QBrush(QColor(204, 204, 204)),
80 10: QBrush(QColor(118, 118, 118)),
81 11: QBrush(QColor(231, 72, 86)),
82 12: QBrush(QColor(22, 198, 12)),
83 13: QBrush(QColor(249, 241, 165)),
84 14: QBrush(QColor(59, 12, 255)),
85 15: QBrush(QColor(180, 0, 158)),
86 16: QBrush(QColor(97, 214, 214)),
87 17: QBrush(QColor(242, 242, 242)),
88 },
89 "PuTTY": {
90 0: QBrush(QColor(0, 0, 0)),
91 1: QBrush(QColor(187, 0, 0)),
92 2: QBrush(QColor(0, 187, 0)),
93 3: QBrush(QColor(187, 187, 0)),
94 4: QBrush(QColor(0, 0, 187)),
95 5: QBrush(QColor(187, 0, 187)),
96 6: QBrush(QColor(0, 187, 187)),
97 7: QBrush(QColor(187, 187, 187)),
98 10: QBrush(QColor(85, 85, 85)),
99 11: QBrush(QColor(255, 85, 85)),
100 12: QBrush(QColor(85, 255, 85)),
101 13: QBrush(QColor(255, 255, 85)),
102 14: QBrush(QColor(85, 85, 255)),
103 15: QBrush(QColor(255, 85, 255)),
104 16: QBrush(QColor(85, 255, 255)),
105 17: QBrush(QColor(255, 255, 255)),
106 },
107 "xterm": {
108 0: QBrush(QColor(0, 0, 0)),
109 1: QBrush(QColor(205, 0, 0)),
110 2: QBrush(QColor(0, 205, 0)),
111 3: QBrush(QColor(205, 205, 0)),
112 4: QBrush(QColor(0, 0, 238)),
113 5: QBrush(QColor(205, 0, 205)),
114 6: QBrush(QColor(0, 205, 205)),
115 7: QBrush(QColor(229, 229, 229)),
116 10: QBrush(QColor(127, 127, 127)),
117 11: QBrush(QColor(255, 0, 0)),
118 12: QBrush(QColor(0, 255, 0)),
119 13: QBrush(QColor(255, 255, 0)),
120 14: QBrush(QColor(0, 0, 255)),
121 15: QBrush(QColor(255, 0, 255)),
122 16: QBrush(QColor(0, 255, 255)),
123 17: QBrush(QColor(255, 255, 255)),
124 },
125 "Ubuntu": {
126 0: QBrush(QColor(1, 1, 1)),
127 1: QBrush(QColor(222, 56, 43)),
128 2: QBrush(QColor(57, 181, 74)),
129 3: QBrush(QColor(255, 199, 6)),
130 4: QBrush(QColor(0, 11, 184)),
131 5: QBrush(QColor(118, 38, 113)),
132 6: QBrush(QColor(44, 181, 233)),
133 7: QBrush(QColor(204, 204, 204)),
134 10: QBrush(QColor(128, 128, 128)),
135 11: QBrush(QColor(255, 0, 0)),
136 12: QBrush(QColor(0, 255, 0)),
137 13: QBrush(QColor(255, 255, 0)),
138 14: QBrush(QColor(0, 0, 255)),
139 15: QBrush(QColor(255, 0, 255)),
140 16: QBrush(QColor(0, 255, 255)),
141 17: QBrush(QColor(255, 255, 255)),
142 },
143 "Ubuntu (dark)": {
144 0: QBrush(QColor(96, 96, 96)),
145 1: QBrush(QColor(235, 58, 45)),
146 2: QBrush(QColor(57, 181, 74)),
147 3: QBrush(QColor(255, 199, 29)),
148 4: QBrush(QColor(25, 56, 230)),
149 5: QBrush(QColor(200, 64, 193)),
150 6: QBrush(QColor(48, 200, 255)),
151 7: QBrush(QColor(204, 204, 204)),
152 10: QBrush(QColor(128, 128, 128)),
153 11: QBrush(QColor(255, 0, 0)),
154 12: QBrush(QColor(0, 255, 0)),
155 13: QBrush(QColor(255, 255, 0)),
156 14: QBrush(QColor(0, 0, 255)),
157 15: QBrush(QColor(255, 0, 255)),
158 16: QBrush(QColor(0, 255, 255)),
159 17: QBrush(QColor(255, 255, 255)),
160 },
161 "Breeze (dark)": {
162 0: QBrush(QColor(35, 38, 39)),
163 1: QBrush(QColor(237, 21, 21)),
164 2: QBrush(QColor(17, 209, 22)),
165 3: QBrush(QColor(246, 116, 0)),
166 4: QBrush(QColor(29, 153, 243)),
167 5: QBrush(QColor(155, 89, 182)),
168 6: QBrush(QColor(26, 188, 156)),
169 7: QBrush(QColor(252, 252, 252)),
170 10: QBrush(QColor(127, 140, 141)),
171 11: QBrush(QColor(192, 57, 43)),
172 12: QBrush(QColor(28, 220, 154)),
173 13: QBrush(QColor(253, 188, 75)),
174 14: QBrush(QColor(61, 174, 233)),
175 15: QBrush(QColor(142, 68, 173)),
176 16: QBrush(QColor(22, 160, 133)),
177 17: QBrush(QColor(255, 255, 255)),
178 },
179 }
180
181
182 class MicroPythonWidget(QWidget, Ui_MicroPythonWidget):
183 """
184 Class implementing the MicroPython REPL widget.
185
186 @signal dataReceived(data) emitted to send data received via the serial
187 connection for further processing
188 """
189 ZoomMin = -10
190 ZoomMax = 20
191
192 DeviceTypeRole = Qt.ItemDataRole.UserRole
193 DeviceBoardRole = Qt.ItemDataRole.UserRole + 1
194 DevicePortRole = Qt.ItemDataRole.UserRole + 2
195 DeviceVidRole = Qt.ItemDataRole.UserRole + 3
196 DevicePidRole = Qt.ItemDataRole.UserRole + 4
197
198 dataReceived = pyqtSignal(bytes)
199
200 ManualMarker = "<manual>"
201
202 def __init__(self, parent=None):
203 """
204 Constructor
205
206 @param parent reference to the parent widget
207 @type QWidget
208 """
209 super().__init__(parent)
210 self.setupUi(self)
211
212 self.__ui = parent
213
214 self.__superMenu = QMenu(self)
215 self.__superMenu.aboutToShow.connect(self.__aboutToShowSuperMenu)
216
217 self.menuButton.setObjectName(
218 "micropython_supermenu_button")
219 self.menuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
220 self.menuButton.setToolTip(self.tr("MicroPython Menu"))
221 self.menuButton.setPopupMode(
222 QToolButton.ToolButtonPopupMode.InstantPopup)
223 self.menuButton.setToolButtonStyle(
224 Qt.ToolButtonStyle.ToolButtonIconOnly)
225 self.menuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
226 self.menuButton.setAutoRaise(True)
227 self.menuButton.setShowMenuInside(True)
228 self.menuButton.setMenu(self.__superMenu)
229
230 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
231 "", False))
232
233 self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
234 self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
235
236 self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
237 self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
238 self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
239 self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager"))
240 self.chartButton.setIcon(UI.PixmapCache.getIcon("chart"))
241 self.connectButton.setIcon(UI.PixmapCache.getIcon("linkConnect"))
242
243 self.__zoomLayout = QHBoxLayout()
244 spacerItem = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding,
245 QSizePolicy.Policy.Minimum)
246 self.__zoomLayout.addSpacerItem(spacerItem)
247
248 self.__zoom0 = self.replEdit.fontPointSize()
249 self.__zoomWidget = E5ZoomWidget(
250 UI.PixmapCache.getPixmap("zoomOut"),
251 UI.PixmapCache.getPixmap("zoomIn"),
252 UI.PixmapCache.getPixmap("zoomReset"), self)
253 self.__zoomLayout.addWidget(self.__zoomWidget)
254 self.layout().insertLayout(
255 self.layout().count() - 1,
256 self.__zoomLayout)
257 self.__zoomWidget.setMinimum(self.ZoomMin)
258 self.__zoomWidget.setMaximum(self.ZoomMax)
259 self.__zoomWidget.valueChanged.connect(self.__doZoom)
260 self.__currentZoom = 0
261
262 self.__fileManagerWidget = None
263 self.__chartWidget = None
264
265 self.__unknownPorts = []
266 self.__lastPort = None
267 self.__lastDeviceType = None
268
269 if HAS_QTSERIALPORT:
270 self.__interface = MicroPythonCommandsInterface(self)
271 else:
272 self.__interface = None
273 self.__device = None
274 self.__connected = False
275 self.__setConnected(False)
276
277 if not HAS_QTSERIALPORT:
278 self.replEdit.setHtml(self.tr(
279 "<h3>The QtSerialPort package is not available.<br/>"
280 "MicroPython support is deactivated.</h3>"))
281 self.setEnabled(False)
282 return
283
284 self.__vt100Re = re.compile(
285 r'(?P<count>\d*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])')
286
287 self.__populateDeviceTypeComboBox()
288
289 self.replEdit.installEventFilter(self)
290 # Hack to intercept middle button paste
291 self.__origReplEditMouseReleaseEvent = self.replEdit.mouseReleaseEvent
292 self.replEdit.mouseReleaseEvent = self.__replEditMouseReleaseEvent
293
294 self.replEdit.customContextMenuRequested.connect(
295 self.__showContextMenu)
296 self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
297 self.__ui.preferencesChanged.connect(
298 self.__interface.handlePreferencesChanged)
299
300 self.__handlePreferencesChanged()
301
302 charFormat = self.replEdit.currentCharFormat()
303 self.DefaultForeground = charFormat.foreground()
304 self.DefaultBackground = charFormat.background()
305
306 def __populateDeviceTypeComboBox(self):
307 """
308 Private method to populate the device type selector.
309 """
310 currentDevice = self.deviceTypeComboBox.currentText()
311
312 self.deviceTypeComboBox.clear()
313 self.deviceInfoLabel.clear()
314
315 self.deviceTypeComboBox.addItem("", "")
316 devices, unknownDevices, unknownPorts = (
317 MicroPythonDevices.getFoundDevices()
318 )
319 if devices:
320 supportedMessage = self.tr(
321 "%n supported device(s) detected.", "", len(devices))
322
323 for index, (boardType, boardName, description, portName,
324 vid, pid) in enumerate(sorted(devices), 1):
325 self.deviceTypeComboBox.addItem(
326 self.tr("{0} - {1} ({2})",
327 "board name, description, port name")
328 .format(boardName, description, portName)
329 )
330 self.deviceTypeComboBox.setItemData(
331 index, boardType, self.DeviceTypeRole)
332 self.deviceTypeComboBox.setItemData(
333 index, boardName, self.DeviceBoardRole)
334 self.deviceTypeComboBox.setItemData(
335 index, portName, self.DevicePortRole)
336 self.deviceTypeComboBox.setItemData(
337 index, vid, self.DeviceVidRole)
338 self.deviceTypeComboBox.setItemData(
339 index, pid, self.DevicePidRole)
340
341 else:
342 supportedMessage = self.tr("No supported devices detected.")
343
344 self.__unknownPorts = unknownPorts
345 if self.__unknownPorts:
346 unknownMessage = self.tr(
347 "\n%n unknown device(s) for manual selection.", "",
348 len(self.__unknownPorts))
349 if self.deviceTypeComboBox.count():
350 self.deviceTypeComboBox.insertSeparator(
351 self.deviceTypeComboBox.count())
352 self.deviceTypeComboBox.addItem(self.tr("Manual Selection"))
353 self.deviceTypeComboBox.setItemData(
354 self.deviceTypeComboBox.count() - 1,
355 self.ManualMarker, self.DeviceTypeRole)
356 else:
357 unknownMessage = ""
358
359 self.deviceInfoLabel.setText(supportedMessage + unknownMessage)
360
361 index = self.deviceTypeComboBox.findText(currentDevice,
362 Qt.MatchFlag.MatchExactly)
363 if index == -1:
364 # entry is no longer present
365 index = 0
366 if self.__connected:
367 # we are still connected, so disconnect
368 self.on_connectButton_clicked()
369
370 self.on_deviceTypeComboBox_activated(index)
371 self.deviceTypeComboBox.setCurrentIndex(index)
372
373 if unknownDevices:
374 ignoredUnknown = {
375 tuple(d)
376 for d in Preferences.getMicroPython("IgnoredUnknownDevices")
377 }
378 uf2Devices = {(*x[2], x[1])
379 for x in UF2FlashDialog.getFoundDevices()}
380 newUnknownDevices = (
381 set(unknownDevices) - ignoredUnknown - uf2Devices
382 )
383 if newUnknownDevices:
384 button = E5MessageBox.information(
385 self,
386 self.tr("Unknown MicroPython Device"),
387 self.tr(
388 '<p>Detected these unknown serial devices</p>'
389 '<ul>'
390 '<li>{0}</li>'
391 '</ul>'
392 '<p>Please report them together with the board name'
393 ' and a short description to <a href="mailto:{1}">'
394 ' the eric bug reporting address</a> if it is a'
395 ' MicroPython board.</p>'
396 ).format("</li><li>".join([
397 self.tr("{0} (0x{1:04x}/0x{2:04x})",
398 "description, VId, PId").format(
399 desc, vid, pid)
400 for vid, pid, desc in newUnknownDevices]),
401 BugAddress),
402 E5MessageBox.StandardButtons(
403 E5MessageBox.Ignore |
404 E5MessageBox.Ok
405 )
406 )
407 if button == E5MessageBox.Ignore:
408 ignoredUnknown = list(ignoredUnknown | newUnknownDevices)
409 Preferences.setMicroPython("IgnoredUnknownDevices",
410 ignoredUnknown)
411 else:
412 yes = E5MessageBox.yesNo(
413 self,
414 self.tr("Unknown MicroPython Device"),
415 self.tr("""Would you like to add them to the list of"""
416 """ manually configured devices?"""),
417 yesDefault=True)
418 if yes:
419 self.__addUnknownDevices(list(newUnknownDevices))
420
421 def __handlePreferencesChanged(self):
422 """
423 Private slot to handle a change in preferences.
424 """
425 self.__colorScheme = Preferences.getMicroPython("ColorScheme")
426
427 self.__font = Preferences.getEditorOtherFonts("MonospacedFont")
428 self.replEdit.setFontFamily(self.__font.family())
429 self.replEdit.setFontPointSize(self.__font.pointSize())
430
431 if Preferences.getMicroPython("ReplLineWrap"):
432 self.replEdit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
433 else:
434 self.replEdit.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
435
436 if self.__chartWidget is not None:
437 self.__chartWidget.preferencesChanged()
438
439 def commandsInterface(self):
440 """
441 Public method to get a reference to the commands interface object.
442
443 @return reference to the commands interface object
444 @rtype MicroPythonCommandsInterface
445 """
446 return self.__interface
447
448 def isMicrobit(self):
449 """
450 Public method to check, if the connected/selected device is a
451 BBC micro:bit or Calliope mini.
452
453 @return flag indicating a micro:bit device
454 rtype bool
455 """
456 if self.__device and (
457 "micro:bit" in self.__device.deviceName() or
458 "Calliope" in self.__device.deviceName()
459 ):
460 return True
461
462 return False
463
464 @pyqtSlot(int)
465 def on_deviceTypeComboBox_activated(self, index):
466 """
467 Private slot handling the selection of a device type.
468
469 @param index index of the selected device
470 @type int
471 """
472 deviceType = self.deviceTypeComboBox.itemData(
473 index, self.DeviceTypeRole)
474 if deviceType == self.ManualMarker:
475 self.connectButton.setEnabled(bool(self.__unknownPorts))
476 else:
477 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
478 deviceType, False))
479
480 vid = self.deviceTypeComboBox.itemData(
481 index, self.DeviceVidRole)
482 pid = self.deviceTypeComboBox.itemData(
483 index, self.DevicePidRole)
484
485 self.__device = MicroPythonDevices.getDevice(deviceType, self,
486 vid, pid)
487 self.__device.setButtons()
488
489 self.connectButton.setEnabled(bool(deviceType))
490
491 @pyqtSlot()
492 def on_checkButton_clicked(self):
493 """
494 Private slot to check for connected devices.
495 """
496 self.__populateDeviceTypeComboBox()
497
498 def setActionButtons(self, **kwargs):
499 """
500 Public method to set the enabled state of the various action buttons.
501
502 @keyparam kwargs keyword arguments containg the enabled states (keys
503 are 'run', 'repl', 'files', 'chart', 'open', 'save'
504 @type dict
505 """
506 if "open" in kwargs:
507 self.openButton.setEnabled(kwargs["open"])
508 if "save" in kwargs:
509 self.saveButton.setEnabled(kwargs["save"])
510 if "run" in kwargs:
511 self.runButton.setEnabled(kwargs["run"])
512 if "repl" in kwargs:
513 self.replButton.setEnabled(kwargs["repl"])
514 if "files" in kwargs:
515 self.filesButton.setEnabled(kwargs["files"])
516 if "chart" in kwargs:
517 self.chartButton.setEnabled(kwargs["chart"] and HAS_QTCHART)
518
519 @pyqtSlot(QPoint)
520 def __showContextMenu(self, pos):
521 """
522 Private slot to show the REPL context menu.
523
524 @param pos position to show the menu at
525 @type QPoint
526 """
527 if Globals.isMacPlatform():
528 copyKeys = QKeySequence(Qt.Modifier.CTRL + Qt.Key.Key_C)
529 pasteKeys = QKeySequence(Qt.Modifier.CTRL + Qt.Key.Key_V)
530 else:
531 copyKeys = QKeySequence(
532 Qt.Modifier.CTRL + Qt.Modifier.SHIFT + Qt.Key.Key_C)
533 pasteKeys = QKeySequence(
534 Qt.Modifier.CTRL + Qt.Modifier.SHIFT + Qt.Key.Key_V)
535 menu = QMenu(self)
536 menu.addAction(self.tr("Clear"), self.__clear)
537 menu.addSeparator()
538 menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
539 menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
540 menu.addSeparator()
541 menu.exec(self.replEdit.mapToGlobal(pos))
542
543 def __setConnected(self, connected):
544 """
545 Private method to set the connection status LED.
546
547 @param connected connection state
548 @type bool
549 """
550 self.__connected = connected
551
552 self.deviceConnectedLed.setOn(connected)
553 if self.__fileManagerWidget:
554 self.__fileManagerWidget.deviceConnectedLed.setOn(connected)
555
556 self.deviceTypeComboBox.setEnabled(not connected)
557
558 if connected:
559 self.connectButton.setIcon(
560 UI.PixmapCache.getIcon("linkDisconnect"))
561 self.connectButton.setToolTip(self.tr(
562 "Press to disconnect the current device"))
563 else:
564 self.connectButton.setIcon(
565 UI.PixmapCache.getIcon("linkConnect"))
566 self.connectButton.setToolTip(self.tr(
567 "Press to connect the selected device"))
568
569 def isConnected(self):
570 """
571 Public method to get the connection state.
572
573 @return connection state
574 @rtype bool
575 """
576 return self.__connected
577
578 def __showNoDeviceMessage(self):
579 """
580 Private method to show a message dialog indicating a missing device.
581 """
582 E5MessageBox.critical(
583 self,
584 self.tr("No device attached"),
585 self.tr("""Please ensure the device is plugged into your"""
586 """ computer and selected.\n\nIt must have a version"""
587 """ of MicroPython (or CircuitPython) flashed onto"""
588 """ it before anything will work.\n\nFinally press"""
589 """ the device's reset button and wait a few seconds"""
590 """ before trying again."""))
591
592 @pyqtSlot(bool)
593 def on_replButton_clicked(self, checked):
594 """
595 Private slot to connect to enable or disable the REPL widget.
596
597 If the selected device is not connected yet, this will be done now.
598
599 @param checked state of the button
600 @type bool
601 """
602 if not self.__device:
603 self.__showNoDeviceMessage()
604 return
605
606 if checked:
607 ok, reason = self.__device.canStartRepl()
608 if not ok:
609 E5MessageBox.warning(
610 self,
611 self.tr("Start REPL"),
612 self.tr("""<p>The REPL cannot be started.</p><p>Reason:"""
613 """ {0}</p>""").format(reason))
614 return
615
616 self.replEdit.clear()
617 self.__interface.dataReceived.connect(self.__processData)
618
619 if not self.__interface.isConnected():
620 self.__connectToDevice()
621 if self.__device.forceInterrupt():
622 # send a Ctrl-B (exit raw mode)
623 self.__interface.write(b'\x02')
624 # send Ctrl-C (keyboard interrupt)
625 self.__interface.write(b'\x03')
626
627 self.__device.setRepl(True)
628 self.replEdit.setFocus(Qt.FocusReason.OtherFocusReason)
629 else:
630 self.__interface.dataReceived.disconnect(self.__processData)
631 if (not self.chartButton.isChecked() and
632 not self.filesButton.isChecked()):
633 self.__disconnectFromDevice()
634 self.__device.setRepl(False)
635 self.replButton.setChecked(checked)
636
637 @pyqtSlot()
638 def on_connectButton_clicked(self):
639 """
640 Private slot to connect to the selected device or disconnect from the
641 currently connected device.
642 """
643 if self.__connected:
644 with E5OverrideCursor():
645 self.__disconnectFromDevice()
646
647 if self.replButton.isChecked():
648 self.on_replButton_clicked(False)
649 if self.filesButton.isChecked():
650 self.on_filesButton_clicked(False)
651 if self.chartButton.isChecked():
652 self.on_chartButton_clicked(False)
653 else:
654 with E5OverrideCursor():
655 self.__connectToDevice()
656
657 @pyqtSlot()
658 def __clear(self):
659 """
660 Private slot to clear the REPL pane.
661 """
662 self.replEdit.clear()
663 self.__interface.isConnected() and self.__interface.write(b"\r")
664
665 @pyqtSlot()
666 def __paste(self, mode=QClipboard.Mode.Clipboard):
667 """
668 Private slot to perform a paste operation.
669
670 @param mode paste mode (defaults to QClipboard.Mode.Clipboard)
671 @type QClipboard.Mode (optional)
672 """
673 # add support for paste by mouse middle button
674 clipboard = QApplication.clipboard()
675 if clipboard:
676 pasteText = clipboard.text(mode=mode)
677 if pasteText:
678 pasteText = pasteText.replace('\n\r', '\r')
679 pasteText = pasteText.replace('\n', '\r')
680 self.__interface.isConnected() and self.__interface.write(
681 pasteText.encode("utf-8"))
682
683 def eventFilter(self, obj, evt):
684 """
685 Public method to process events for the REPL pane.
686
687 @param obj reference to the object the event was meant for
688 @type QObject
689 @param evt reference to the event object
690 @type QEvent
691 @return flag to indicate that the event was handled
692 @rtype bool
693 """
694 if obj is self.replEdit and evt.type() == QEvent.Type.KeyPress:
695 # handle the key press event on behalve of the REPL pane
696 key = evt.key()
697 msg = bytes(evt.text(), 'utf8')
698 if key == Qt.Key.Key_Backspace:
699 msg = b'\b'
700 elif key == Qt.Key.Key_Delete:
701 msg = b'\x1B[\x33\x7E'
702 elif key == Qt.Key.Key_Up:
703 msg = b'\x1B[A'
704 elif key == Qt.Key.Key_Down:
705 msg = b'\x1B[B'
706 elif key == Qt.Key.Key_Right:
707 msg = b'\x1B[C'
708 elif key == Qt.Key.Key_Left:
709 msg = b'\x1B[D'
710 elif key == Qt.Key.Key_Home:
711 msg = b'\x1B[H'
712 elif key == Qt.Key.Key_End:
713 msg = b'\x1B[F'
714 elif ((Globals.isMacPlatform() and
715 evt.modifiers() == Qt.KeyboardModifier.MetaModifier) or
716 (not Globals.isMacPlatform() and
717 evt.modifiers() == Qt.KeyboardModifier.ControlModifier)):
718 if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
719 # devices treat an input of \x01 as Ctrl+A, etc.
720 msg = bytes([1 + key - Qt.Key.Key_A])
721 elif (
722 evt.modifiers() == (
723 Qt.KeyboardModifier.ControlModifier |
724 Qt.KeyboardModifier.ShiftModifier) or
725 (Globals.isMacPlatform() and
726 evt.modifiers() == Qt.KeyboardModifier.ControlModifier)
727 ):
728 if key == Qt.Key.Key_C:
729 self.replEdit.copy()
730 msg = b''
731 elif key == Qt.Key.Key_V:
732 self.__paste()
733 msg = b''
734 elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
735 tc = self.replEdit.textCursor()
736 tc.movePosition(QTextCursor.MoveOperation.EndOfLine)
737 self.replEdit.setTextCursor(tc)
738 self.__interface.isConnected() and self.__interface.write(msg)
739 return True
740 else:
741 # standard event processing
742 return super().eventFilter(obj, evt)
743
744 def __replEditMouseReleaseEvent(self, evt):
745 """
746 Private method handling mouse release events for the replEdit widget.
747
748 Note: this is a hack because QTextEdit does not allow filtering of
749 QEvent.Type.MouseButtonRelease. To make middle button paste work, we
750 had to intercept the protected event method (some kind of
751 reimplementing it).
752
753 @param evt reference to the event object
754 @type QMouseEvent
755 """
756 if evt.button() == Qt.MouseButton.MiddleButton:
757 self.__paste(mode=QClipboard.Mode.Selection)
758 msg = b''
759 if self.__interface.isConnected():
760 self.__interface.write(msg)
761 evt.accept()
762 else:
763 self.__origReplEditMouseReleaseEvent(evt)
764
765 def __processData(self, data):
766 """
767 Private slot to process bytes received from the device.
768
769 @param data bytes received from the device
770 @type bytes
771 """
772 tc = self.replEdit.textCursor()
773 # the text cursor must be on the last line
774 while tc.movePosition(QTextCursor.MoveOperation.Down):
775 pass
776
777 # set the font
778 charFormat = tc.charFormat()
779 charFormat.setFontFamily(self.__font.family())
780 charFormat.setFontPointSize(self.__font.pointSize())
781 tc.setCharFormat(charFormat)
782
783 index = 0
784 while index < len(data):
785 if data[index] == 8: # \b
786 tc.movePosition(QTextCursor.MoveOperation.Left)
787 self.replEdit.setTextCursor(tc)
788 elif data[index] in (4, 13): # EOT, \r
789 pass
790 elif (len(data) > index + 1 and
791 data[index] == 27 and
792 data[index + 1] == 91):
793 # VT100 cursor command detected: <Esc>[
794 index += 2 # move index to after the [
795 match = self.__vt100Re.search(data[index:].decode("utf-8"))
796 if match:
797 # move to last position in control sequence
798 # ++ will be done at end of loop
799 index += match.end() - 1
800
801 action = match.group("action")
802 if action in "ABCD":
803 if match.group("count") == "":
804 count = 1
805 else:
806 count = int(match.group("count"))
807
808 if action == "A": # up
809 tc.movePosition(QTextCursor.MoveOperation.Up,
810 n=count)
811 self.replEdit.setTextCursor(tc)
812 elif action == "B": # down
813 tc.movePosition(QTextCursor.MoveOperation.Down,
814 n=count)
815 self.replEdit.setTextCursor(tc)
816 elif action == "C": # right
817 tc.movePosition(QTextCursor.MoveOperation.Right,
818 n=count)
819 self.replEdit.setTextCursor(tc)
820 elif action == "D": # left
821 tc.movePosition(QTextCursor.MoveOperation.Left,
822 n=count)
823 self.replEdit.setTextCursor(tc)
824 elif action == "K": # delete things
825 if match.group("count") in ("", "0"):
826 # delete to end of line
827 tc.movePosition(
828 QTextCursor.MoveOperation.EndOfLine,
829 mode=QTextCursor.MoveMode.KeepAnchor)
830 tc.removeSelectedText()
831 self.replEdit.setTextCursor(tc)
832 elif match.group("count") == "1":
833 # delete to beinning of line
834 tc.movePosition(
835 QTextCursor.MoveOperation.StartOfLine,
836 mode=QTextCursor.MoveMode.KeepAnchor)
837 tc.removeSelectedText()
838 self.replEdit.setTextCursor(tc)
839 elif match.group("count") == "2":
840 # delete whole line
841 tc.movePosition(
842 QTextCursor.MoveOperation.EndOfLine)
843 tc.movePosition(
844 QTextCursor.MoveOperation.StartOfLine,
845 mode=QTextCursor.MoveMode.KeepAnchor)
846 tc.removeSelectedText()
847 self.replEdit.setTextCursor(tc)
848 elif action == "m":
849 self.__setCharFormat(match.group(0)[:-1].split(";"),
850 tc)
851 else:
852 tc.deleteChar()
853 self.replEdit.setTextCursor(tc)
854 self.replEdit.insertPlainText(chr(data[index]))
855
856 index += 1
857
858 self.replEdit.ensureCursorVisible()
859
860 def __setCharFormat(self, formatCodes, textCursor):
861 """
862 Private method setting the current text format of the REPL pane based
863 on the passed ANSI codes.
864
865 Following codes are used:
866 <ul>
867 <li>0: Reset</li>
868 <li>1: Bold font (weight 75)</li>
869 <li>2: Light font (weight 25)</li>
870 <li>3: Italic font</li>
871 <li>4: Underlined font</li>
872 <li>9: Strikeout font</li>
873 <li>21: Bold off (weight 50)</li>
874 <li>22: Light off (weight 50)</li>
875 <li>23: Italic off</li>
876 <li>24: Underline off</li>
877 <li>29: Strikeout off</li>
878 <li>30: foreground Black</li>
879 <li>31: foreground Dark Red</li>
880 <li>32: foreground Dark Green</li>
881 <li>33: foreground Dark Yellow</li>
882 <li>34: foreground Dark Blue</li>
883 <li>35: foreground Dark Magenta</li>
884 <li>36: foreground Dark Cyan</li>
885 <li>37: foreground Light Gray</li>
886 <li>39: reset foreground to default</li>
887 <li>40: background Black</li>
888 <li>41: background Dark Red</li>
889 <li>42: background Dark Green</li>
890 <li>43: background Dark Yellow</li>
891 <li>44: background Dark Blue</li>
892 <li>45: background Dark Magenta</li>
893 <li>46: background Dark Cyan</li>
894 <li>47: background Light Gray</li>
895 <li>49: reset background to default</li>
896 <li>53: Overlined font</li>
897 <li>55: Overline off</li>
898 <li>90: bright foreground Dark Gray</li>
899 <li>91: bright foreground Red</li>
900 <li>92: bright foreground Green</li>
901 <li>93: bright foreground Yellow</li>
902 <li>94: bright foreground Blue</li>
903 <li>95: bright foreground Magenta</li>
904 <li>96: bright foreground Cyan</li>
905 <li>97: bright foreground White</li>
906 <li>100: bright background Dark Gray</li>
907 <li>101: bright background Red</li>
908 <li>102: bright background Green</li>
909 <li>103: bright background Yellow</li>
910 <li>104: bright background Blue</li>
911 <li>105: bright background Magenta</li>
912 <li>106: bright background Cyan</li>
913 <li>107: bright background White</li>
914 </ul>
915
916 @param formatCodes list of format codes
917 @type list of str
918 @param textCursor reference to the text cursor
919 @type QTextCursor
920 """
921 if not formatCodes:
922 # empty format codes list is treated as a reset
923 formatCodes = ["0"]
924
925 charFormat = textCursor.charFormat()
926 for formatCode in formatCodes:
927 try:
928 formatCode = int(formatCode)
929 except ValueError:
930 # ignore non digit values
931 continue
932
933 if formatCode == 0:
934 charFormat.setFontWeight(50)
935 charFormat.setFontItalic(False)
936 charFormat.setFontUnderline(False)
937 charFormat.setFontStrikeOut(False)
938 charFormat.setFontOverline(False)
939 charFormat.setForeground(self.DefaultForeground)
940 charFormat.setBackground(self.DefaultBackground)
941 elif formatCode == 1:
942 charFormat.setFontWeight(75)
943 elif formatCode == 2:
944 charFormat.setFontWeight(25)
945 elif formatCode == 3:
946 charFormat.setFontItalic(True)
947 elif formatCode == 4:
948 charFormat.setFontUnderline(True)
949 elif formatCode == 9:
950 charFormat.setFontStrikeOut(True)
951 elif formatCode in (21, 22):
952 charFormat.setFontWeight(50)
953 elif formatCode == 23:
954 charFormat.setFontItalic(False)
955 elif formatCode == 24:
956 charFormat.setFontUnderline(False)
957 elif formatCode == 29:
958 charFormat.setFontStrikeOut(False)
959 elif formatCode == 53:
960 charFormat.setFontOverline(True)
961 elif formatCode == 55:
962 charFormat.setFontOverline(False)
963 elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37):
964 charFormat.setForeground(
965 AnsiColorSchemes[self.__colorScheme][formatCode - 30])
966 elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47):
967 charFormat.setBackground(
968 AnsiColorSchemes[self.__colorScheme][formatCode - 40])
969 elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97):
970 charFormat.setForeground(
971 AnsiColorSchemes[self.__colorScheme][formatCode - 80])
972 elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107):
973 charFormat.setBackground(
974 AnsiColorSchemes[self.__colorScheme][formatCode - 90])
975 elif formatCode == 39:
976 charFormat.setForeground(self.DefaultForeground)
977 elif formatCode == 49:
978 charFormat.setBackground(self.DefaultBackground)
979
980 textCursor.setCharFormat(charFormat)
981
982 def __doZoom(self, value):
983 """
984 Private slot to zoom the REPL pane.
985
986 @param value zoom value
987 @type int
988 """
989 if value < self.__currentZoom:
990 self.replEdit.zoomOut(self.__currentZoom - value)
991 elif value > self.__currentZoom:
992 self.replEdit.zoomIn(value - self.__currentZoom)
993 self.__currentZoom = value
994
995 def getCurrentPort(self):
996 """
997 Public method to determine the port path of the selected device.
998
999 @return path of the port of the selected device
1000 @rtype str
1001 """
1002 portName = self.deviceTypeComboBox.currentData(self.DevicePortRole)
1003 if portName:
1004 if Globals.isWindowsPlatform():
1005 # return it unchanged
1006 return portName
1007 else:
1008 # return with device path prepended
1009 return "/dev/{0}".format(portName)
1010 else:
1011 return ""
1012
1013 def getCurrentBoard(self):
1014 """
1015 Public method to get the board name of the selected device.
1016
1017 @return board name of the selected device
1018 @rtype str
1019 """
1020 boardName = self.deviceTypeComboBox.currentData(self.DeviceBoardRole)
1021 return boardName
1022
1023 def getDeviceWorkspace(self):
1024 """
1025 Public method to get the workspace directory of the device.
1026
1027 @return workspace directory of the device
1028 @rtype str
1029 """
1030 if self.__device:
1031 return self.__device.getWorkspace()
1032 else:
1033 return ""
1034
1035 def __connectToDevice(self):
1036 """
1037 Private method to connect to the selected device.
1038 """
1039 port = self.getCurrentPort()
1040 if not port:
1041 from .ConnectionSelectionDialog import ConnectionSelectionDialog
1042 with E5OverridenCursor():
1043 dlg = ConnectionSelectionDialog(
1044 self.__unknownPorts, self.__lastPort, self.__lastDeviceType
1045 )
1046 if dlg.exec() == QDialog.DialogCode.Accepted:
1047 vid, pid, port, deviceType = dlg.getData()
1048
1049 self.deviceIconLabel.setPixmap(
1050 MicroPythonDevices.getDeviceIcon(deviceType, False))
1051 self.__device = MicroPythonDevices.getDevice(
1052 deviceType, self, vid, pid)
1053 self.__device.setButtons()
1054
1055 self.__lastPort = port
1056 self.__lastDeviceType = deviceType
1057 else:
1058 return
1059
1060 if self.__interface.connectToDevice(port):
1061 self.__setConnected(True)
1062
1063 if (Preferences.getMicroPython("SyncTimeAfterConnect") and
1064 self.__device.hasTimeCommands()):
1065 self.__synchronizeTime(quiet=True)
1066 else:
1067 with E5OverridenCursor():
1068 E5MessageBox.warning(
1069 self,
1070 self.tr("Serial Device Connect"),
1071 self.tr("""<p>Cannot connect to device at serial"""
1072 """ port <b>{0}</b>.</p>""").format(port))
1073
1074 def __disconnectFromDevice(self):
1075 """
1076 Private method to disconnect from the device.
1077 """
1078 self.__interface.disconnectFromDevice()
1079 self.__setConnected(False)
1080
1081 @pyqtSlot()
1082 def on_runButton_clicked(self):
1083 """
1084 Private slot to execute the script of the active editor on the
1085 selected device.
1086
1087 If the REPL is not active yet, it will be activated, which might cause
1088 an unconnected device to be connected.
1089 """
1090 if not self.__device:
1091 self.__showNoDeviceMessage()
1092 return
1093
1094 aw = e5App().getObject("ViewManager").activeWindow()
1095 if aw is None:
1096 E5MessageBox.critical(
1097 self,
1098 self.tr("Run Script"),
1099 self.tr("""There is no editor open. Abort..."""))
1100 return
1101
1102 script = aw.text()
1103 if not script:
1104 E5MessageBox.critical(
1105 self,
1106 self.tr("Run Script"),
1107 self.tr("""The current editor does not contain a script."""
1108 """ Abort..."""))
1109 return
1110
1111 ok, reason = self.__device.canRunScript()
1112 if not ok:
1113 E5MessageBox.warning(
1114 self,
1115 self.tr("Run Script"),
1116 self.tr("""<p>Cannot run script.</p><p>Reason:"""
1117 """ {0}</p>""").format(reason))
1118 return
1119
1120 if not self.replButton.isChecked():
1121 # activate on the REPL
1122 self.on_replButton_clicked(True)
1123 if self.replButton.isChecked():
1124 self.__device.runScript(script)
1125
1126 @pyqtSlot()
1127 def on_openButton_clicked(self):
1128 """
1129 Private slot to open a file of the connected device.
1130 """
1131 if not self.__device:
1132 self.__showNoDeviceMessage()
1133 return
1134
1135 workspace = self.getDeviceWorkspace()
1136 if workspace:
1137 fileName = E5FileDialog.getOpenFileName(
1138 self,
1139 self.tr("Open Python File"),
1140 workspace,
1141 self.tr("Python3 Files (*.py);;All Files (*)"))
1142 if fileName:
1143 e5App().getObject("ViewManager").openSourceFile(fileName)
1144
1145 @pyqtSlot()
1146 def on_saveButton_clicked(self):
1147 """
1148 Private slot to save the current editor to the connected device.
1149 """
1150 if not self.__device:
1151 self.__showNoDeviceMessage()
1152 return
1153
1154 aw = e5App().getObject("ViewManager").activeWindow()
1155 if aw:
1156 workspace = self.getDeviceWorkspace()
1157 if workspace:
1158 aw.saveFileAs(workspace)
1159
1160 @pyqtSlot(bool)
1161 def on_chartButton_clicked(self, checked):
1162 """
1163 Private slot to open a chart view to plot data received from the
1164 connected device.
1165
1166 If the selected device is not connected yet, this will be done now.
1167
1168 @param checked state of the button
1169 @type bool
1170 """
1171 if not HAS_QTCHART:
1172 # QtChart not available => fail silently
1173 return
1174
1175 if not self.__device:
1176 self.__showNoDeviceMessage()
1177 return
1178
1179 if checked:
1180 ok, reason = self.__device.canStartPlotter()
1181 if not ok:
1182 E5MessageBox.warning(
1183 self,
1184 self.tr("Start Chart"),
1185 self.tr("""<p>The Chart cannot be started.</p><p>Reason:"""
1186 """ {0}</p>""").format(reason))
1187 return
1188
1189 self.__chartWidget = MicroPythonGraphWidget(self)
1190 self.__interface.dataReceived.connect(
1191 self.__chartWidget.processData)
1192 self.__chartWidget.dataFlood.connect(
1193 self.handleDataFlood)
1194
1195 self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget,
1196 UI.PixmapCache.getIcon("chart"),
1197 self.tr("µPy Chart"))
1198 self.__ui.showSideWidget(self.__chartWidget)
1199
1200 if not self.__interface.isConnected():
1201 self.__connectToDevice()
1202 if self.__device.forceInterrupt():
1203 # send a Ctrl-B (exit raw mode)
1204 self.__interface.write(b'\x02')
1205 # send Ctrl-C (keyboard interrupt)
1206 self.__interface.write(b'\x03')
1207
1208 self.__device.setPlotter(True)
1209 else:
1210 if self.__chartWidget.isDirty():
1211 res = E5MessageBox.okToClearData(
1212 self,
1213 self.tr("Unsaved Chart Data"),
1214 self.tr("""The chart contains unsaved data."""),
1215 self.__chartWidget.saveData)
1216 if not res:
1217 # abort
1218 return
1219
1220 self.__interface.dataReceived.disconnect(
1221 self.__chartWidget.processData)
1222 self.__chartWidget.dataFlood.disconnect(
1223 self.handleDataFlood)
1224
1225 if (not self.replButton.isChecked() and
1226 not self.filesButton.isChecked()):
1227 self.__disconnectFromDevice()
1228
1229 self.__device.setPlotter(False)
1230 self.__ui.removeSideWidget(self.__chartWidget)
1231
1232 self.__chartWidget.deleteLater()
1233 self.__chartWidget = None
1234
1235 self.chartButton.setChecked(checked)
1236
1237 @pyqtSlot()
1238 def handleDataFlood(self):
1239 """
1240 Public slot handling a data flood from the device.
1241 """
1242 self.on_connectButton_clicked()
1243 self.__device.handleDataFlood()
1244
1245 @pyqtSlot(bool)
1246 def on_filesButton_clicked(self, checked):
1247 """
1248 Private slot to open a file manager window to the connected device.
1249
1250 If the selected device is not connected yet, this will be done now.
1251
1252 @param checked state of the button
1253 @type bool
1254 """
1255 if not self.__device:
1256 self.__showNoDeviceMessage()
1257 return
1258
1259 if checked:
1260 ok, reason = self.__device.canStartFileManager()
1261 if not ok:
1262 E5MessageBox.warning(
1263 self,
1264 self.tr("Start File Manager"),
1265 self.tr("""<p>The File Manager cannot be started.</p>"""
1266 """<p>Reason: {0}</p>""").format(reason))
1267 return
1268
1269 with E5OverrideCursor():
1270 if not self.__interface.isConnected():
1271 self.__connectToDevice()
1272 if self.__connected:
1273 self.__fileManagerWidget = MicroPythonFileManagerWidget(
1274 self.__interface,
1275 self.__device.supportsLocalFileAccess(),
1276 self)
1277
1278 self.__ui.addSideWidget(
1279 self.__ui.BottomSide,
1280 self.__fileManagerWidget,
1281 UI.PixmapCache.getIcon("filemanager"),
1282 self.tr("µPy Files")
1283 )
1284 self.__ui.showSideWidget(self.__fileManagerWidget)
1285
1286 self.__device.setFileManager(True)
1287
1288 self.__fileManagerWidget.start()
1289 else:
1290 self.__fileManagerWidget.stop()
1291
1292 if (not self.replButton.isChecked() and
1293 not self.chartButton.isChecked()):
1294 self.__disconnectFromDevice()
1295
1296 self.__device.setFileManager(False)
1297 self.__ui.removeSideWidget(self.__fileManagerWidget)
1298
1299 self.__fileManagerWidget.deleteLater()
1300 self.__fileManagerWidget = None
1301
1302 self.filesButton.setChecked(checked)
1303
1304 ##################################################################
1305 ## Super Menu related methods below
1306 ##################################################################
1307
1308 def __aboutToShowSuperMenu(self):
1309 """
1310 Private slot to populate the Super Menu before showing it.
1311 """
1312 self.__superMenu.clear()
1313
1314 # prepare the download menu
1315 if self.__device:
1316 menuEntries = self.__device.getDownloadMenuEntries()
1317 if menuEntries:
1318 downloadMenu = QMenu(self.tr("Downloads"), self.__superMenu)
1319 for text, url in menuEntries:
1320 if text == "<separator>":
1321 downloadMenu.addSeparator()
1322 else:
1323 downloadMenu.addAction(
1324 text,
1325 functools.partial(self.__downloadFromUrl, url)
1326 )
1327 else:
1328 downloadMenu = None
1329
1330 # populate the super menu
1331 hasTime = self.__device.hasTimeCommands() if self.__device else False
1332
1333 act = self.__superMenu.addAction(
1334 self.tr("Show Version"), self.__showDeviceVersion)
1335 act.setEnabled(self.__connected)
1336 act = self.__superMenu.addAction(
1337 self.tr("Show Implementation"), self.__showImplementation)
1338 act.setEnabled(self.__connected)
1339 self.__superMenu.addSeparator()
1340 if hasTime:
1341 act = self.__superMenu.addAction(
1342 self.tr("Synchronize Time"), self.__synchronizeTime)
1343 act.setEnabled(self.__connected)
1344 act = self.__superMenu.addAction(
1345 self.tr("Show Device Time"), self.__showDeviceTime)
1346 act.setEnabled(self.__connected)
1347 self.__superMenu.addAction(
1348 self.tr("Show Local Time"), self.__showLocalTime)
1349 if hasTime:
1350 act = self.__superMenu.addAction(
1351 self.tr("Show Time"), self.__showLocalAndDeviceTime)
1352 act.setEnabled(self.__connected)
1353 self.__superMenu.addSeparator()
1354 if not Globals.isWindowsPlatform():
1355 available = self.__mpyCrossAvailable()
1356 act = self.__superMenu.addAction(
1357 self.tr("Compile Python File"), self.__compileFile2Mpy)
1358 act.setEnabled(available)
1359 act = self.__superMenu.addAction(
1360 self.tr("Compile Current Editor"), self.__compileEditor2Mpy)
1361 aw = e5App().getObject("ViewManager").activeWindow()
1362 act.setEnabled(available and bool(aw))
1363 self.__superMenu.addSeparator()
1364 if self.__device:
1365 self.__device.addDeviceMenuEntries(self.__superMenu)
1366 self.__superMenu.addSeparator()
1367 if downloadMenu is None:
1368 # generic download action
1369 act = self.__superMenu.addAction(
1370 self.tr("Download Firmware"), self.__downloadFirmware)
1371 act.setEnabled(self.__device.hasFirmwareUrl())
1372 else:
1373 # download sub-menu
1374 self.__superMenu.addMenu(downloadMenu)
1375 self.__superMenu.addSeparator()
1376 act = self.__superMenu.addAction(
1377 self.tr("Show Documentation"), self.__showDocumentation)
1378 act.setEnabled(self.__device.hasDocumentationUrl())
1379 self.__superMenu.addSeparator()
1380 if not self.__device.hasFlashMenuEntry():
1381 self.__superMenu.addAction(self.tr("Flash UF2 Device"),
1382 self.__flashUF2)
1383 self.__superMenu.addSeparator()
1384 self.__superMenu.addAction(self.tr("Manage Unknown Devices"),
1385 self.__manageUnknownDevices)
1386 self.__superMenu.addAction(self.tr("Ignored Serial Devices"),
1387 self.__manageIgnored)
1388 self.__superMenu.addSeparator()
1389 self.__superMenu.addAction(self.tr("Configure"), self.__configure)
1390
1391 @pyqtSlot()
1392 def __showDeviceVersion(self):
1393 """
1394 Private slot to show some version info about MicroPython of the device.
1395 """
1396 try:
1397 versionInfo = self.__interface.version()
1398 if versionInfo:
1399 msg = self.tr(
1400 "<h3>Device Version Information</h3>"
1401 )
1402 msg += "<table>"
1403 for key, value in versionInfo.items():
1404 msg += "<tr><td><b>{0}</b></td><td>{1}</td></tr>".format(
1405 key.capitalize(), value)
1406 msg += "</table>"
1407 else:
1408 msg = self.tr("No version information available.")
1409
1410 E5MessageBox.information(
1411 self,
1412 self.tr("Device Version Information"),
1413 msg)
1414 except Exception as exc:
1415 self.__showError("version()", str(exc))
1416
1417 @pyqtSlot()
1418 def __showImplementation(self):
1419 """
1420 Private slot to show some implementation related information.
1421 """
1422 try:
1423 impInfo = self.__interface.getImplementation()
1424 if impInfo["name"] == "micropython":
1425 name = "MicroPython"
1426 elif impInfo["name"] == "circuitpython":
1427 name = "CircuitPython"
1428 elif impInfo["name"] == "unknown":
1429 name = self.tr("unknown")
1430 else:
1431 name = impInfo["name"]
1432 version = (
1433 self.tr("unknown")
1434 if impInfo["version"] == "unknown" else
1435 impInfo["version"]
1436 )
1437
1438 E5MessageBox.information(
1439 self,
1440 self.tr("Device Implementation Information"),
1441 self.tr(
1442 "<h3>Device Implementation Information</h3>"
1443 "<p>This device contains <b>{0} {1}</b>.</p>"
1444 ).format(name, version)
1445 )
1446 except Exception as exc:
1447 self.__showError("getImplementation()", str(exc))
1448
1449 @pyqtSlot()
1450 def __synchronizeTime(self, quiet=False):
1451 """
1452 Private slot to set the time of the connected device to the local
1453 computer's time.
1454
1455 @param quiet flag indicating to not show a message
1456 @type bool
1457 """
1458 if self.__device and self.__device.hasTimeCommands():
1459 try:
1460 self.__interface.syncTime(self.__device.getDeviceType())
1461
1462 if not quiet:
1463 with E5OverridenCursor():
1464 E5MessageBox.information(
1465 self,
1466 self.tr("Synchronize Time"),
1467 self.tr("<p>The time of the connected device was"
1468 " synchronized with the local time.</p>") +
1469 self.__getDeviceTime()
1470 )
1471 except Exception as exc:
1472 self.__showError("syncTime()", str(exc))
1473
1474 def __getDeviceTime(self):
1475 """
1476 Private method to get a string containing the date and time of the
1477 connected device.
1478
1479 @return date and time of the connected device
1480 @rtype str
1481 """
1482 if self.__device and self.__device.hasTimeCommands():
1483 try:
1484 dateTimeString = self.__interface.getTime()
1485 try:
1486 date, time = dateTimeString.strip().split(None, 1)
1487 return self.tr(
1488 "<h3>Device Date and Time</h3>"
1489 "<table>"
1490 "<tr><td><b>Date</b></td><td>{0}</td></tr>"
1491 "<tr><td><b>Time</b></td><td>{1}</td></tr>"
1492 "</table>"
1493 ).format(date, time)
1494 except ValueError:
1495 return self.tr(
1496 "<h3>Device Date and Time</h3>"
1497 "<p>{0}</p>"
1498 ).format(dateTimeString.strip())
1499 except Exception as exc:
1500 self.__showError("getTime()", str(exc))
1501 return ""
1502 else:
1503 return ""
1504
1505 @pyqtSlot()
1506 def __showDeviceTime(self):
1507 """
1508 Private slot to show the date and time of the connected device.
1509 """
1510 msg = self.__getDeviceTime()
1511 if msg:
1512 E5MessageBox.information(
1513 self,
1514 self.tr("Device Date and Time"),
1515 msg)
1516
1517 @pyqtSlot()
1518 def __showLocalTime(self):
1519 """
1520 Private slot to show the local date and time.
1521 """
1522 localdatetime = time.localtime()
1523 localdate = time.strftime('%Y-%m-%d', localdatetime)
1524 localtime = time.strftime('%H:%M:%S', localdatetime)
1525 E5MessageBox.information(
1526 self,
1527 self.tr("Local Date and Time"),
1528 self.tr("<h3>Local Date and Time</h3>"
1529 "<table>"
1530 "<tr><td><b>Date</b></td><td>{0}</td></tr>"
1531 "<tr><td><b>Time</b></td><td>{1}</td></tr>"
1532 "</table>"
1533 ).format(localdate, localtime)
1534 )
1535
1536 @pyqtSlot()
1537 def __showLocalAndDeviceTime(self):
1538 """
1539 Private slot to show the local and device time side-by-side.
1540 """
1541 localdatetime = time.localtime()
1542 localdate = time.strftime('%Y-%m-%d', localdatetime)
1543 localtime = time.strftime('%H:%M:%S', localdatetime)
1544
1545 try:
1546 deviceDateTimeString = self.__interface.getTime()
1547 try:
1548 devicedate, devicetime = (
1549 deviceDateTimeString.strip().split(None, 1)
1550 )
1551 E5MessageBox.information(
1552 self,
1553 self.tr("Date and Time"),
1554 self.tr("<table>"
1555 "<tr><th></th><th>Local Date and Time</th>"
1556 "<th>Device Date and Time</th></tr>"
1557 "<tr><td><b>Date</b></td>"
1558 "<td align='center'>{0}</td>"
1559 "<td align='center'>{2}</td></tr>"
1560 "<tr><td><b>Time</b></td>"
1561 "<td align='center'>{1}</td>"
1562 "<td align='center'>{3}</td></tr>"
1563 "</table>"
1564 ).format(localdate, localtime,
1565 devicedate, devicetime)
1566 )
1567 except ValueError:
1568 E5MessageBox.information(
1569 self,
1570 self.tr("Date and Time"),
1571 self.tr("<table>"
1572 "<tr><th>Local Date and Time</th>"
1573 "<th>Device Date and Time</th></tr>"
1574 "<tr><td align='center'>{0} {1}</td>"
1575 "<td align='center'>{2}</td></tr>"
1576 "</table>"
1577 ).format(localdate, localtime,
1578 deviceDateTimeString.strip())
1579 )
1580 except Exception as exc:
1581 self.__showError("getTime()", str(exc))
1582
1583 def __showError(self, method, error):
1584 """
1585 Private method to show some error message.
1586
1587 @param method name of the method the error occured in
1588 @type str
1589 @param error error message
1590 @type str
1591 """
1592 with E5OverridenCursor():
1593 E5MessageBox.warning(
1594 self,
1595 self.tr("Error handling device"),
1596 self.tr("<p>There was an error communicating with the"
1597 " connected device.</p><p>Method: {0}</p>"
1598 "<p>Message: {1}</p>")
1599 .format(method, error))
1600
1601 def __mpyCrossAvailable(self):
1602 """
1603 Private method to check the availability of mpy-cross.
1604
1605 @return flag indicating the availability of mpy-cross
1606 @rtype bool
1607 """
1608 available = False
1609 program = Preferences.getMicroPython("MpyCrossCompiler")
1610 if not program:
1611 program = "mpy-cross"
1612 if Utilities.isinpath(program):
1613 available = True
1614 else:
1615 if Utilities.isExecutable(program):
1616 available = True
1617
1618 return available
1619
1620 def __crossCompile(self, pythonFile="", title=""):
1621 """
1622 Private method to cross compile a Python file to a .mpy file.
1623
1624 @param pythonFile name of the Python file to be compiled
1625 @type str
1626 @param title title for the various dialogs
1627 @type str
1628 """
1629 program = Preferences.getMicroPython("MpyCrossCompiler")
1630 if not program:
1631 program = "mpy-cross"
1632 if not Utilities.isinpath(program):
1633 E5MessageBox.critical(
1634 self,
1635 title,
1636 self.tr("""The MicroPython cross compiler"""
1637 """ <b>mpy-cross</b> cannot be found. Ensure it"""
1638 """ is in the search path or configure it on"""
1639 """ the MicroPython configuration page."""))
1640 return
1641
1642 if not pythonFile:
1643 defaultDirectory = ""
1644 aw = e5App().getObject("ViewManager").activeWindow()
1645 if aw:
1646 fn = aw.getFileName()
1647 if fn:
1648 defaultDirectory = os.path.dirname(fn)
1649 if not defaultDirectory:
1650 defaultDirectory = (
1651 Preferences.getMicroPython("MpyWorkspace") or
1652 Preferences.getMultiProject("Workspace") or
1653 os.path.expanduser("~")
1654 )
1655 pythonFile = E5FileDialog.getOpenFileName(
1656 self,
1657 title,
1658 defaultDirectory,
1659 self.tr("Python Files (*.py);;All Files (*)"))
1660 if not pythonFile:
1661 # user cancelled
1662 return
1663
1664 if not os.path.exists(pythonFile):
1665 E5MessageBox.critical(
1666 self,
1667 title,
1668 self.tr("""The Python file <b>{0}</b> does not exist."""
1669 """ Aborting...""").format(pythonFile))
1670 return
1671
1672 compileArgs = [
1673 pythonFile,
1674 ]
1675 dlg = E5ProcessDialog(self.tr("'mpy-cross' Output"), title)
1676 res = dlg.startProcess(program, compileArgs)
1677 if res:
1678 dlg.exec()
1679
1680 @pyqtSlot()
1681 def __compileFile2Mpy(self):
1682 """
1683 Private slot to cross compile a Python file (*.py) to a .mpy file.
1684 """
1685 self.__crossCompile(title=self.tr("Compile Python File"))
1686
1687 @pyqtSlot()
1688 def __compileEditor2Mpy(self):
1689 """
1690 Private slot to cross compile the current editor to a .mpy file.
1691 """
1692 aw = e5App().getObject("ViewManager").activeWindow()
1693 if not aw.checkDirty():
1694 # editor still has unsaved changes, abort...
1695 return
1696 if not aw.isPyFile():
1697 # no Python file
1698 E5MessageBox.critical(
1699 self,
1700 self.tr("Compile Current Editor"),
1701 self.tr("""The current editor does not contain a Python"""
1702 """ file. Aborting..."""))
1703 return
1704
1705 self.__crossCompile(
1706 pythonFile=aw.getFileName(),
1707 title=self.tr("Compile Current Editor")
1708 )
1709
1710 @pyqtSlot()
1711 def __showDocumentation(self):
1712 """
1713 Private slot to open the documentation URL for the selected device.
1714 """
1715 if self.__device is None or not self.__device.hasDocumentationUrl():
1716 # abort silently
1717 return
1718
1719 url = self.__device.getDocumentationUrl()
1720 e5App().getObject("UserInterface").launchHelpViewer(url)
1721
1722 @pyqtSlot()
1723 def __downloadFirmware(self):
1724 """
1725 Private slot to open the firmware download page.
1726 """
1727 if self.__device is None or not self.__device.hasFirmwareUrl():
1728 # abort silently
1729 return
1730
1731 self.__device.downloadFirmware()
1732
1733 def __downloadFromUrl(self, url):
1734 """
1735 Private method to open a web browser for the given URL.
1736
1737 @param url URL to be opened
1738 @type str
1739 """
1740 if self.__device is None:
1741 # abort silently
1742 return
1743
1744 if url:
1745 e5App().getObject("UserInterface").launchHelpViewer(url)
1746
1747 @pyqtSlot()
1748 def __manageIgnored(self):
1749 """
1750 Private slot to manage the list of ignored serial devices.
1751 """
1752 from .IgnoredDevicesDialog import IgnoredDevicesDialog
1753
1754 dlg = IgnoredDevicesDialog(
1755 Preferences.getMicroPython("IgnoredUnknownDevices"),
1756 self)
1757 if dlg.exec() == QDialog.DialogCode.Accepted:
1758 ignoredDevices = dlg.getDevices()
1759 Preferences.setMicroPython("IgnoredUnknownDevices",
1760 ignoredDevices)
1761
1762 @pyqtSlot()
1763 def __configure(self):
1764 """
1765 Private slot to open the MicroPython configuration page.
1766 """
1767 e5App().getObject("UserInterface").showPreferences("microPythonPage")
1768
1769 @pyqtSlot()
1770 def __manageUnknownDevices(self):
1771 """
1772 Private slot to manage manually added boards (i.e. those not in the
1773 list of supported boards).
1774 """
1775 from .UnknownDevicesDialog import UnknownDevicesDialog
1776 dlg = UnknownDevicesDialog()
1777 dlg.exec()
1778
1779 def __addUnknownDevices(self, devices):
1780 """
1781 Private method to add devices to the list of manually added boards.
1782
1783 @param devices list of not ignored but unknown devices
1784 @type list of tuple of (int, int, str)
1785 """
1786 from .AddEditDevicesDialog import AddEditDevicesDialog
1787
1788 if len(devices) > 1:
1789 from E5Gui.E5ListSelectionDialog import E5ListSelectionDialog
1790 sdlg = E5ListSelectionDialog(
1791 [d[2] for d in devices],
1792 title=self.tr("Add Unknown Devices"),
1793 message=self.tr("Select the devices to be added:"),
1794 checkBoxSelection=True
1795 )
1796 if sdlg.exec() == QDialog.DialogCode.Accepted:
1797 selectedDevices = sdlg.getSelection()
1798 else:
1799 selectedDevices = devices[0][2]
1800
1801 if selectedDevices:
1802 manualDevices = Preferences.getMicroPython("ManualDevices")
1803 for vid, pid, description in devices:
1804 if description in selectedDevices:
1805 dlg = AddEditDevicesDialog(vid, pid, description)
1806 if dlg.exec() == QDialog.DialogCode.Accepted:
1807 manualDevices.append(dlg.getDeviceDict())
1808 Preferences.setMicroPython("ManualDevices", manualDevices)
1809
1810 # rescan the ports
1811 self.__populateDeviceTypeComboBox()
1812
1813 @pyqtSlot()
1814 def __flashUF2(self):
1815 """
1816 Private slot to flash MicroPython/CircuitPython to a device
1817 support the UF2 bootloader.
1818 """
1819 dlg = UF2FlashDialog.UF2FlashDialog()
1820 dlg.exec()

eric ide

mercurial