eric6/MicroPython/MicroPythonWidget.py

branch
micropython
changeset 7134
21d23ca51680
parent 7133
7aa4832b3730
child 7135
44fcfc99b864
equal deleted inserted replaced
7133:7aa4832b3730 7134:21d23ca51680
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the MicroPython REPL widget.
8 """
9
10 from __future__ import unicode_literals
11
12 import re
13 import time
14
15 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint, QEvent
16 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush
17 from PyQt5.QtWidgets import (
18 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy,
19 QTextEdit, QToolButton
20 )
21
22 from E5Gui.E5ZoomWidget import E5ZoomWidget
23 from E5Gui import E5MessageBox, E5FileDialog
24 from E5Gui.E5Application import e5App
25
26 from .Ui_MicroPythonWidget import Ui_MicroPythonWidget
27
28 from . import MicroPythonDevices
29 try:
30 from .MicroPythonGraphWidget import MicroPythonGraphWidget
31 HAS_QTCHART = True
32 except ImportError:
33 HAS_QTCHART = False
34 from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget
35 try:
36 from .MicroPythonCommandsInterface import MicroPythonCommandsInterface
37 HAS_QTSERIALPORT = True
38 except ImportError:
39 HAS_QTSERIALPORT = False
40
41 import Globals
42 import UI.PixmapCache
43 import Preferences
44
45 # ANSI Colors (see https://en.wikipedia.org/wiki/ANSI_escape_code)
46 AnsiColorSchemes = {
47 "Windows 7": {
48 0: QBrush(QColor(0, 0, 0)),
49 1: QBrush(QColor(128, 0, 0)),
50 2: QBrush(QColor(0, 128, 0)),
51 3: QBrush(QColor(128, 128, 0)),
52 4: QBrush(QColor(0, 0, 128)),
53 5: QBrush(QColor(128, 0, 128)),
54 6: QBrush(QColor(0, 128, 128)),
55 7: QBrush(QColor(192, 192, 192)),
56 10: QBrush(QColor(128, 128, 128)),
57 11: QBrush(QColor(255, 0, 0)),
58 12: QBrush(QColor(0, 255, 0)),
59 13: QBrush(QColor(255, 255, 0)),
60 14: QBrush(QColor(0, 0, 255)),
61 15: QBrush(QColor(255, 0, 255)),
62 16: QBrush(QColor(0, 255, 255)),
63 17: QBrush(QColor(255, 255, 255)),
64 },
65 "Windows 10": {
66 0: QBrush(QColor(12, 12, 12)),
67 1: QBrush(QColor(197, 15, 31)),
68 2: QBrush(QColor(19, 161, 14)),
69 3: QBrush(QColor(193, 156, 0)),
70 4: QBrush(QColor(0, 55, 218)),
71 5: QBrush(QColor(136, 23, 152)),
72 6: QBrush(QColor(58, 150, 221)),
73 7: QBrush(QColor(204, 204, 204)),
74 10: QBrush(QColor(118, 118, 118)),
75 11: QBrush(QColor(231, 72, 86)),
76 12: QBrush(QColor(22, 198, 12)),
77 13: QBrush(QColor(249, 241, 165)),
78 14: QBrush(QColor(59, 12, 255)),
79 15: QBrush(QColor(180, 0, 158)),
80 16: QBrush(QColor(97, 214, 214)),
81 17: QBrush(QColor(242, 242, 242)),
82 },
83 "PuTTY": {
84 0: QBrush(QColor(0, 0, 0)),
85 1: QBrush(QColor(187, 0, 0)),
86 2: QBrush(QColor(0, 187, 0)),
87 3: QBrush(QColor(187, 187, 0)),
88 4: QBrush(QColor(0, 0, 187)),
89 5: QBrush(QColor(187, 0, 187)),
90 6: QBrush(QColor(0, 187, 187)),
91 7: QBrush(QColor(187, 187, 187)),
92 10: QBrush(QColor(85, 85, 85)),
93 11: QBrush(QColor(255, 85, 85)),
94 12: QBrush(QColor(85, 255, 85)),
95 13: QBrush(QColor(255, 255, 85)),
96 14: QBrush(QColor(85, 85, 255)),
97 15: QBrush(QColor(255, 85, 255)),
98 16: QBrush(QColor(85, 255, 255)),
99 17: QBrush(QColor(255, 255, 255)),
100 },
101 "xterm": {
102 0: QBrush(QColor(0, 0, 0)),
103 1: QBrush(QColor(205, 0, 0)),
104 2: QBrush(QColor(0, 205, 0)),
105 3: QBrush(QColor(205, 205, 0)),
106 4: QBrush(QColor(0, 0, 238)),
107 5: QBrush(QColor(205, 0, 205)),
108 6: QBrush(QColor(0, 205, 205)),
109 7: QBrush(QColor(229, 229, 229)),
110 10: QBrush(QColor(127, 127, 127)),
111 11: QBrush(QColor(255, 0, 0)),
112 12: QBrush(QColor(0, 255, 0)),
113 13: QBrush(QColor(255, 255, 0)),
114 14: QBrush(QColor(0, 0, 255)),
115 15: QBrush(QColor(255, 0, 255)),
116 16: QBrush(QColor(0, 255, 255)),
117 17: QBrush(QColor(255, 255, 255)),
118 },
119 "Ubuntu": {
120 0: QBrush(QColor(1, 1, 1)),
121 1: QBrush(QColor(222, 56, 43)),
122 2: QBrush(QColor(57, 181, 74)),
123 3: QBrush(QColor(255, 199, 6)),
124 4: QBrush(QColor(0, 11, 184)),
125 5: QBrush(QColor(118, 38, 113)),
126 6: QBrush(QColor(44, 181, 233)),
127 7: QBrush(QColor(204, 204, 204)),
128 10: QBrush(QColor(128, 128, 128)),
129 11: QBrush(QColor(255, 0, 0)),
130 12: QBrush(QColor(0, 255, 0)),
131 13: QBrush(QColor(255, 255, 0)),
132 14: QBrush(QColor(0, 0, 255)),
133 15: QBrush(QColor(255, 0, 255)),
134 16: QBrush(QColor(0, 255, 255)),
135 17: QBrush(QColor(255, 255, 255)),
136 },
137 }
138
139
140 # TODO: add config option to synchronize the device time upon connect
141
142 class MicroPythonWidget(QWidget, Ui_MicroPythonWidget):
143 """
144 Class implementing the MicroPython REPL widget.
145
146 @signal dataReceived(data) emitted to send data received via the serial
147 connection for further processing
148 """
149 ZoomMin = -10
150 ZoomMax = 20
151
152 DeviceTypeRole = Qt.UserRole
153 DevicePortRole = Qt.UserRole + 1
154
155 dataReceived = pyqtSignal(bytes)
156
157 def __init__(self, parent=None):
158 """
159 Constructor
160
161 @param parent reference to the parent widget
162 @type QWidget
163 """
164 super(MicroPythonWidget, self).__init__(parent)
165 self.setupUi(self)
166
167 self.__ui = parent
168
169 self.__superMenu = QMenu(self)
170 self.__superMenu.aboutToShow.connect(self.__aboutToShowSuperMenu)
171
172 self.menuButton.setObjectName(
173 "micropython_supermenu_button")
174 self.menuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
175 self.menuButton.setToolTip(self.tr("pip Menu"))
176 self.menuButton.setPopupMode(QToolButton.InstantPopup)
177 self.menuButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
178 self.menuButton.setFocusPolicy(Qt.NoFocus)
179 self.menuButton.setAutoRaise(True)
180 self.menuButton.setShowMenuInside(True)
181 self.menuButton.setMenu(self.__superMenu)
182
183 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
184 "", False))
185
186 self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
187 self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
188
189 self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
190 self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
191 self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
192 self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager"))
193 self.chartButton.setIcon(UI.PixmapCache.getIcon("chart"))
194 self.connectButton.setIcon(UI.PixmapCache.getIcon("linkConnect"))
195
196 self.__zoomLayout = QHBoxLayout()
197 spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding,
198 QSizePolicy.Minimum)
199 self.__zoomLayout.addSpacerItem(spacerItem)
200
201 self.__zoom0 = self.replEdit.fontPointSize()
202 self.__zoomWidget = E5ZoomWidget(
203 UI.PixmapCache.getPixmap("zoomOut"),
204 UI.PixmapCache.getPixmap("zoomIn"),
205 UI.PixmapCache.getPixmap("zoomReset"), self)
206 self.__zoomLayout.addWidget(self.__zoomWidget)
207 self.layout().insertLayout(
208 self.layout().count() - 1,
209 self.__zoomLayout)
210 self.__zoomWidget.setMinimum(self.ZoomMin)
211 self.__zoomWidget.setMaximum(self.ZoomMax)
212 self.__zoomWidget.valueChanged.connect(self.__doZoom)
213 self.__currentZoom = 0
214
215 self.__fileManagerWidget = None
216
217 self.__interface = MicroPythonCommandsInterface(self)
218 self.__device = None
219 self.__connected = False
220 self.setConnected(False)
221
222 if not HAS_QTSERIALPORT:
223 self.replEdit.setHtml(self.tr(
224 "<h3>The QtSerialPort package is not available.<br/>"
225 "MicroPython support is deactivated.</h3>"))
226 self.setEnabled(False)
227 return
228
229 self.__vt100Re = re.compile(
230 r'(?P<count>\d*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])')
231
232 self.__populateDeviceTypeComboBox()
233
234 self.replEdit.installEventFilter(self)
235
236 self.replEdit.customContextMenuRequested.connect(
237 self.__showContextMenu)
238 self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
239 self.__ui.preferencesChanged.connect(
240 self.__interface.handlePreferencesChanged)
241
242 self.__handlePreferencesChanged()
243
244 charFormat = self.replEdit.currentCharFormat()
245 self.DefaultForeground = charFormat.foreground()
246 self.DefaultBackground = charFormat.background()
247
248 def __populateDeviceTypeComboBox(self):
249 """
250 Private method to populate the device type selector.
251 """
252 currentDevice = self.deviceTypeComboBox.currentText()
253
254 self.deviceTypeComboBox.clear()
255 self.deviceInfoLabel.clear()
256
257 self.deviceTypeComboBox.addItem("", "")
258 devices = MicroPythonDevices.getFoundDevices()
259 if devices:
260 self.deviceInfoLabel.setText(
261 self.tr("%n supported device(s) detected.", n=len(devices)))
262
263 index = 0
264 for device in sorted(devices):
265 index += 1
266 self.deviceTypeComboBox.addItem(
267 self.tr("{0} at {1}".format(device[1], device[2])))
268 self.deviceTypeComboBox.setItemData(
269 index, device[0], self.DeviceTypeRole)
270 self.deviceTypeComboBox.setItemData(
271 index, device[2], self.DevicePortRole)
272
273 else:
274 self.deviceInfoLabel.setText(
275 self.tr("No supported devices detected."))
276
277 index = self.deviceTypeComboBox.findText(currentDevice,
278 Qt.MatchExactly)
279 if index == -1:
280 # entry is no longer present
281 index = 0
282 if self.__connected:
283 # we are still connected, so disconnect
284 self.on_connectButton_clicked()
285
286 self.on_deviceTypeComboBox_activated(index)
287 self.deviceTypeComboBox.setCurrentIndex(index)
288
289 def __handlePreferencesChanged(self):
290 """
291 Private slot to handle a change in preferences.
292 """
293 self.__colorScheme = Preferences.getMicroPython("ColorScheme")
294
295 self.__font = Preferences.getEditorOtherFonts("MonospacedFont")
296 self.replEdit.setFontFamily(self.__font.family())
297 self.replEdit.setFontPointSize(self.__font.pointSize())
298
299 if Preferences.getMicroPython("ReplLineWrap"):
300 self.replEdit.setLineWrapMode(QTextEdit.WidgetWidth)
301 else:
302 self.replEdit.setLineWrapMode(QTextEdit.NoWrap)
303
304 def commandsInterface(self):
305 """
306 Public method to get a reference to the commands interface object.
307
308 @return reference to the commands interface object
309 @rtype MicroPythonCommandsInterface
310 """
311 return self.__interface
312
313 def isMicrobit(self):
314 """
315 Public method to check, if the connected/selected device is a
316 BBC micro:bit.
317
318 @return flag indicating a micro:bit device
319 rtype bool
320 """
321 if self.__device and "micro:bit" in self.__device.deviceName():
322 return True
323
324 return False
325
326 @pyqtSlot(int)
327 def on_deviceTypeComboBox_activated(self, index):
328 """
329 Private slot handling the selection of a device type.
330
331 @param index index of the selected device
332 @type int
333 """
334 deviceType = self.deviceTypeComboBox.itemData(
335 index, self.DeviceTypeRole)
336 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
337 deviceType, False))
338
339 self.__device = MicroPythonDevices.getDevice(deviceType, self)
340 self.__device.setButtons()
341
342 self.connectButton.setEnabled(bool(deviceType))
343
344 @pyqtSlot()
345 def on_checkButton_clicked(self):
346 """
347 Private slot to check for connected devices.
348 """
349 self.__populateDeviceTypeComboBox()
350
351 def setActionButtons(self, **kwargs):
352 """
353 Public method to set the enabled state of the various action buttons.
354
355 @keyparam kwargs keyword arguments containg the enabled states (keys
356 are 'run', 'repl', 'files', 'chart', 'open', 'save'
357 @type dict
358 """
359 if "open" in kwargs:
360 self.openButton.setEnabled(kwargs["open"])
361 if "save" in kwargs:
362 self.saveButton.setEnabled(kwargs["save"])
363 if "run" in kwargs:
364 self.runButton.setEnabled(kwargs["run"])
365 if "repl" in kwargs:
366 self.replButton.setEnabled(kwargs["repl"])
367 if "files" in kwargs:
368 self.filesButton.setEnabled(kwargs["files"])
369 if "chart" in kwargs:
370 self.chartButton.setEnabled(kwargs["chart"])
371
372 @pyqtSlot(QPoint)
373 def __showContextMenu(self, pos):
374 """
375 Private slot to show the REPL context menu.
376
377 @param pos position to show the menu at
378 @type QPoint
379 """
380 if Globals.isMacPlatform():
381 copyKeys = QKeySequence(Qt.CTRL + Qt.Key_C)
382 pasteKeys = QKeySequence(Qt.CTRL + Qt.Key_V)
383 else:
384 copyKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_C)
385 pasteKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_V)
386 menu = QMenu(self)
387 menu.addAction(self.tr("Clear"), self.__clear)
388 menu.addSeparator()
389 menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
390 menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
391 menu.addSeparator()
392 menu.exec_(self.replEdit.mapToGlobal(pos))
393
394 def setConnected(self, connected):
395 """
396 Public method to set the connection status LED.
397
398 @param connected connection state
399 @type bool
400 """
401 self.__connected = connected
402
403 self.deviceConnectedLed.setOn(connected)
404 if self.__fileManagerWidget:
405 self.__fileManagerWidget.deviceConnectedLed.setOn(connected)
406
407 self.deviceTypeComboBox.setEnabled(not connected)
408
409 if connected:
410 self.connectButton.setIcon(
411 UI.PixmapCache.getIcon("linkDisconnect"))
412 self.connectButton.setToolTip(self.tr(
413 "Press to disconnect the current device"))
414 else:
415 self.connectButton.setIcon(
416 UI.PixmapCache.getIcon("linkConnect"))
417 self.connectButton.setToolTip(self.tr(
418 "Press to connect the selected device"))
419
420 def isConnected(self):
421 """
422 Public method to get the connection state.
423
424 @return connection state
425 @rtype bool
426 """
427 return self.__connected
428
429 def __showNoDeviceMessage(self):
430 """
431 Private method to show a message dialog indicating a missing device.
432 """
433 E5MessageBox.critical(
434 self,
435 self.tr("No device attached"),
436 self.tr("""Please ensure the device is plugged into your"""
437 """ computer and selected.\n\nIt must have a version"""
438 """ of MicroPython (or CircuitPython) flashed onto"""
439 """ it before anything will work.\n\nFinally press"""
440 """ the device's reset button and wait a few seconds"""
441 """ before trying again."""))
442
443 @pyqtSlot(bool)
444 def on_replButton_clicked(self, checked):
445 """
446 Private slot to connect to enable or disable the REPL widget.
447
448 If the selected device is not connected yet, this will be done now.
449
450 @param checked state of the button
451 @type bool
452 """
453 if not self.__device:
454 self.__showNoDeviceMessage()
455 return
456
457 if checked:
458 ok, reason = self.__device.canStartRepl()
459 if not ok:
460 E5MessageBox.warning(
461 self,
462 self.tr("Start REPL"),
463 self.tr("""<p>The REPL cannot be started.</p><p>Reason:"""
464 """ {0}</p>""").format(reason))
465 return
466
467 self.replEdit.clear()
468 self.__interface.dataReceived.connect(self.__processData)
469
470 if not self.__interface.isConnected():
471 self.__connectToDevice()
472 if self.__device.forceInterrupt():
473 # send a Ctrl-B (exit raw mode)
474 self.__interface.write(b'\x02')
475 # send Ctrl-C (keyboard interrupt)
476 self.__interface.write(b'\x03')
477
478 self.__device.setRepl(True)
479 self.replEdit.setFocus(Qt.OtherFocusReason)
480 else:
481 self.__interface.dataReceived.disconnect(self.__processData)
482 if (not self.chartButton.isChecked() and
483 not self.filesButton.isChecked()):
484 self.__disconnectFromDevice()
485 self.__device.setRepl(False)
486 self.replButton.setChecked(checked)
487
488 @pyqtSlot()
489 def on_connectButton_clicked(self):
490 """
491 Private slot to connect to the selected device or disconnect from the
492 currently connected device.
493 """
494 if self.__connected:
495 self.__disconnectFromDevice()
496
497 if self.replButton.isChecked():
498 self.on_replButton_clicked(False)
499 if self.filesButton.isChecked():
500 self.on_filesButton_clicked(False)
501 if self.chartButton.isChecked():
502 self.on_chartButton_clicked(False)
503 else:
504 self.__connectToDevice()
505
506 @pyqtSlot()
507 def __clear(self):
508 """
509 Private slot to clear the REPL pane.
510 """
511 self.replEdit.clear()
512 self.__interface.isConnected() and self.__interface.write(b"\r")
513
514 @pyqtSlot()
515 def __paste(self):
516 """
517 Private slot to perform a paste operation.
518 """
519 clipboard = QApplication.clipboard()
520 if clipboard:
521 pasteText = clipboard.text()
522 if pasteText:
523 pasteText = pasteText.replace('\n\r', '\r')
524 pasteText = pasteText.replace('\n', '\r')
525 self.__interface.isConnected() and self.__interface.write(
526 pasteText.encode("utf-8"))
527
528 def eventFilter(self, obj, evt):
529 """
530 Public method to process events for the REPL pane.
531
532 @param obj reference to the object the event was meant for
533 @type QObject
534 @param evt reference to the event object
535 @type QEvent
536 @return flag to indicate that the event was handled
537 @rtype bool
538 """
539 if obj is self.replEdit and evt.type() == QEvent.KeyPress:
540 # handle the key press event on behalve of the REPL pane
541 key = evt.key()
542 msg = bytes(evt.text(), 'utf8')
543 if key == Qt.Key_Backspace:
544 msg = b'\b'
545 elif key == Qt.Key_Delete:
546 msg = b'\x1B[\x33\x7E'
547 elif key == Qt.Key_Up:
548 msg = b'\x1B[A'
549 elif key == Qt.Key_Down:
550 msg = b'\x1B[B'
551 elif key == Qt.Key_Right:
552 msg = b'\x1B[C'
553 elif key == Qt.Key_Left:
554 msg = b'\x1B[D'
555 elif key == Qt.Key_Home:
556 msg = b'\x1B[H'
557 elif key == Qt.Key_End:
558 msg = b'\x1B[F'
559 elif ((Globals.isMacPlatform() and
560 evt.modifiers() == Qt.MetaModifier) or
561 (not Globals.isMacPlatform() and
562 evt.modifiers() == Qt.ControlModifier)):
563 if Qt.Key_A <= key <= Qt.Key_Z:
564 # devices treat an input of \x01 as Ctrl+A, etc.
565 msg = bytes([1 + key - Qt.Key_A])
566 elif (evt.modifiers() == Qt.ControlModifier | Qt.ShiftModifier or
567 (Globals.isMacPlatform() and
568 evt.modifiers() == Qt.ControlModifier)):
569 if key == Qt.Key_C:
570 self.replEdit.copy()
571 msg = b''
572 elif key == Qt.Key_V:
573 self.__paste()
574 msg = b''
575 elif key in (Qt.Key_Return, Qt.Key_Enter):
576 tc = self.replEdit.textCursor()
577 tc.movePosition(QTextCursor.EndOfLine)
578 self.replEdit.setTextCursor(tc)
579 self.__interface.isConnected() and self.__interface.write(msg)
580 return True
581
582 else:
583 # standard event processing
584 return super(MicroPythonWidget, self).eventFilter(obj, evt)
585
586 def __processData(self, data):
587 """
588 Private slot to process bytes received from the device.
589
590 @param data bytes received from the device
591 @type bytes
592 """
593 tc = self.replEdit.textCursor()
594 # the text cursor must be on the last line
595 while tc.movePosition(QTextCursor.Down):
596 pass
597
598 # set the font
599 charFormat = tc.charFormat()
600 charFormat.setFontFamily(self.__font.family())
601 charFormat.setFontPointSize(self.__font.pointSize())
602 tc.setCharFormat(charFormat)
603
604 index = 0
605 while index < len(data):
606 if data[index] == 8: # \b
607 tc.movePosition(QTextCursor.Left)
608 self.replEdit.setTextCursor(tc)
609 elif data[index] == 13: # \r
610 pass
611 elif (len(data) > index + 1 and
612 data[index] == 27 and
613 data[index + 1] == 91):
614 # VT100 cursor command detected: <Esc>[
615 index += 2 # move index to after the [
616 match = self.__vt100Re.search(data[index:].decode("utf-8"))
617 if match:
618 # move to last position in control sequence
619 # ++ will be done at end of loop
620 index += match.end() - 1
621
622 action = match.group("action")
623 if action in "ABCD":
624 if match.group("count") == "":
625 count = 1
626 else:
627 count = int(match.group("count"))
628
629 if action == "A": # up
630 tc.movePosition(QTextCursor.Up, n=count)
631 self.replEdit.setTextCursor(tc)
632 elif action == "B": # down
633 tc.movePosition(QTextCursor.Down, n=count)
634 self.replEdit.setTextCursor(tc)
635 elif action == "C": # right
636 tc.movePosition(QTextCursor.Right, n=count)
637 self.replEdit.setTextCursor(tc)
638 elif action == "D": # left
639 tc.movePosition(QTextCursor.Left, n=count)
640 self.replEdit.setTextCursor(tc)
641 elif action == "K": # delete things
642 if match.group("count") in ("", "0"):
643 # delete to end of line
644 tc.movePosition(QTextCursor.EndOfLine,
645 mode=QTextCursor.KeepAnchor)
646 tc.removeSelectedText()
647 self.replEdit.setTextCursor(tc)
648 elif match.group("count") == "1":
649 # delete to beinning of line
650 tc.movePosition(QTextCursor.StartOfLine,
651 mode=QTextCursor.KeepAnchor)
652 tc.removeSelectedText()
653 self.replEdit.setTextCursor(tc)
654 elif match.group("count") == "2":
655 # delete whole line
656 tc.movePosition(QTextCursor.EndOfLine)
657 tc.movePosition(QTextCursor.StartOfLine,
658 mode=QTextCursor.KeepAnchor)
659 tc.removeSelectedText()
660 self.replEdit.setTextCursor(tc)
661 elif action == "m":
662 self.__setCharFormat(match.group(0)[:-1].split(";"),
663 tc)
664 else:
665 tc.deleteChar()
666 self.replEdit.setTextCursor(tc)
667 self.replEdit.insertPlainText(chr(data[index]))
668
669 index += 1
670
671 self.replEdit.ensureCursorVisible()
672
673 def __setCharFormat(self, formatCodes, textCursor):
674 """
675 Private method setting the current text format of the REPL pane based
676 on the passed ANSI codes.
677
678 Following codes are used:
679 <ul>
680 <li>0: Reset</li>
681 <li>1: Bold font (weight 75)</li>
682 <li>2: Light font (weight 25)</li>
683 <li>3: Italic font</li>
684 <li>4: Underlined font</li>
685 <li>9: Strikeout font</li>
686 <li>21: Bold off (weight 50)</li>
687 <li>22: Light off (weight 50)</li>
688 <li>23: Italic off</li>
689 <li>24: Underline off</li>
690 <li>29: Strikeout off</li>
691 <li>30: foreground Black</li>
692 <li>31: foreground Dark Red</li>
693 <li>32: foreground Dark Green</li>
694 <li>33: foreground Dark Yellow</li>
695 <li>34: foreground Dark Blue</li>
696 <li>35: foreground Dark Magenta</li>
697 <li>36: foreground Dark Cyan</li>
698 <li>37: foreground Light Gray</li>
699 <li>39: reset foreground to default</li>
700 <li>40: background Black</li>
701 <li>41: background Dark Red</li>
702 <li>42: background Dark Green</li>
703 <li>43: background Dark Yellow</li>
704 <li>44: background Dark Blue</li>
705 <li>45: background Dark Magenta</li>
706 <li>46: background Dark Cyan</li>
707 <li>47: background Light Gray</li>
708 <li>49: reset background to default</li>
709 <li>53: Overlined font</li>
710 <li>55: Overline off</li>
711 <li>90: bright foreground Dark Gray</li>
712 <li>91: bright foreground Red</li>
713 <li>92: bright foreground Green</li>
714 <li>93: bright foreground Yellow</li>
715 <li>94: bright foreground Blue</li>
716 <li>95: bright foreground Magenta</li>
717 <li>96: bright foreground Cyan</li>
718 <li>97: bright foreground White</li>
719 <li>100: bright background Dark Gray</li>
720 <li>101: bright background Red</li>
721 <li>102: bright background Green</li>
722 <li>103: bright background Yellow</li>
723 <li>104: bright background Blue</li>
724 <li>105: bright background Magenta</li>
725 <li>106: bright background Cyan</li>
726 <li>107: bright background White</li>
727 </ul>
728
729 @param formatCodes list of format codes
730 @type list of str
731 @param textCursor reference to the text cursor
732 @type QTextCursor
733 """
734 if not formatCodes:
735 # empty format codes list is treated as a reset
736 formatCodes = ["0"]
737
738 charFormat = textCursor.charFormat()
739 for formatCode in formatCodes:
740 try:
741 formatCode = int(formatCode)
742 except ValueError:
743 # ignore non digit values
744 continue
745
746 if formatCode == 0:
747 charFormat.setFontWeight(50)
748 charFormat.setFontItalic(False)
749 charFormat.setFontUnderline(False)
750 charFormat.setFontStrikeOut(False)
751 charFormat.setFontOverline(False)
752 charFormat.setForeground(self.DefaultForeground)
753 charFormat.setBackground(self.DefaultBackground)
754 elif formatCode == 1:
755 charFormat.setFontWeight(75)
756 elif formatCode == 2:
757 charFormat.setFontWeight(25)
758 elif formatCode == 3:
759 charFormat.setFontItalic(True)
760 elif formatCode == 4:
761 charFormat.setFontUnderline(True)
762 elif formatCode == 9:
763 charFormat.setFontStrikeOut(True)
764 elif formatCode in (21, 22):
765 charFormat.setFontWeight(50)
766 elif formatCode == 23:
767 charFormat.setFontItalic(False)
768 elif formatCode == 24:
769 charFormat.setFontUnderline(False)
770 elif formatCode == 29:
771 charFormat.setFontStrikeOut(False)
772 elif formatCode == 53:
773 charFormat.setFontOverline(True)
774 elif formatCode == 55:
775 charFormat.setFontOverline(False)
776 elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37):
777 charFormat.setForeground(
778 AnsiColorSchemes[self.__colorScheme][formatCode - 30])
779 elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47):
780 charFormat.setBackground(
781 AnsiColorSchemes[self.__colorScheme][formatCode - 40])
782 elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97):
783 charFormat.setForeground(
784 AnsiColorSchemes[self.__colorScheme][formatCode - 80])
785 elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107):
786 charFormat.setBackground(
787 AnsiColorSchemes[self.__colorScheme][formatCode - 90])
788 elif formatCode == 39:
789 charFormat.setForeground(self.DefaultForeground)
790 elif formatCode == 49:
791 charFormat.setBackground(self.DefaultBackground)
792
793 textCursor.setCharFormat(charFormat)
794
795 def __doZoom(self, value):
796 """
797 Private slot to zoom the REPL pane.
798
799 @param value zoom value
800 @type int
801 """
802 if value < self.__currentZoom:
803 self.replEdit.zoomOut(self.__currentZoom - value)
804 elif value > self.__currentZoom:
805 self.replEdit.zoomIn(value - self.__currentZoom)
806 self.__currentZoom = value
807
808 def getCurrentPort(self):
809 """
810 Public method to determine the port path of the selected device.
811
812 @return path of the port of the selected device
813 @rtype str
814 """
815 portName = self.deviceTypeComboBox.itemData(
816 self.deviceTypeComboBox.currentIndex(),
817 self.DevicePortRole)
818
819 if Globals.isWindowsPlatform():
820 # return it unchanged
821 return portName
822 else:
823 # return with device path prepended
824 return "/dev/{0}".format(portName)
825
826 def getDeviceWorkspace(self):
827 """
828 Public method to get the workspace directory of the device.
829
830 @return workspace directory of the device
831 @rtype str
832 """
833 if self.__device:
834 return self.__device.getWorkspace()
835 else:
836 return ""
837
838 def __connectToDevice(self):
839 """
840 Private method to connect to the selected device.
841 """
842 port = self.getCurrentPort()
843 if self.__interface.connectToDevice(port):
844 self.setConnected(True)
845 else:
846 E5MessageBox.warning(
847 self,
848 self.tr("Serial Device Connect"),
849 self.tr("""<p>Cannot connect to device at serial port"""
850 """ <b>{0}</b>.</p>""").format(port))
851
852 def __disconnectFromDevice(self):
853 """
854 Private method to disconnect from the device.
855 """
856 self.__interface.disconnectFromDevice()
857 self.setConnected(False)
858
859 @pyqtSlot()
860 def on_runButton_clicked(self):
861 """
862 Private slot to execute the script of the active editor on the
863 selected device.
864
865 If the REPL is not active yet, it will be activated, which might cause
866 an unconnected device to be connected.
867 """
868 if not self.__device:
869 self.__showNoDeviceMessage()
870 return
871
872 aw = e5App().getObject("ViewManager").activeWindow()
873 if aw is None:
874 E5MessageBox.critical(
875 self,
876 self.tr("Run Script"),
877 self.tr("""There is no editor open. Abort..."""))
878 return
879
880 script = aw.text()
881 if not script:
882 E5MessageBox.critical(
883 self,
884 self.tr("Run Script"),
885 self.tr("""The current editor does not contain a script."""
886 """ Abort..."""))
887 return
888
889 ok, reason = self.__device.canRunScript()
890 if not ok:
891 E5MessageBox.warning(
892 self,
893 self.tr("Run Script"),
894 self.tr("""<p>Cannot run script.</p><p>Reason:"""
895 """ {0}</p>""").format(reason))
896 return
897
898 if not self.replButton.isChecked():
899 # activate on the REPL
900 self.on_replButton_clicked(True)
901 if self.replButton.isChecked():
902 self.__device.runScript(script)
903
904 @pyqtSlot()
905 def on_openButton_clicked(self):
906 """
907 Private slot to open a file of the connected device.
908 """
909 if not self.__device:
910 self.__showNoDeviceMessage()
911 return
912
913 workspace = self.__device.getWorkspace()
914 fileName = E5FileDialog.getOpenFileName(
915 self,
916 self.tr("Open Python File"),
917 workspace,
918 self.tr("Python3 Files (*.py);;All Files (*)"))
919 if fileName:
920 e5App().getObject("ViewManager").openSourceFile(fileName)
921
922 @pyqtSlot()
923 def on_saveButton_clicked(self):
924 """
925 Private slot to save the current editor to the connected device.
926 """
927 if not self.__device:
928 self.__showNoDeviceMessage()
929 return
930
931 workspace = self.__device.getWorkspace()
932 aw = e5App().getObject("ViewManager").activeWindow()
933 if aw:
934 aw.saveFileAs(workspace)
935
936 @pyqtSlot(bool)
937 def on_chartButton_clicked(self, checked):
938 """
939 Private slot to open a chart view to plot data received from the
940 connected device.
941
942 If the selected device is not connected yet, this will be done now.
943
944 @param checked state of the button
945 @type bool
946 """
947 if not HAS_QTCHART:
948 # QtChart not available => fail silently
949 return
950
951 if not self.__device:
952 self.__showNoDeviceMessage()
953 return
954
955 if checked:
956 ok, reason = self.__device.canStartPlotter()
957 if not ok:
958 E5MessageBox.warning(
959 self,
960 self.tr("Start Chart"),
961 self.tr("""<p>The Chart cannot be started.</p><p>Reason:"""
962 """ {0}</p>""").format(reason))
963 return
964
965 self.__chartWidget = MicroPythonGraphWidget(self)
966 self.__interface.dataReceived.connect(
967 self.__chartWidget.processData)
968 self.__chartWidget.dataFlood.connect(
969 self.handleDataFlood)
970
971 self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget,
972 UI.PixmapCache.getIcon("chart"),
973 self.tr("μPy Chart"))
974 self.__ui.showSideWidget(self.__chartWidget)
975
976 if not self.__interface.isConnected():
977 self.__connectToDevice()
978 if self.__device.forceInterrupt():
979 # send a Ctrl-B (exit raw mode)
980 self.__interface.write(b'\x02')
981 # send Ctrl-C (keyboard interrupt)
982 self.__interface.write(b'\x03')
983
984 self.__device.setPlotter(True)
985 else:
986 if self.__chartWidget.isDirty():
987 res = E5MessageBox.okToClearData(
988 self,
989 self.tr("Unsaved Chart Data"),
990 self.tr("""The chart contains unsaved data."""),
991 self.__chartWidget.saveData)
992 if not res:
993 # abort
994 return
995
996 self.__interface.dataReceived.disconnect(
997 self.__chartWidget.processData)
998 self.__chartWidget.dataFlood.disconnect(
999 self.handleDataFlood)
1000
1001 if (not self.replButton.isChecked() and
1002 not self.filesButton.isChecked()):
1003 self.__disconnectFromDevice()
1004
1005 self.__device.setPlotter(False)
1006 self.__ui.removeSideWidget(self.__chartWidget)
1007
1008 self.__chartWidget.deleteLater()
1009 self.__chartWidget = None
1010
1011 self.chartButton.setChecked(checked)
1012
1013 @pyqtSlot()
1014 def handleDataFlood(self):
1015 """
1016 Public slot handling a data flood from the device.
1017 """
1018 self.on_connectButton_clicked()
1019 self.__device.handleDataFlood()
1020
1021 @pyqtSlot(bool)
1022 def on_filesButton_clicked(self, checked):
1023 """
1024 Private slot to open a file manager window to the connected device.
1025
1026 If the selected device is not connected yet, this will be done now.
1027
1028 @param checked state of the button
1029 @type bool
1030 """
1031 if not self.__device:
1032 self.__showNoDeviceMessage()
1033 return
1034
1035 if checked:
1036 ok, reason = self.__device.canStartFileManager()
1037 if not ok:
1038 E5MessageBox.warning(
1039 self,
1040 self.tr("Start File Manager"),
1041 self.tr("""<p>The File Manager cannot be started.</p>"""
1042 """<p>Reason: {0}</p>""").format(reason))
1043 return
1044
1045 if not self.__interface.isConnected():
1046 self.__connectToDevice()
1047 self.__fileManagerWidget = MicroPythonFileManagerWidget(
1048 self.__interface, self.__device.supportsLocalFileAccess(),
1049 self)
1050
1051 self.__ui.addSideWidget(self.__ui.BottomSide,
1052 self.__fileManagerWidget,
1053 UI.PixmapCache.getIcon("filemanager"),
1054 self.tr("μPy Files"))
1055 self.__ui.showSideWidget(self.__fileManagerWidget)
1056
1057 self.__device.setFileManager(True)
1058
1059 self.__fileManagerWidget.start()
1060 else:
1061 self.__fileManagerWidget.stop()
1062
1063 if (not self.replButton.isChecked() and
1064 not self.chartButton.isChecked()):
1065 self.__disconnectFromDevice()
1066
1067 self.__device.setFileManager(False)
1068 self.__ui.removeSideWidget(self.__fileManagerWidget)
1069
1070 self.__fileManagerWidget.deleteLater()
1071 self.__fileManagerWidget = None
1072
1073 self.filesButton.setChecked(checked)
1074
1075 ##################################################################
1076 ## Super Menu related methods below
1077 ##################################################################
1078
1079 def __aboutToShowSuperMenu(self):
1080 """
1081 Private slot to populate the Super Menu before showing it.
1082 """
1083 self.__superMenu.clear()
1084 if self.__device:
1085 hasTime = self.__device.hasTimeCommands()
1086 else:
1087 hasTime = False
1088
1089 # TODO: add menu entry to cross-compile a .py file (using mpy-cross)
1090 act = self.__superMenu.addAction(
1091 self.tr("Show Version"), self.__showDeviceVersion)
1092 act.setEnabled(self.__connected)
1093 act = self.__superMenu.addAction(
1094 self.tr("Show Implementation"), self.__showImplementation)
1095 act.setEnabled(self.__connected)
1096 self.__superMenu.addSeparator()
1097 if hasTime:
1098 act = self.__superMenu.addAction(
1099 self.tr("Synchronize Time"), self.__synchronizeTime)
1100 act.setEnabled(self.__connected)
1101 act = self.__superMenu.addAction(
1102 self.tr("Show Device Time"), self.__showDeviceTime)
1103 act.setEnabled(self.__connected)
1104 self.__superMenu.addAction(
1105 self.tr("Show Local Time"), self.__showLocalTime)
1106 self.__superMenu.addSeparator()
1107 if self.__device:
1108 self.__device.addDeviceMenuEntries(self.__superMenu)
1109
1110 @pyqtSlot()
1111 def __showDeviceVersion(self):
1112 """
1113 Private slot to show some version info about MicroPython of the device.
1114 """
1115 try:
1116 versionInfo = self.__interface.version()
1117 if versionInfo:
1118 msg = self.tr(
1119 "<h3>Device Version Information</h3>"
1120 )
1121 msg += "<table>"
1122 for key, value in versionInfo.items():
1123 msg += "<tr><td><b>{0}</b></td><td>{1}</td></tr>".format(
1124 key.capitalize(), value)
1125 msg += "</table>"
1126 else:
1127 msg = self.tr("No version information available.")
1128
1129 E5MessageBox.information(
1130 self,
1131 self.tr("Device Version Information"),
1132 msg)
1133 except Exception as exc:
1134 self.__showError("version()", str(exc))
1135
1136 @pyqtSlot()
1137 def __showImplementation(self):
1138 """
1139 Private slot to show some implementation related information.
1140 """
1141 try:
1142 impInfo = self.__interface.getImplementation()
1143 if impInfo["name"] == "micropython":
1144 name = "MicroPython"
1145 elif impInfo["name"] == "circuitpython":
1146 name = "CircuitPython"
1147 elif impInfo["name"] == "unknown":
1148 name = self.tr("unknown")
1149 else:
1150 name = impInfo["name"]
1151 if impInfo["version"] == "unknown":
1152 version = self.tr("unknown")
1153 else:
1154 version = impInfo["version"]
1155
1156 E5MessageBox.information(
1157 self,
1158 self.tr("Device Implementation Information"),
1159 self.tr(
1160 "<h3>Device Implementation Information</h3>"
1161 "<p>This device contains <b>{0} {1}</b>.</p>"
1162 ).format(name, version)
1163 )
1164 except Exception as exc:
1165 self.__showError("getImplementation()", str(exc))
1166
1167 # TODO: show device time after sync
1168 @pyqtSlot()
1169 def __synchronizeTime(self):
1170 """
1171 Private slot to set the time of the connected device to the local
1172 computer's time.
1173 """
1174 try:
1175 self.__interface.syncTime()
1176
1177 E5MessageBox.information(
1178 self,
1179 self.tr("Synchronize Time"),
1180 self.tr("The time of the connected device was synchronized"
1181 " with the local time."))
1182 except Exception as exc:
1183 self.__showError("syncTime()", str(exc))
1184
1185 @pyqtSlot()
1186 def __showDeviceTime(self):
1187 """
1188 Private slot to show the date and time of the connected device.
1189 """
1190 try:
1191 dateTimeString = self.__interface.getTime()
1192 try:
1193 date, time = dateTimeString.strip().split(None, 1)
1194 msg = self.tr(
1195 "<h3>Device Date and Time</h3>"
1196 "<table>"
1197 "<tr><td><b>Date</b></td><td>{0}</td></tr>"
1198 "<tr><td><b>Time</b></td><td>{1}</td></tr>"
1199 "</table>"
1200 ).format(date, time)
1201 except ValueError:
1202 msg = self.tr(
1203 "<h3>Device Date and Time</h3>"
1204 "<p>{0}</p>"
1205 ).format(dateTimeString.strip())
1206
1207 E5MessageBox.information(
1208 self,
1209 self.tr("Device Date and Time"),
1210 msg)
1211 except Exception as exc:
1212 self.__showError("getTime()", str(exc))
1213
1214 @pyqtSlot()
1215 def __showLocalTime(self):
1216 """
1217 Private slot to show the local date and time.
1218 """
1219 localdatetime = time.localtime()
1220 loacldate = time.strftime('%Y-%m-%d', localdatetime)
1221 localtime = time.strftime('%H:%M:%S', localdatetime)
1222 E5MessageBox.information(
1223 self,
1224 self.tr("Local Date and Time"),
1225 self.tr("<h3>Local Date and Time</h3>"
1226 "<table>"
1227 "<tr><td><b>Date</b></td><td>{0}</td></tr>"
1228 "<tr><td><b>Time</b></td><td>{1}</td></tr>"
1229 "</table>"
1230 ).format(loacldate, localtime)
1231 )
1232
1233 def __showError(self, method, error):
1234 """
1235 Private method to show some error message.
1236
1237 @param method name of the method the error occured in
1238 @type str
1239 @param error error message
1240 @type str
1241 """
1242 E5MessageBox.warning(
1243 self,
1244 self.tr("Error handling device"),
1245 self.tr("<p>There was an error communicating with the connected"
1246 " device.</p><p>Method: {0}</p><p>Message: {1}</p>")
1247 .format(method, error))

eric ide

mercurial