src/eric7/MicroPython/MicroPythonWidget.py

branch
mpy_network
changeset 10011
26a7d607b8f6
parent 10010
8a68a7a7ab88
child 10012
d649d500a9a1
equal deleted inserted replaced
10010:8a68a7a7ab88 10011:26a7d607b8f6
5 5
6 """ 6 """
7 Module implementing the MicroPython REPL widget. 7 Module implementing the MicroPython REPL widget.
8 """ 8 """
9 9
10 # TODO: refactor the code such to have the MicroPythonWidget as the top level
11 # container and a MicroPythonReplWidget containing the REPL related stuff
12 # (incl. device status line and zoom widget ?)
13
14 import contextlib 10 import contextlib
15 import functools 11 import functools
16 import os 12 import os
17 import re
18 import time 13 import time
19 14
20 from PyQt6.QtCore import QEvent, QPoint, Qt, pyqtSignal, pyqtSlot 15 from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
21 from PyQt6.QtGui import QBrush, QClipboard, QColor, QKeySequence, QTextCursor
22 from PyQt6.QtWidgets import ( 16 from PyQt6.QtWidgets import (
23 QApplication,
24 QDialog, 17 QDialog,
25 QHBoxLayout,
26 QInputDialog, 18 QInputDialog,
27 QLabel,
28 QLineEdit, 19 QLineEdit,
29 QMenu, 20 QMenu,
30 QSizePolicy,
31 QTextEdit,
32 QToolButton, 21 QToolButton,
33 QWidget, 22 QWidget,
34 ) 23 )
35 24
36 from eric7 import Preferences 25 from eric7 import Preferences
39 from eric7.EricWidgets import EricFileDialog, EricMessageBox 28 from eric7.EricWidgets import EricFileDialog, EricMessageBox
40 from eric7.EricWidgets.EricApplication import ericApp 29 from eric7.EricWidgets.EricApplication import ericApp
41 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog 30 from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog
42 from eric7.EricWidgets.EricPlainTextDialog import EricPlainTextDialog 31 from eric7.EricWidgets.EricPlainTextDialog import EricPlainTextDialog
43 from eric7.EricWidgets.EricProcessDialog import EricProcessDialog 32 from eric7.EricWidgets.EricProcessDialog import EricProcessDialog
44 from eric7.EricWidgets.EricZoomWidget import EricZoomWidget
45 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities 33 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities
46 from eric7.UI.Info import BugAddress 34 from eric7.UI.Info import BugAddress
47 35
48 from . import ConvertToUF2Dialog, Devices, UF2FlashDialog 36 from . import ConvertToUF2Dialog, Devices, UF2FlashDialog
49 from .BluetoothDialogs.BluetoothController import BluetoothController 37 from .BluetoothDialogs.BluetoothController import BluetoothController
66 54
67 HAS_QTSERIALPORT = True 55 HAS_QTSERIALPORT = True
68 except ImportError: 56 except ImportError:
69 HAS_QTSERIALPORT = False 57 HAS_QTSERIALPORT = False
70 58
71 # ANSI Colors (see https://en.wikipedia.org/wiki/ANSI_escape_code)
72 AnsiColorSchemes = {
73 "Windows 7": {
74 0: QBrush(QColor(0, 0, 0)),
75 1: QBrush(QColor(128, 0, 0)),
76 2: QBrush(QColor(0, 128, 0)),
77 3: QBrush(QColor(128, 128, 0)),
78 4: QBrush(QColor(0, 0, 128)),
79 5: QBrush(QColor(128, 0, 128)),
80 6: QBrush(QColor(0, 128, 128)),
81 7: QBrush(QColor(192, 192, 192)),
82 10: QBrush(QColor(128, 128, 128)),
83 11: QBrush(QColor(255, 0, 0)),
84 12: QBrush(QColor(0, 255, 0)),
85 13: QBrush(QColor(255, 255, 0)),
86 14: QBrush(QColor(0, 0, 255)),
87 15: QBrush(QColor(255, 0, 255)),
88 16: QBrush(QColor(0, 255, 255)),
89 17: QBrush(QColor(255, 255, 255)),
90 },
91 "Windows 10": {
92 0: QBrush(QColor(12, 12, 12)),
93 1: QBrush(QColor(197, 15, 31)),
94 2: QBrush(QColor(19, 161, 14)),
95 3: QBrush(QColor(193, 156, 0)),
96 4: QBrush(QColor(0, 55, 218)),
97 5: QBrush(QColor(136, 23, 152)),
98 6: QBrush(QColor(58, 150, 221)),
99 7: QBrush(QColor(204, 204, 204)),
100 10: QBrush(QColor(118, 118, 118)),
101 11: QBrush(QColor(231, 72, 86)),
102 12: QBrush(QColor(22, 198, 12)),
103 13: QBrush(QColor(249, 241, 165)),
104 14: QBrush(QColor(59, 12, 255)),
105 15: QBrush(QColor(180, 0, 158)),
106 16: QBrush(QColor(97, 214, 214)),
107 17: QBrush(QColor(242, 242, 242)),
108 },
109 "PuTTY": {
110 0: QBrush(QColor(0, 0, 0)),
111 1: QBrush(QColor(187, 0, 0)),
112 2: QBrush(QColor(0, 187, 0)),
113 3: QBrush(QColor(187, 187, 0)),
114 4: QBrush(QColor(0, 0, 187)),
115 5: QBrush(QColor(187, 0, 187)),
116 6: QBrush(QColor(0, 187, 187)),
117 7: QBrush(QColor(187, 187, 187)),
118 10: QBrush(QColor(85, 85, 85)),
119 11: QBrush(QColor(255, 85, 85)),
120 12: QBrush(QColor(85, 255, 85)),
121 13: QBrush(QColor(255, 255, 85)),
122 14: QBrush(QColor(85, 85, 255)),
123 15: QBrush(QColor(255, 85, 255)),
124 16: QBrush(QColor(85, 255, 255)),
125 17: QBrush(QColor(255, 255, 255)),
126 },
127 "xterm": {
128 0: QBrush(QColor(0, 0, 0)),
129 1: QBrush(QColor(205, 0, 0)),
130 2: QBrush(QColor(0, 205, 0)),
131 3: QBrush(QColor(205, 205, 0)),
132 4: QBrush(QColor(0, 0, 238)),
133 5: QBrush(QColor(205, 0, 205)),
134 6: QBrush(QColor(0, 205, 205)),
135 7: QBrush(QColor(229, 229, 229)),
136 10: QBrush(QColor(127, 127, 127)),
137 11: QBrush(QColor(255, 0, 0)),
138 12: QBrush(QColor(0, 255, 0)),
139 13: QBrush(QColor(255, 255, 0)),
140 14: QBrush(QColor(0, 0, 255)),
141 15: QBrush(QColor(255, 0, 255)),
142 16: QBrush(QColor(0, 255, 255)),
143 17: QBrush(QColor(255, 255, 255)),
144 },
145 "Ubuntu": {
146 0: QBrush(QColor(1, 1, 1)),
147 1: QBrush(QColor(222, 56, 43)),
148 2: QBrush(QColor(57, 181, 74)),
149 3: QBrush(QColor(255, 199, 6)),
150 4: QBrush(QColor(0, 11, 184)),
151 5: QBrush(QColor(118, 38, 113)),
152 6: QBrush(QColor(44, 181, 233)),
153 7: QBrush(QColor(204, 204, 204)),
154 10: QBrush(QColor(128, 128, 128)),
155 11: QBrush(QColor(255, 0, 0)),
156 12: QBrush(QColor(0, 255, 0)),
157 13: QBrush(QColor(255, 255, 0)),
158 14: QBrush(QColor(0, 0, 255)),
159 15: QBrush(QColor(255, 0, 255)),
160 16: QBrush(QColor(0, 255, 255)),
161 17: QBrush(QColor(255, 255, 255)),
162 },
163 "Ubuntu (dark)": {
164 0: QBrush(QColor(96, 96, 96)),
165 1: QBrush(QColor(235, 58, 45)),
166 2: QBrush(QColor(57, 181, 74)),
167 3: QBrush(QColor(255, 199, 29)),
168 4: QBrush(QColor(25, 56, 230)),
169 5: QBrush(QColor(200, 64, 193)),
170 6: QBrush(QColor(48, 200, 255)),
171 7: QBrush(QColor(204, 204, 204)),
172 10: QBrush(QColor(128, 128, 128)),
173 11: QBrush(QColor(255, 0, 0)),
174 12: QBrush(QColor(0, 255, 0)),
175 13: QBrush(QColor(255, 255, 0)),
176 14: QBrush(QColor(0, 0, 255)),
177 15: QBrush(QColor(255, 0, 255)),
178 16: QBrush(QColor(0, 255, 255)),
179 17: QBrush(QColor(255, 255, 255)),
180 },
181 "Breeze (dark)": {
182 0: QBrush(QColor(35, 38, 39)),
183 1: QBrush(QColor(237, 21, 21)),
184 2: QBrush(QColor(17, 209, 22)),
185 3: QBrush(QColor(246, 116, 0)),
186 4: QBrush(QColor(29, 153, 243)),
187 5: QBrush(QColor(155, 89, 182)),
188 6: QBrush(QColor(26, 188, 156)),
189 7: QBrush(QColor(252, 252, 252)),
190 10: QBrush(QColor(127, 140, 141)),
191 11: QBrush(QColor(192, 57, 43)),
192 12: QBrush(QColor(28, 220, 154)),
193 13: QBrush(QColor(253, 188, 75)),
194 14: QBrush(QColor(61, 174, 233)),
195 15: QBrush(QColor(142, 68, 173)),
196 16: QBrush(QColor(22, 160, 133)),
197 17: QBrush(QColor(255, 255, 255)),
198 },
199 }
200
201 59
202 class MicroPythonWidget(QWidget, Ui_MicroPythonWidget): 60 class MicroPythonWidget(QWidget, Ui_MicroPythonWidget):
203 """ 61 """
204 Class implementing the MicroPython REPL widget. 62 Class implementing the MicroPython REPL widget.
205 63
206 @signal dataReceived(data) emitted to send data received via the serial 64 @signal dataReceived(data) emitted to send data received via the serial
207 connection for further processing 65 connection for further processing
208 """ 66 """
209
210 ZoomMin = -10
211 ZoomMax = 20
212 67
213 DeviceTypeRole = Qt.ItemDataRole.UserRole 68 DeviceTypeRole = Qt.ItemDataRole.UserRole
214 DeviceBoardRole = Qt.ItemDataRole.UserRole + 1 69 DeviceBoardRole = Qt.ItemDataRole.UserRole + 1
215 DevicePortRole = Qt.ItemDataRole.UserRole + 2 70 DevicePortRole = Qt.ItemDataRole.UserRole + 2
216 DeviceVidRole = Qt.ItemDataRole.UserRole + 3 71 DeviceVidRole = Qt.ItemDataRole.UserRole + 3
267 self.replButton.setIcon(EricPixmapCache.getIcon("terminal")) 122 self.replButton.setIcon(EricPixmapCache.getIcon("terminal"))
268 self.filesButton.setIcon(EricPixmapCache.getIcon("filemanager")) 123 self.filesButton.setIcon(EricPixmapCache.getIcon("filemanager"))
269 self.chartButton.setIcon(EricPixmapCache.getIcon("chart")) 124 self.chartButton.setIcon(EricPixmapCache.getIcon("chart"))
270 self.connectButton.setIcon(EricPixmapCache.getIcon("linkConnect")) 125 self.connectButton.setIcon(EricPixmapCache.getIcon("linkConnect"))
271 126
272 self.__zoomLayout = QHBoxLayout()
273 self.__osdLabel = QLabel()
274 self.__osdLabel.setSizePolicy(
275 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
276 )
277 self.__zoomLayout.addWidget(self.__osdLabel)
278
279 self.__zoom0 = self.replEdit.fontPointSize()
280 self.__zoomWidget = EricZoomWidget(
281 EricPixmapCache.getPixmap("zoomOut"),
282 EricPixmapCache.getPixmap("zoomIn"),
283 EricPixmapCache.getPixmap("zoomReset"),
284 self,
285 )
286 self.__zoomLayout.addWidget(self.__zoomWidget)
287 self.layout().insertLayout(self.layout().count() - 1, self.__zoomLayout)
288 self.__zoomWidget.setMinimum(self.ZoomMin)
289 self.__zoomWidget.setMaximum(self.ZoomMax)
290 self.__zoomWidget.valueChanged.connect(self.__doZoom)
291 self.__currentZoom = 0
292
293 self.__fileManager = None 127 self.__fileManager = None
294 self.__fileManagerWidget = None 128 self.__fileManagerWidget = None
295 self.__chartWidget = None 129 self.__chartWidget = None
296 130
297 self.__unknownPorts = [] 131 self.__unknownPorts = []
304 self.__device = None 138 self.__device = None
305 self.__connected = False 139 self.__connected = False
306 self.__linkConnected = False 140 self.__linkConnected = False
307 self.__setConnected(False) 141 self.__setConnected(False)
308 142
309 self.__replBuffer = b""
310
311 if not HAS_QTSERIALPORT: 143 if not HAS_QTSERIALPORT:
312 self.replEdit.setHtml( 144 self.replWidget.replEdit().setHtml(
313 self.tr( 145 self.tr(
314 "<h3>The QtSerialPort package is not available.<br/>" 146 "<h3>The QtSerialPort package is not available.<br/>"
315 "MicroPython support is deactivated.</h3>" 147 "MicroPython support is deactivated.</h3>"
316 ) 148 )
317 ) 149 )
318 self.setEnabled(False) 150 self.setEnabled(False)
319 return 151 return
320 152
321 self.__vt100Re = re.compile(
322 r"(?P<count>\d*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])"
323 )
324
325 self.__populateDeviceTypeComboBox() 153 self.__populateDeviceTypeComboBox()
326 154
327 self.repopulateButton.clicked.connect(self.__populateDeviceTypeComboBox) 155 self.repopulateButton.clicked.connect(self.__populateDeviceTypeComboBox)
328 self.webreplConfigButton.clicked.connect(self.__configureWebreplUrls) 156 self.webreplConfigButton.clicked.connect(self.__configureWebreplUrls)
329
330 self.replEdit.installEventFilter(self)
331 # Hack to intercept middle button paste
332 self.__origReplEditMouseReleaseEvent = self.replEdit.mouseReleaseEvent
333 self.replEdit.mouseReleaseEvent = self.__replEditMouseReleaseEvent
334
335 self.replEdit.customContextMenuRequested.connect(self.__showContextMenu)
336 self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged) 157 self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
337 158
338 self.__handlePreferencesChanged() 159 self.__handlePreferencesChanged()
339
340 charFormat = self.replEdit.currentCharFormat()
341 self.DefaultForeground = charFormat.foreground()
342 self.DefaultBackground = charFormat.background()
343 160
344 def __populateDeviceTypeComboBox(self): 161 def __populateDeviceTypeComboBox(self):
345 """ 162 """
346 Private method to populate the device type selector. 163 Private method to populate the device type selector.
347 """ 164 """
429 ) 246 )
430 self.deviceTypeComboBox.setItemData( 247 self.deviceTypeComboBox.setItemData(
431 index, webreplUrlsDict[name]["url"], self.DeviceWebreplUrlRole 248 index, webreplUrlsDict[name]["url"], self.DeviceWebreplUrlRole
432 ) 249 )
433 webreplMessage = ( 250 webreplMessage = (
434 self.tr( 251 self.tr("\n%n WebREPL connection(s) defined.", "", len(webreplUrlsDict))
435 "\n%n WebREPL connection(s) defined.", "", len(webreplUrlsDict)
436 )
437 if webreplUrlsDict 252 if webreplUrlsDict
438 else "" 253 else ""
439 ) 254 )
440 255
441 self.deviceInfoLabel.setText(supportedMessage + unknownMessage + webreplMessage) 256 self.deviceInfoLabel.setText(supportedMessage + unknownMessage + webreplMessage)
505 320
506 def __handlePreferencesChanged(self): 321 def __handlePreferencesChanged(self):
507 """ 322 """
508 Private slot to handle a change in preferences. 323 Private slot to handle a change in preferences.
509 """ 324 """
510 self.__colorScheme = Preferences.getMicroPython("ColorScheme") 325 self.replWidget.replEdit().handlePreferencesChanged()
511
512 self.__font = Preferences.getEditorOtherFonts("MonospacedFont")
513 self.replEdit.setFontFamily(self.__font.family())
514 self.replEdit.setFontPointSize(self.__font.pointSize())
515
516 if Preferences.getMicroPython("ReplLineWrap"):
517 self.replEdit.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
518 else:
519 self.replEdit.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
520 326
521 if self.__interface is not None: 327 if self.__interface is not None:
522 self.__interface.handlePreferencesChanged 328 self.__interface.handlePreferencesChanged
523 329
524 if self.__chartWidget is not None: 330 if self.__chartWidget is not None:
620 self.filesButton.setEnabled(kwargs["files"] and self.__connected) 426 self.filesButton.setEnabled(kwargs["files"] and self.__connected)
621 if "chart" in kwargs: 427 if "chart" in kwargs:
622 self.chartButton.setEnabled( 428 self.chartButton.setEnabled(
623 kwargs["chart"] and HAS_QTCHART and self.__connected 429 kwargs["chart"] and HAS_QTCHART and self.__connected
624 ) 430 )
625
626 @pyqtSlot(QPoint)
627 def __showContextMenu(self, pos):
628 """
629 Private slot to show the REPL context menu.
630
631 @param pos position to show the menu at
632 @type QPoint
633 """
634 if OSUtilities.isMacPlatform():
635 copyKeys = QKeySequence("Ctrl+C")
636 pasteKeys = QKeySequence("Ctrl+V")
637 selectAllKeys = QKeySequence("Ctrl+A")
638 else:
639 copyKeys = QKeySequence("Ctrl+Shift+C")
640 pasteKeys = QKeySequence("Ctrl+Shift+V")
641 selectAllKeys = QKeySequence("Ctrl+Shift+A")
642
643 menu = QMenu(self)
644 menu.addAction(
645 EricPixmapCache.getIcon("editDelete"), self.tr("Clear"), self.__clear
646 ).setEnabled(bool(self.replEdit.toPlainText()))
647 menu.addSeparator()
648 menu.addAction(
649 EricPixmapCache.getIcon("editCopy"),
650 self.tr("Copy"),
651 copyKeys,
652 self.replEdit.copy,
653 ).setEnabled(self.replEdit.textCursor().hasSelection())
654 menu.addAction(
655 EricPixmapCache.getIcon("editPaste"),
656 self.tr("Paste"),
657 pasteKeys,
658 self.__paste,
659 ).setEnabled(self.replEdit.canPaste() and self.__connected)
660 menu.addSeparator()
661 menu.addAction(
662 EricPixmapCache.getIcon("editSelectAll"),
663 self.tr("Select All"),
664 selectAllKeys,
665 self.replEdit.selectAll,
666 ).setEnabled(bool(self.replEdit.toPlainText()))
667
668 menu.exec(self.replEdit.mapToGlobal(pos))
669 431
670 def __setConnected(self, connected): 432 def __setConnected(self, connected):
671 """ 433 """
672 Private method to set the connection status LED. 434 Private method to set the connection status LED.
673 435
759 """ {0}</p>""" 521 """ {0}</p>"""
760 ).format(reason), 522 ).format(reason),
761 ) 523 )
762 return 524 return
763 525
764 self.replEdit.clear() 526 self.replWidget.replEdit().clear()
765 self.__interface.dataReceived.connect(self.__processData) 527 self.__interface.dataReceived.connect(
528 self.replWidget.replEdit().processData
529 )
766 530
767 if not self.__interface.isConnected(): 531 if not self.__interface.isConnected():
768 self.__connectToDevice() 532 self.__connectToDevice()
769 if self.__device.forceInterrupt(): 533 if self.__device.forceInterrupt():
770 # send a Ctrl-B (exit raw mode) 534 # send a Ctrl-B (exit raw mode)
771 self.__interface.write(b"\x02") 535 self.__interface.write(b"\x02")
772 # send Ctrl-C (keyboard interrupt) 536 # send Ctrl-C (keyboard interrupt)
773 self.__interface.write(b"\x03") 537 self.__interface.write(b"\x03")
774 538
775 self.__device.setRepl(True) 539 self.__device.setRepl(True)
776 self.replEdit.setFocus(Qt.FocusReason.OtherFocusReason) 540 self.replWidget.replEdit().setFocus(Qt.FocusReason.OtherFocusReason)
777 else: 541 else:
778 with contextlib.suppress(TypeError): 542 with contextlib.suppress(TypeError):
779 if self.__interface is not None: 543 if self.__interface is not None:
780 self.__interface.dataReceived.disconnect(self.__processData) 544 self.__interface.dataReceived.disconnect(
545 self.replWidget.replEdit().processData
546 )
781 if not self.chartButton.isChecked() and not self.filesButton.isChecked(): 547 if not self.chartButton.isChecked() and not self.filesButton.isChecked():
782 self.__disconnectFromDevice() 548 self.__disconnectFromDevice()
783 self.__device.setRepl(False) 549 self.__device.setRepl(False)
784 self.replButton.setChecked(checked) 550 self.replButton.setChecked(checked)
785 551
787 def on_connectButton_clicked(self): 553 def on_connectButton_clicked(self):
788 """ 554 """
789 Private slot to connect to the selected device or disconnect from the 555 Private slot to connect to the selected device or disconnect from the
790 currently connected device. 556 currently connected device.
791 """ 557 """
792 self.__osdLabel.clear() 558 self.replWidget.clearOSD()
793 if self.__linkConnected: 559 if self.__linkConnected:
794 with EricOverrideCursor(): 560 with EricOverrideCursor():
795 self.__disconnectFromDevice() 561 self.__disconnectFromDevice()
796 562
797 if self.replButton.isChecked(): 563 if self.replButton.isChecked():
801 if self.chartButton.isChecked(): 567 if self.chartButton.isChecked():
802 self.on_chartButton_clicked(False) 568 self.on_chartButton_clicked(False)
803 else: 569 else:
804 with EricOverrideCursor(): 570 with EricOverrideCursor():
805 self.__connectToDevice(withAutostart=True) 571 self.__connectToDevice(withAutostart=True)
806
807 @pyqtSlot()
808 def __clear(self):
809 """
810 Private slot to clear the REPL pane.
811 """
812 self.replEdit.clear()
813 if bool(self.__interface) and self.__interface.isConnected():
814 self.__interface.write(b"\r")
815
816 @pyqtSlot()
817 def __paste(self, mode=QClipboard.Mode.Clipboard):
818 """
819 Private slot to perform a paste operation.
820
821 @param mode paste mode (defaults to QClipboard.Mode.Clipboard)
822 @type QClipboard.Mode (optional)
823 """
824 # add support for paste by mouse middle button
825 clipboard = QApplication.clipboard()
826 if clipboard:
827 pasteText = clipboard.text(mode=mode)
828 if pasteText:
829 pasteText = pasteText.replace("\n\r", "\r")
830 pasteText = pasteText.replace("\n", "\r")
831 if bool(self.__interface) and self.__interface.isConnected():
832 self.__interface.write(b"\x05")
833 self.__interface.write(pasteText.encode("utf-8"))
834 self.__interface.write(b"\x04")
835
836 def eventFilter(self, obj, evt):
837 """
838 Public method to process events for the REPL pane.
839
840 @param obj reference to the object the event was meant for
841 @type QObject
842 @param evt reference to the event object
843 @type QEvent
844 @return flag to indicate that the event was handled
845 @rtype bool
846 """
847 if obj is self.replEdit and evt.type() == QEvent.Type.KeyPress:
848 # handle the key press event on behalf of the REPL pane
849 key = evt.key()
850 msg = bytes(evt.text(), "utf8")
851 if key == Qt.Key.Key_Backspace:
852 msg = b"\b"
853 elif key == Qt.Key.Key_Delete:
854 msg = b"\x1B[\x33\x7E"
855 elif key == Qt.Key.Key_Up:
856 msg = b"\x1B[A"
857 elif key == Qt.Key.Key_Down:
858 msg = b"\x1B[B"
859 elif key == Qt.Key.Key_Right:
860 msg = b"\x1B[C"
861 elif key == Qt.Key.Key_Left:
862 msg = b"\x1B[D"
863 elif key == Qt.Key.Key_Home:
864 msg = b"\x1B[H"
865 elif key == Qt.Key.Key_End:
866 msg = b"\x1B[F"
867 elif (
868 OSUtilities.isMacPlatform()
869 and evt.modifiers() == Qt.KeyboardModifier.MetaModifier
870 ) or (
871 not OSUtilities.isMacPlatform()
872 and evt.modifiers() == Qt.KeyboardModifier.ControlModifier
873 ):
874 if Qt.Key.Key_A <= key <= Qt.Key.Key_Z:
875 # devices treat an input of \x01 as Ctrl+A, etc.
876 msg = bytes([1 + key - Qt.Key.Key_A])
877 elif evt.modifiers() == (
878 Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier
879 ) or (
880 OSUtilities.isMacPlatform()
881 and evt.modifiers() == Qt.KeyboardModifier.ControlModifier
882 ):
883 if key == Qt.Key.Key_C:
884 self.replEdit.copy()
885 msg = b""
886 elif key == Qt.Key.Key_V:
887 self.__paste()
888 msg = b""
889 elif key == Qt.Key.Key_A:
890 self.replEdit.selectAll()
891 msg = b""
892 elif key in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
893 tc = self.replEdit.textCursor()
894 tc.movePosition(QTextCursor.MoveOperation.EndOfLine)
895 self.replEdit.setTextCursor(tc)
896 if bool(self.__interface) and self.__interface.isConnected():
897 self.__interface.write(msg)
898 return True
899 else:
900 # standard event processing
901 return super().eventFilter(obj, evt)
902
903 def __replEditMouseReleaseEvent(self, evt):
904 """
905 Private method handling mouse release events for the replEdit widget.
906
907 Note: this is a hack because QTextEdit does not allow filtering of
908 QEvent.Type.MouseButtonRelease. To make middle button paste work, we
909 had to intercept the protected event method (some kind of
910 reimplementing it).
911
912 @param evt reference to the event object
913 @type QMouseEvent
914 """
915 if evt.button() == Qt.MouseButton.MiddleButton:
916 self.__paste(mode=QClipboard.Mode.Selection)
917 msg = b""
918 if bool(self.__interface) and self.__interface.isConnected():
919 self.__interface.write(msg)
920 evt.accept()
921 else:
922 self.__origReplEditMouseReleaseEvent(evt)
923
924 def __processData(self, data):
925 """
926 Private slot to process bytes received from the device.
927
928 @param data bytes received from the device
929 @type bytes
930 """
931 tc = self.replEdit.textCursor()
932 # the text cursor must be on the last line
933 while tc.movePosition(QTextCursor.MoveOperation.Down):
934 pass
935
936 # set the font
937 charFormat = tc.charFormat()
938 charFormat.setFontFamilies([self.__font.family()])
939 charFormat.setFontPointSize(self.__font.pointSize())
940 tc.setCharFormat(charFormat)
941
942 # add received data to the buffered one
943 data = self.__replBuffer + data
944
945 index = 0
946 while index < len(data):
947 if data[index] == 8: # \b
948 tc.movePosition(QTextCursor.MoveOperation.Left)
949 self.replEdit.setTextCursor(tc)
950 elif data[index] in (4, 13): # EOT, \r
951 pass
952 elif len(data) > index + 1 and data[index] == 27 and data[index + 1] == 91:
953 # VT100 cursor command detected: <Esc>[
954 index += 2 # move index to after the [
955 match = self.__vt100Re.search(
956 data[index:].decode("utf-8", errors="replace")
957 )
958 if match:
959 # move to last position in control sequence
960 # ++ will be done at end of loop
961 index += match.end() - 1
962
963 action = match.group("action")
964 if action in "ABCD":
965 if match.group("count") == "":
966 count = 1
967 else:
968 count = int(match.group("count"))
969
970 if action == "A": # up
971 tc.movePosition(QTextCursor.MoveOperation.Up, n=count)
972 self.replEdit.setTextCursor(tc)
973 elif action == "B": # down
974 tc.movePosition(QTextCursor.MoveOperation.Down, n=count)
975 self.replEdit.setTextCursor(tc)
976 elif action == "C": # right
977 tc.movePosition(QTextCursor.MoveOperation.Right, n=count)
978 self.replEdit.setTextCursor(tc)
979 elif action == "D": # left
980 tc.movePosition(QTextCursor.MoveOperation.Left, n=count)
981 self.replEdit.setTextCursor(tc)
982 elif action == "K": # delete things
983 if match.group("count") in ("", "0"):
984 # delete to end of line
985 tc.movePosition(
986 QTextCursor.MoveOperation.EndOfLine,
987 mode=QTextCursor.MoveMode.KeepAnchor,
988 )
989 tc.removeSelectedText()
990 self.replEdit.setTextCursor(tc)
991 elif match.group("count") == "1":
992 # delete to beginning of line
993 tc.movePosition(
994 QTextCursor.MoveOperation.StartOfLine,
995 mode=QTextCursor.MoveMode.KeepAnchor,
996 )
997 tc.removeSelectedText()
998 self.replEdit.setTextCursor(tc)
999 elif match.group("count") == "2":
1000 # delete whole line
1001 tc.movePosition(QTextCursor.MoveOperation.EndOfLine)
1002 tc.movePosition(
1003 QTextCursor.MoveOperation.StartOfLine,
1004 mode=QTextCursor.MoveMode.KeepAnchor,
1005 )
1006 tc.removeSelectedText()
1007 self.replEdit.setTextCursor(tc)
1008 elif action == "m":
1009 self.__setCharFormat(match.group(0)[:-1].split(";"), tc)
1010 elif (
1011 len(data) > index + 1
1012 and data[index] == 27
1013 and data[index + 1 : index + 4] == b"]0;"
1014 ):
1015 if b"\x1b\\" in data[index + 4 :]:
1016 # 'set window title' command detected: <Esc>]0;...<Esc>\
1017 # __IGNORE_WARNING_M891__
1018 titleData = data[index + 4 :].split(b"\x1b\\")[0]
1019 title = titleData.decode()
1020 index += len(titleData) + 5 # one more is done at the end
1021 self.__osdLabel.setText(title)
1022 else:
1023 # data is incomplete; buffer and stop processing
1024 self.__replBuffer = data[index:]
1025 return
1026 else:
1027 tc.deleteChar()
1028 self.replEdit.setTextCursor(tc)
1029 self.replEdit.insertPlainText(chr(data[index]))
1030
1031 index += 1
1032
1033 self.replEdit.ensureCursorVisible()
1034 self.__replBuffer = b""
1035
1036 def __setCharFormat(self, formatCodes, textCursor):
1037 """
1038 Private method setting the current text format of the REPL pane based
1039 on the passed ANSI codes.
1040
1041 Following codes are used:
1042 <ul>
1043 <li>0: Reset</li>
1044 <li>1: Bold font (weight 75)</li>
1045 <li>2: Light font (weight 25)</li>
1046 <li>3: Italic font</li>
1047 <li>4: Underlined font</li>
1048 <li>9: Strikeout font</li>
1049 <li>21: Bold off (weight 50)</li>
1050 <li>22: Light off (weight 50)</li>
1051 <li>23: Italic off</li>
1052 <li>24: Underline off</li>
1053 <li>29: Strikeout off</li>
1054 <li>30: foreground Black</li>
1055 <li>31: foreground Dark Red</li>
1056 <li>32: foreground Dark Green</li>
1057 <li>33: foreground Dark Yellow</li>
1058 <li>34: foreground Dark Blue</li>
1059 <li>35: foreground Dark Magenta</li>
1060 <li>36: foreground Dark Cyan</li>
1061 <li>37: foreground Light Gray</li>
1062 <li>39: reset foreground to default</li>
1063 <li>40: background Black</li>
1064 <li>41: background Dark Red</li>
1065 <li>42: background Dark Green</li>
1066 <li>43: background Dark Yellow</li>
1067 <li>44: background Dark Blue</li>
1068 <li>45: background Dark Magenta</li>
1069 <li>46: background Dark Cyan</li>
1070 <li>47: background Light Gray</li>
1071 <li>49: reset background to default</li>
1072 <li>53: Overlined font</li>
1073 <li>55: Overline off</li>
1074 <li>90: bright foreground Dark Gray</li>
1075 <li>91: bright foreground Red</li>
1076 <li>92: bright foreground Green</li>
1077 <li>93: bright foreground Yellow</li>
1078 <li>94: bright foreground Blue</li>
1079 <li>95: bright foreground Magenta</li>
1080 <li>96: bright foreground Cyan</li>
1081 <li>97: bright foreground White</li>
1082 <li>100: bright background Dark Gray</li>
1083 <li>101: bright background Red</li>
1084 <li>102: bright background Green</li>
1085 <li>103: bright background Yellow</li>
1086 <li>104: bright background Blue</li>
1087 <li>105: bright background Magenta</li>
1088 <li>106: bright background Cyan</li>
1089 <li>107: bright background White</li>
1090 </ul>
1091
1092 @param formatCodes list of format codes
1093 @type list of str
1094 @param textCursor reference to the text cursor
1095 @type QTextCursor
1096 """
1097 if not formatCodes:
1098 # empty format codes list is treated as a reset
1099 formatCodes = ["0"]
1100
1101 charFormat = textCursor.charFormat()
1102 for formatCode in formatCodes:
1103 try:
1104 formatCode = int(formatCode)
1105 except ValueError:
1106 # ignore non digit values
1107 continue
1108
1109 if formatCode == 0:
1110 charFormat.setFontWeight(50)
1111 charFormat.setFontItalic(False)
1112 charFormat.setFontUnderline(False)
1113 charFormat.setFontStrikeOut(False)
1114 charFormat.setFontOverline(False)
1115 charFormat.setForeground(self.DefaultForeground)
1116 charFormat.setBackground(self.DefaultBackground)
1117 elif formatCode == 1:
1118 charFormat.setFontWeight(75)
1119 elif formatCode == 2:
1120 charFormat.setFontWeight(25)
1121 elif formatCode == 3:
1122 charFormat.setFontItalic(True)
1123 elif formatCode == 4:
1124 charFormat.setFontUnderline(True)
1125 elif formatCode == 9:
1126 charFormat.setFontStrikeOut(True)
1127 elif formatCode in (21, 22):
1128 charFormat.setFontWeight(50)
1129 elif formatCode == 23:
1130 charFormat.setFontItalic(False)
1131 elif formatCode == 24:
1132 charFormat.setFontUnderline(False)
1133 elif formatCode == 29:
1134 charFormat.setFontStrikeOut(False)
1135 elif formatCode == 53:
1136 charFormat.setFontOverline(True)
1137 elif formatCode == 55:
1138 charFormat.setFontOverline(False)
1139 elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37):
1140 charFormat.setForeground(
1141 AnsiColorSchemes[self.__colorScheme][formatCode - 30]
1142 )
1143 elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47):
1144 charFormat.setBackground(
1145 AnsiColorSchemes[self.__colorScheme][formatCode - 40]
1146 )
1147 elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97):
1148 charFormat.setForeground(
1149 AnsiColorSchemes[self.__colorScheme][formatCode - 80]
1150 )
1151 elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107):
1152 charFormat.setBackground(
1153 AnsiColorSchemes[self.__colorScheme][formatCode - 90]
1154 )
1155 elif formatCode == 39:
1156 charFormat.setForeground(self.DefaultForeground)
1157 elif formatCode == 49:
1158 charFormat.setBackground(self.DefaultBackground)
1159
1160 textCursor.setCharFormat(charFormat)
1161
1162 def __doZoom(self, value):
1163 """
1164 Private slot to zoom the REPL pane.
1165
1166 @param value zoom value
1167 @type int
1168 """
1169 if value < self.__currentZoom:
1170 self.replEdit.zoomOut(self.__currentZoom - value)
1171 elif value > self.__currentZoom:
1172 self.replEdit.zoomIn(value - self.__currentZoom)
1173 self.__currentZoom = value
1174 572
1175 def getCurrentPort(self): 573 def getCurrentPort(self):
1176 """ 574 """
1177 Public method to determine the port path of the selected device. 575 Public method to determine the port path of the selected device.
1178 576
1285 self.__lastDeviceType = deviceType 683 self.__lastDeviceType = deviceType
1286 else: 684 else:
1287 return 685 return
1288 686
1289 self.__interface = MicroPythonWebreplDeviceInterface(self) 687 self.__interface = MicroPythonWebreplDeviceInterface(self)
688 self.replWidget.replEdit().setInterface(self.__interface)
1290 689
1291 if self.__interface.connectToDevice(port): 690 if self.__interface.connectToDevice(port):
1292 deviceResponding = self.__interface.probeDevice() 691 deviceResponding = self.__interface.probeDevice()
1293 self.__setConnected(deviceResponding) 692 self.__setConnected(deviceResponding)
1294 self.__device.setConnected(deviceResponding) 693 self.__device.setConnected(deviceResponding)
1338 737
1339 if self.__interface is not None: 738 if self.__interface is not None:
1340 self.__interface.disconnectFromDevice() 739 self.__interface.disconnectFromDevice()
1341 self.__interface.deleteLater() 740 self.__interface.deleteLater()
1342 self.__interface = None 741 self.__interface = None
742 self.replWidget.replEdit().setInterface(None)
1343 743
1344 @pyqtSlot() 744 @pyqtSlot()
1345 def on_runButton_clicked(self): 745 def on_runButton_clicked(self):
1346 """ 746 """
1347 Private slot to execute the script of the active editor on the 747 Private slot to execute the script of the active editor on the

eric ide

mercurial