eric6/MicroPython/MicroPythonReplWidget.py

branch
micropython
changeset 7054
fb84d8489bc1
child 7058
bdd583f96e96
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/MicroPython/MicroPythonReplWidget.py	Sun Jul 07 18:48:17 2019 +0200
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the MicroPython REPL widget.
+"""
+
+from __future__ import unicode_literals
+
+import re
+
+from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QEvent
+from PyQt5.QtGui import QColor, QKeySequence, QTextCursor
+from PyQt5.QtWidgets import (
+    QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy)
+try:
+    from PyQt5.QtSerialPort import QSerialPortInfo  # __IGNORE_WARNING__
+    HAS_QTSERIALPORT = True
+except ImportError:
+    HAS_QTSERIALPORT = False
+
+from E5Gui.E5ZoomWidget import E5ZoomWidget
+
+from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget
+
+from . import MicroPythonDevices
+
+import Globals
+import UI.PixmapCache
+
+
+class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget):
+    """
+    Class implementing the MicroPython REPL widget.
+    """
+    ZoomMin = -10
+    ZoomMax = 20
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
+        
+        @param parent reference to the parent widget
+        @type QWidget
+        """
+        super(MicroPythonReplWidget, self).__init__(parent)
+        self.setupUi(self)
+        
+        self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
+            "", False))
+        self.checkButton.setIcon(UI.PixmapCache.getIcon("question"))
+        self.runButton.setIcon(UI.PixmapCache.getIcon("start"))
+        self.replButton.setIcon(UI.PixmapCache.getIcon("terminal"))
+        self.filesButton.setIcon(UI.PixmapCache.getIcon("filemanager"))
+        self.chartButton.setIcon(UI.PixmapCache.getIcon("chart"))
+        self.disconnectButton.setIcon(UI.PixmapCache.getIcon("disconnect"))
+        
+        self.__zoomLayout = QHBoxLayout()
+        spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding,
+                                 QSizePolicy.Minimum)
+        self.__zoomLayout.addSpacerItem(spacerItem)
+        
+        self.__zoom0 = self.replEdit.fontPointSize()
+        self.__zoomWidget = E5ZoomWidget(
+            UI.PixmapCache.getPixmap("zoomOut"),
+            UI.PixmapCache.getPixmap("zoomIn"),
+            UI.PixmapCache.getPixmap("zoomReset"), self)
+        self.__zoomLayout.addWidget(self.__zoomWidget)
+        self.layout().insertLayout(
+            self.layout().count() - 1,
+            self.__zoomLayout)
+        self.__zoomWidget.setMinimum(self.ZoomMin)
+        self.__zoomWidget.setMaximum(self.ZoomMax)
+        self.__zoomWidget.valueChanged.connect(self.__doZoom)
+        self.__currentZoom = 0
+        
+        self.__serial = None
+        self.__device = None
+        self.setConnected(False)
+        
+        if not HAS_QTSERIALPORT:
+            self.replEdit.setHtml(self.tr(
+                "<h3>The QtSerialPort package is not available.<br/>"
+                "MicroPython support is deactivated.</h3>"))
+            self.setEnabled(False)
+            return
+        
+        self.__vt100Re = re.compile(
+            r'(?P<count>[\d]*)(;?[\d]*)*(?P<action>[ABCDKm])')
+        
+        self.__populateDeviceTypeComboBox()
+        
+        self.replEdit.setAcceptRichText(False)
+        self.replEdit.setUndoRedoEnabled(False)
+        self.replEdit.setContextMenuPolicy(Qt.CustomContextMenu)
+        
+        self.replEdit.installEventFilter(self)
+        
+        self.replEdit.customContextMenuRequested.connect(
+            self.__showContextMenu)
+    
+    def __populateDeviceTypeComboBox(self):
+        """
+        Private method to populate the device type selector.
+        """
+        self.deviceTypeComboBox.clear()
+        self.deviceInfoLabel.clear()
+        
+        self.deviceTypeComboBox.addItem("", "")
+        devices = MicroPythonDevices.getFoundDevices()
+        if devices:
+            self.deviceInfoLabel.setText(
+                self.tr("%n supported device(s) detected.", n=len(devices)))
+            for device in sorted(devices):
+                self.deviceTypeComboBox.addItem(
+                    self.tr("{0} at {1}".format(device[1], device[2])),
+                    device[0])
+        else:
+            self.deviceInfoLabel.setText(
+                self.tr("No supported devices detected."))
+        
+        self.on_deviceTypeComboBox_activated(0)
+    
+    @pyqtSlot(int)
+    def on_deviceTypeComboBox_activated(self, index):
+        """
+        Private slot handling the selection of a device type.
+        
+        @param index index of the selected device
+        @type int
+        """
+        deviceType = self.deviceTypeComboBox.itemData(index)
+        self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
+            deviceType, False))
+        
+        self.__device = MicroPythonDevices.getDevice(deviceType)
+        if self.__device:
+            actions = self.__device.supportedActions()
+        else:
+            actions = tuple()
+        self.__setActionButtons(actions)
+    
+    @pyqtSlot()
+    def on_checkButton_clicked(self):
+        """
+        Private slot to check for connected devices.
+        """
+        self.__populateDeviceTypeComboBox()
+    
+    def __setActionButtons(self, actions):
+        """
+        Private method to set the enabled state of the various action buttons.
+        
+        @param actions tuple of supported actions out of "repl", "run",
+            "files", "chart"
+        @type tuple of str
+        """
+        self.runButton.setEnabled("run" in actions)
+        self.replButton.setEnabled("repl" in actions)
+        self.filesButton.setEnabled("files" in actions)
+        self.chartButton.setEnabled("chart" in actions)
+    
+    @pyqtSlot(QPoint)
+    def __showContextMenu(self, pos):
+        """
+        Privat slot to show the REPL context menu.
+        
+        @param pos position to show the menu at
+        @type QPoint
+        """
+        if Globals.isMacPlatform():
+            copyKeys = QKeySequence(Qt.CTRL + Qt.Key_C)
+            pasteKeys = QKeySequence(Qt.CTRL + Qt.Key_V)
+        else:
+            copyKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_C)
+            pasteKeys = QKeySequence(Qt.CTRL + Qt.SHIFT + Qt.Key_V)
+        menu = QMenu(self)
+        menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
+        menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
+        menu.exec_(self.replEdit.mapToGlobal(pos))
+    
+    def setConnected(self, connected):
+        """
+        Public method to set the connection status LED.
+        
+        @param connected connection state
+        @type bool
+        """
+        if connected:
+            self.deviceConnectedLed.setColor(QColor(Qt.green))
+        else:
+            self.deviceConnectedLed.setColor(QColor(Qt.red))
+        
+        self.deviceTypeComboBox.setEnabled(not connected)
+        
+        self.disconnectButton.setEnabled(connected)
+    
+    @pyqtSlot()
+    def on_replButton_clicked(self):
+        """
+        Private slot to connect to the selected device and start a REPL.
+        """
+        # TODO: not implemented yet
+        raise NotImplementedError
+    
+    @pyqtSlot()
+    def on_disconnectButton_clicked(self):
+        """
+        Private slot to disconnect from the currently connected device.
+        """
+        # TODO: not implemented yet
+        raise NotImplementedError
+    
+    def __activatePlotter(self):
+        """
+        Private method to activate a data plotter widget.
+        """
+        # TODO: not implemented yet
+        raise NotImplementedError
+    
+    def __deactivatePlotter(self):
+        """
+        Private method to deactivate the plotter widget.
+        """
+        # TODO: not implemented yet
+    
+    @pyqtSlot()
+    def __paste(self):
+        """
+        Private slot to perform a paste operation.
+        """
+        clipboard = QApplication.clipboard()
+        if clipboard and clipboard.text():
+            pasteText = clipboard.text().replace('\n\r', '\r')
+            pasteText = pasteText.replace('\n', '\r')
+            self.__serial and self.__serial.write(pasteText.encode("utf-8"))
+    
+    def eventFilter(self, obj, evt):
+        """
+        Public method to process events for the REPL pane.
+        
+        @param obj reference to the object the event was meant for
+        @type QObject
+        @param evt reference to the event object
+        @type QEvent
+        @return flag to indicate that the event was handled
+        @rtype bool
+        """
+        if obj is self.replEdit and evt.type() == QEvent.KeyPress:
+            # handle the key press event on behalve of the REPL pane
+            key = evt.key()
+            msg = bytes(evt.text(), 'utf8')
+            if key == Qt.Key_Backspace:
+                msg = b'\b'
+            elif key == Qt.Key_Delete:
+                msg = b'\x1B[\x33\x7E'
+            elif key == Qt.Key_Up:
+                msg = b'\x1B[A'
+            elif key == Qt.Key_Down:
+                msg = b'\x1B[B'
+            elif key == Qt.Key_Right:
+                msg = b'\x1B[C'
+            elif key == Qt.Key_Left:
+                msg = b'\x1B[D'
+            elif key == Qt.Key_Home:
+                msg = b'\x1B[H'
+            elif key == Qt.Key_End:
+                msg = b'\x1B[F'
+            elif ((Globals.isMacPlatform() and
+                   evt.modifiers() == Qt.MetaModifier) or
+                  (not Globals.isMacPlatform() and
+                   evt.modifiers() == Qt.ControlModifier)):
+                if Qt.Key_A <= key <= Qt.Key_Z:
+                    # devices treat an input of \x01 as Ctrl+A, etc.
+                    msg = bytes([1 + key - Qt.Key_A])
+            elif (evt.modifiers() == Qt.ControlModifier | Qt.ShiftModifier or
+                  (Globals.isMacPlatform() and
+                   evt.modifiers() == Qt.ControlModifier)):
+                if key == Qt.Key_C:
+                    self.replEdit.copy()
+                    msg = b''
+                elif key == Qt.Key_V:
+                    self.__paste()
+                    msg = b''
+            self.__serial and self.__serial.write(msg)
+            return True
+        
+        else:
+            # standard event processing
+            return super(MicroPythonReplWidget, self).eventFilter(obj, evt)
+    
+    def __processData(self, data):
+        """
+        Private slot to process bytes received from the device.
+        
+        @param data bytes received from the device
+        @type bytes
+        """
+        tc = self.replEdit.textCursor()
+        # the text cursor must be on the last line
+        while tc.movePosition(QTextCursor.Down):
+            pass
+        
+        index = 1
+        while index < len(data):
+            if data[index] == 8:        # \b
+                tc.movePosition(QTextCursor.Left)
+                self.replEdit.setTextCursor(tc)
+            elif data[index] == 13:     # \r
+                pass
+            elif (len(data) > index + 1 and
+                  data[index] == 27 and
+                  data[index + 1] == 91):
+                # VT100 cursor command detected: <Esc>[
+                index += 2      # move index to after the [
+                match = self.__vt100Re.search(data[index:].decaode("utf-8"))
+                if match:
+                    # move to last position in control sequence
+                    # ++ will be done at end of loop
+                    index += match.end() - 1
+                    
+                    if match.group("count") == "":
+                        count = 1
+                    else:
+                        count = int(match.group("count"))
+                    
+                    action = match.group("action")
+                    if action == "A":       # up
+                        tc.movePosition(QTextCursor.Up, n=count)
+                        self.replEdit.setTextCursor(tc)
+                    elif action == "B":     # down
+                        tc.movePosition(QTextCursor.Down, n=count)
+                        self.replEdit.setTextCursor(tc)
+                    elif action == "C":     # right
+                        tc.movePosition(QTextCursor.Right, n=count)
+                        self.replEdit.setTextCursor(tc)
+                    elif action == "D":     # left
+                        tc.movePosition(QTextCursor.Left, n=count)
+                        self.replEdit.setTextCursor(tc)
+                    elif action == "K":     # delete things
+                        if match.group("count") == "":      # delete to eol
+                            tc.movePosition(QTextCursor.EndOfLine,
+                                            mode=QTextCursor.KeepAnchor)
+                            tc.removeSelectedText()
+                            self.replEdit.setTextCursor(tc)
+                    # TODO: add handling of 'm' (colors)
+            elif data[index] == 10:     # \n
+                tc.movePosition(QTextCursor.End)
+                self.replEdit.setTextCursor(tc)
+                self.replEdit.insertPlainText(chr(data[index]))
+            else:
+                tc.deleteChar()
+                self.replEdit.setTextCursor(tc)
+                self.replEdit.insertPlainText(chr(data[index]))
+            
+            index += 1
+        
+        self.replEdit.ensureCursorVisible()
+    
+    def __doZoom(self, value):
+        """
+        Private slot to zoom the REPL pane.
+        
+        @param value zoom value
+        @param int
+        """
+        if value < self.__currentZoom:
+            self.replEdit.zoomOut(self.__currentZoom - value)
+        elif value > self.__currentZoom:
+            self.replEdit.zoomIn(value - self.__currentZoom)
+        self.__currentZoom = value

eric ide

mercurial