src/eric7/MicroPython/MicroPythonWidget.py

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

eric ide

mercurial