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

eric ide

mercurial