eric6/MicroPython/MicroPythonReplWidget.py

branch
micropython
changeset 7054
fb84d8489bc1
child 7058
bdd583f96e96
equal deleted inserted replaced
7053:f0a7469a2ad4 7054:fb84d8489bc1
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
14 from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QEvent
15 from PyQt5.QtGui import QColor, QKeySequence, QTextCursor
16 from PyQt5.QtWidgets import (
17 QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy)
18 try:
19 from PyQt5.QtSerialPort import QSerialPortInfo # __IGNORE_WARNING__
20 HAS_QTSERIALPORT = True
21 except ImportError:
22 HAS_QTSERIALPORT = False
23
24 from E5Gui.E5ZoomWidget import E5ZoomWidget
25
26 from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget
27
28 from . import MicroPythonDevices
29
30 import Globals
31 import UI.PixmapCache
32
33
34 class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget):
35 """
36 Class implementing the MicroPython REPL widget.
37 """
38 ZoomMin = -10
39 ZoomMax = 20
40
41 def __init__(self, parent=None):
42 """
43 Constructor
44
45 @param parent reference to the parent widget
46 @type QWidget
47 """
48 super(MicroPythonReplWidget, self).__init__(parent)
49 self.setupUi(self)
50
51 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
52 "", False))
53 self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
54 self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
55 self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
56 self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager"))
57 self.chartButton.setIcon(UI.PixmapCache.getIcon("chart"))
58 self.disconnectButton.setIcon(UI.PixmapCache.getIcon("disconnect"))
59
60 self.__zoomLayout = QHBoxLayout()
61 spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding,
62 QSizePolicy.Minimum)
63 self.__zoomLayout.addSpacerItem(spacerItem)
64
65 self.__zoom0 = self.replEdit.fontPointSize()
66 self.__zoomWidget = E5ZoomWidget(
67 UI.PixmapCache.getPixmap("zoomOut"),
68 UI.PixmapCache.getPixmap("zoomIn"),
69 UI.PixmapCache.getPixmap("zoomReset"), self)
70 self.__zoomLayout.addWidget(self.__zoomWidget)
71 self.layout().insertLayout(
72 self.layout().count() - 1,
73 self.__zoomLayout)
74 self.__zoomWidget.setMinimum(self.ZoomMin)
75 self.__zoomWidget.setMaximum(self.ZoomMax)
76 self.__zoomWidget.valueChanged.connect(self.__doZoom)
77 self.__currentZoom = 0
78
79 self.__serial = None
80 self.__device = None
81 self.setConnected(False)
82
83 if not HAS_QTSERIALPORT:
84 self.replEdit.setHtml(self.tr(
85 "<h3>The QtSerialPort package is not available.<br/>"
86 "MicroPython support is deactivated.</h3>"))
87 self.setEnabled(False)
88 return
89
90 self.__vt100Re = re.compile(
91 r'(?P<count>[\d]*)(;?[\d]*)*(?P<action>[ABCDKm])')
92
93 self.__populateDeviceTypeComboBox()
94
95 self.replEdit.setAcceptRichText(False)
96 self.replEdit.setUndoRedoEnabled(False)
97 self.replEdit.setContextMenuPolicy(Qt.CustomContextMenu)
98
99 self.replEdit.installEventFilter(self)
100
101 self.replEdit.customContextMenuRequested.connect(
102 self.__showContextMenu)
103
104 def __populateDeviceTypeComboBox(self):
105 """
106 Private method to populate the device type selector.
107 """
108 self.deviceTypeComboBox.clear()
109 self.deviceInfoLabel.clear()
110
111 self.deviceTypeComboBox.addItem("", "")
112 devices = MicroPythonDevices.getFoundDevices()
113 if devices:
114 self.deviceInfoLabel.setText(
115 self.tr("%n supported device(s) detected.", n=len(devices)))
116 for device in sorted(devices):
117 self.deviceTypeComboBox.addItem(
118 self.tr("{0} at {1}".format(device[1], device[2])),
119 device[0])
120 else:
121 self.deviceInfoLabel.setText(
122 self.tr("No supported devices detected."))
123
124 self.on_deviceTypeComboBox_activated(0)
125
126 @pyqtSlot(int)
127 def on_deviceTypeComboBox_activated(self, index):
128 """
129 Private slot handling the selection of a device type.
130
131 @param index index of the selected device
132 @type int
133 """
134 deviceType = self.deviceTypeComboBox.itemData(index)
135 self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
136 deviceType, False))
137
138 self.__device = MicroPythonDevices.getDevice(deviceType)
139 if self.__device:
140 actions = self.__device.supportedActions()
141 else:
142 actions = tuple()
143 self.__setActionButtons(actions)
144
145 @pyqtSlot()
146 def on_checkButton_clicked(self):
147 """
148 Private slot to check for connected devices.
149 """
150 self.__populateDeviceTypeComboBox()
151
152 def __setActionButtons(self, actions):
153 """
154 Private method to set the enabled state of the various action buttons.
155
156 @param actions tuple of supported actions out of "repl", "run",
157 "files", "chart"
158 @type tuple of str
159 """
160 self.runButton.setEnabled("run" in actions)
161 self.replButton.setEnabled("repl" in actions)
162 self.filesButton.setEnabled("files" in actions)
163 self.chartButton.setEnabled("chart" in actions)
164
165 @pyqtSlot(QPoint)
166 def __showContextMenu(self, pos):
167 """
168 Privat slot to show the REPL context menu.
169
170 @param pos position to show the menu at
171 @type QPoint
172 """
173 if Globals.isMacPlatform():
174 copyKeys = QKeySequence(Qt.CTRL + Qt.Key_C)
175 pasteKeys = QKeySequence(Qt.CTRL + Qt.Key_V)
176 else:
177 copyKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_C)
178 pasteKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_V)
179 menu = QMenu(self)
180 menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
181 menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
182 menu.exec_(self.replEdit.mapToGlobal(pos))
183
184 def setConnected(self, connected):
185 """
186 Public method to set the connection status LED.
187
188 @param connected connection state
189 @type bool
190 """
191 if connected:
192 self.deviceConnectedLed.setColor(QColor(Qt.green))
193 else:
194 self.deviceConnectedLed.setColor(QColor(Qt.red))
195
196 self.deviceTypeComboBox.setEnabled(not connected)
197
198 self.disconnectButton.setEnabled(connected)
199
200 @pyqtSlot()
201 def on_replButton_clicked(self):
202 """
203 Private slot to connect to the selected device and start a REPL.
204 """
205 # TODO: not implemented yet
206 raise NotImplementedError
207
208 @pyqtSlot()
209 def on_disconnectButton_clicked(self):
210 """
211 Private slot to disconnect from the currently connected device.
212 """
213 # TODO: not implemented yet
214 raise NotImplementedError
215
216 def __activatePlotter(self):
217 """
218 Private method to activate a data plotter widget.
219 """
220 # TODO: not implemented yet
221 raise NotImplementedError
222
223 def __deactivatePlotter(self):
224 """
225 Private method to deactivate the plotter widget.
226 """
227 # TODO: not implemented yet
228
229 @pyqtSlot()
230 def __paste(self):
231 """
232 Private slot to perform a paste operation.
233 """
234 clipboard = QApplication.clipboard()
235 if clipboard and clipboard.text():
236 pasteText = clipboard.text().replace('\n\r', '\r')
237 pasteText = pasteText.replace('\n', '\r')
238 self.__serial and self.__serial.write(pasteText.encode("utf-8"))
239
240 def eventFilter(self, obj, evt):
241 """
242 Public method to process events for the REPL pane.
243
244 @param obj reference to the object the event was meant for
245 @type QObject
246 @param evt reference to the event object
247 @type QEvent
248 @return flag to indicate that the event was handled
249 @rtype bool
250 """
251 if obj is self.replEdit and evt.type() == QEvent.KeyPress:
252 # handle the key press event on behalve of the REPL pane
253 key = evt.key()
254 msg = bytes(evt.text(), 'utf8')
255 if key == Qt.Key_Backspace:
256 msg = b'\b'
257 elif key == Qt.Key_Delete:
258 msg = b'\x1B[\x33\x7E'
259 elif key == Qt.Key_Up:
260 msg = b'\x1B[A'
261 elif key == Qt.Key_Down:
262 msg = b'\x1B[B'
263 elif key == Qt.Key_Right:
264 msg = b'\x1B[C'
265 elif key == Qt.Key_Left:
266 msg = b'\x1B[D'
267 elif key == Qt.Key_Home:
268 msg = b'\x1B[H'
269 elif key == Qt.Key_End:
270 msg = b'\x1B[F'
271 elif ((Globals.isMacPlatform() and
272 evt.modifiers() == Qt.MetaModifier) or
273 (not Globals.isMacPlatform() and
274 evt.modifiers() == Qt.ControlModifier)):
275 if Qt.Key_A <= key <= Qt.Key_Z:
276 # devices treat an input of \x01 as Ctrl+A, etc.
277 msg = bytes([1 + key - Qt.Key_A])
278 elif (evt.modifiers() == Qt.ControlModifier | Qt.ShiftModifier or
279 (Globals.isMacPlatform() and
280 evt.modifiers() == Qt.ControlModifier)):
281 if key == Qt.Key_C:
282 self.replEdit.copy()
283 msg = b''
284 elif key == Qt.Key_V:
285 self.__paste()
286 msg = b''
287 self.__serial and self.__serial.write(msg)
288 return True
289
290 else:
291 # standard event processing
292 return super(MicroPythonReplWidget, self).eventFilter(obj, evt)
293
294 def __processData(self, data):
295 """
296 Private slot to process bytes received from the device.
297
298 @param data bytes received from the device
299 @type bytes
300 """
301 tc = self.replEdit.textCursor()
302 # the text cursor must be on the last line
303 while tc.movePosition(QTextCursor.Down):
304 pass
305
306 index = 1
307 while index < len(data):
308 if data[index] == 8: # \b
309 tc.movePosition(QTextCursor.Left)
310 self.replEdit.setTextCursor(tc)
311 elif data[index] == 13: # \r
312 pass
313 elif (len(data) > index + 1 and
314 data[index] == 27 and
315 data[index + 1] == 91):
316 # VT100 cursor command detected: <Esc>[
317 index += 2 # move index to after the [
318 match = self.__vt100Re.search(data[index:].decaode("utf-8"))
319 if match:
320 # move to last position in control sequence
321 # ++ will be done at end of loop
322 index += match.end() - 1
323
324 if match.group("count") == "":
325 count = 1
326 else:
327 count = int(match.group("count"))
328
329 action = match.group("action")
330 if action == "A": # up
331 tc.movePosition(QTextCursor.Up, n=count)
332 self.replEdit.setTextCursor(tc)
333 elif action == "B": # down
334 tc.movePosition(QTextCursor.Down, n=count)
335 self.replEdit.setTextCursor(tc)
336 elif action == "C": # right
337 tc.movePosition(QTextCursor.Right, n=count)
338 self.replEdit.setTextCursor(tc)
339 elif action == "D": # left
340 tc.movePosition(QTextCursor.Left, n=count)
341 self.replEdit.setTextCursor(tc)
342 elif action == "K": # delete things
343 if match.group("count") == "": # delete to eol
344 tc.movePosition(QTextCursor.EndOfLine,
345 mode=QTextCursor.KeepAnchor)
346 tc.removeSelectedText()
347 self.replEdit.setTextCursor(tc)
348 # TODO: add handling of 'm' (colors)
349 elif data[index] == 10: # \n
350 tc.movePosition(QTextCursor.End)
351 self.replEdit.setTextCursor(tc)
352 self.replEdit.insertPlainText(chr(data[index]))
353 else:
354 tc.deleteChar()
355 self.replEdit.setTextCursor(tc)
356 self.replEdit.insertPlainText(chr(data[index]))
357
358 index += 1
359
360 self.replEdit.ensureCursorVisible()
361
362 def __doZoom(self, value):
363 """
364 Private slot to zoom the REPL pane.
365
366 @param value zoom value
367 @param int
368 """
369 if value < self.__currentZoom:
370 self.replEdit.zoomOut(self.__currentZoom - value)
371 elif value > self.__currentZoom:
372 self.replEdit.zoomIn(value - self.__currentZoom)
373 self.__currentZoom = value

eric ide

mercurial