eric6/MicroPython/MicroPythonReplWidget.py

Mon, 29 Jul 2019 20:20:18 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 29 Jul 2019 20:20:18 +0200
branch
micropython
changeset 7095
8e10acb1cd85
parent 7094
d5f340dfb986
child 7103
aea236dc8002
permissions
-rw-r--r--

Refactored and improved the MicroPython code to be able to show the file manager and the REPL simultaneously.

# -*- 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, pyqtSignal, Qt, QPoint, QEvent
from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush
from PyQt5.QtWidgets import (
    QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy
)

from E5Gui.E5ZoomWidget import E5ZoomWidget
from E5Gui import E5MessageBox, E5FileDialog
from E5Gui.E5Application import e5App

from .Ui_MicroPythonReplWidget import Ui_MicroPythonReplWidget

from . import MicroPythonDevices
try:
    from .MicroPythonGraphWidget import MicroPythonGraphWidget
    HAS_QTCHART = True
except ImportError:
    HAS_QTCHART = False
from .MicroPythonFileManagerWidget import MicroPythonFileManagerWidget
try:
    from .MicroPythonCommandsInterface import MicroPythonCommandsInterface
    HAS_QTSERIALPORT = True
except ImportError:
    HAS_QTSERIALPORT = False

import Globals
import UI.PixmapCache
import Preferences

# ANSI Colors (see https://en.wikipedia.org/wiki/ANSI_escape_code)
AnsiColorSchemes = {
    "Windows 7": {
        0: QBrush(QColor(0, 0, 0)),
        1: QBrush(QColor(128, 0, 0)),
        2: QBrush(QColor(0, 128, 0)),
        3: QBrush(QColor(128, 128, 0)),
        4: QBrush(QColor(0, 0, 128)),
        5: QBrush(QColor(128, 0, 128)),
        6: QBrush(QColor(0, 128, 128)),
        7: QBrush(QColor(192, 192, 192)),
        10: QBrush(QColor(128, 128, 128)),
        11: QBrush(QColor(255, 0, 0)),
        12: QBrush(QColor(0, 255, 0)),
        13: QBrush(QColor(255, 255, 0)),
        14: QBrush(QColor(0, 0, 255)),
        15: QBrush(QColor(255, 0, 255)),
        16: QBrush(QColor(0, 255, 255)),
        17: QBrush(QColor(255, 255, 255)),
    },
    "Windows 10": {
        0: QBrush(QColor(12, 12, 12)),
        1: QBrush(QColor(197, 15, 31)),
        2: QBrush(QColor(19, 161, 14)),
        3: QBrush(QColor(193, 156, 0)),
        4: QBrush(QColor(0, 55, 218)),
        5: QBrush(QColor(136, 23, 152)),
        6: QBrush(QColor(58, 150, 221)),
        7: QBrush(QColor(204, 204, 204)),
        10: QBrush(QColor(118, 118, 118)),
        11: QBrush(QColor(231, 72, 86)),
        12: QBrush(QColor(22, 198, 12)),
        13: QBrush(QColor(249, 241, 165)),
        14: QBrush(QColor(59, 12, 255)),
        15: QBrush(QColor(180, 0, 158)),
        16: QBrush(QColor(97, 214, 214)),
        17: QBrush(QColor(242, 242, 242)),
    },
    "PuTTY": {
        0: QBrush(QColor(0, 0, 0)),
        1: QBrush(QColor(187, 0, 0)),
        2: QBrush(QColor(0, 187, 0)),
        3: QBrush(QColor(187, 187, 0)),
        4: QBrush(QColor(0, 0, 187)),
        5: QBrush(QColor(187, 0, 187)),
        6: QBrush(QColor(0, 187, 187)),
        7: QBrush(QColor(187, 187, 187)),
        10: QBrush(QColor(85, 85, 85)),
        11: QBrush(QColor(255, 85, 85)),
        12: QBrush(QColor(85, 255, 85)),
        13: QBrush(QColor(255, 255, 85)),
        14: QBrush(QColor(85, 85, 255)),
        15: QBrush(QColor(255, 85, 255)),
        16: QBrush(QColor(85, 255, 255)),
        17: QBrush(QColor(255, 255, 255)),
    },
    "xterm": {
        0: QBrush(QColor(0, 0, 0)),
        1: QBrush(QColor(205, 0, 0)),
        2: QBrush(QColor(0, 205, 0)),
        3: QBrush(QColor(205, 205, 0)),
        4: QBrush(QColor(0, 0, 238)),
        5: QBrush(QColor(205, 0, 205)),
        6: QBrush(QColor(0, 205, 205)),
        7: QBrush(QColor(229, 229, 229)),
        10: QBrush(QColor(127, 127, 127)),
        11: QBrush(QColor(255, 0, 0)),
        12: QBrush(QColor(0, 255, 0)),
        13: QBrush(QColor(255, 255, 0)),
        14: QBrush(QColor(0, 0, 255)),
        15: QBrush(QColor(255, 0, 255)),
        16: QBrush(QColor(0, 255, 255)),
        17: QBrush(QColor(255, 255, 255)),
    },
    "Ubuntu": {
        0: QBrush(QColor(1, 1, 1)),
        1: QBrush(QColor(222, 56, 43)),
        2: QBrush(QColor(57, 181, 74)),
        3: QBrush(QColor(255, 199, 6)),
        4: QBrush(QColor(0, 11, 184)),
        5: QBrush(QColor(118, 38, 113)),
        6: QBrush(QColor(44, 181, 233)),
        7: QBrush(QColor(204, 204, 204)),
        10: QBrush(QColor(128, 128, 128)),
        11: QBrush(QColor(255, 0, 0)),
        12: QBrush(QColor(0, 255, 0)),
        13: QBrush(QColor(255, 255, 0)),
        14: QBrush(QColor(0, 0, 255)),
        15: QBrush(QColor(255, 0, 255)),
        16: QBrush(QColor(0, 255, 255)),
        17: QBrush(QColor(255, 255, 255)),
    },
}


# TODO: make wrapping an option for the repl edit
# TODO: add a connect button or make the disconnect button with changing icon (see IRC)
class MicroPythonReplWidget(QWidget, Ui_MicroPythonReplWidget):
    """
    Class implementing the MicroPython REPL widget.
    
    @signal dataReceived(data) emitted to send data received via the serial
        connection for further processing
    """
    ZoomMin = -10
    ZoomMax = 20
    
    DeviceTypeRole = Qt.UserRole
    DevicePortRole = Qt.UserRole + 1
    
    dataReceived = pyqtSignal(bytes)
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(MicroPythonReplWidget, self).__init__(parent)
        self.setupUi(self)
        
        self.__ui = parent
        
        self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
            "", False))
        
        self.openButton.setIcon(UI.PixmapCache.getIcon("open"))
        self.saveButton.setIcon(UI.PixmapCache.getIcon("fileSaveAs"))
        
        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.__fileManagerWidget = None
        
        self.__interface = MicroPythonCommandsInterface(self)
        self.__device = None
        self.__connected = False
        self.setConnected(False)
        
        # TODO: replace these by checking the button states
        self.__replRunning = False
        self.__plotterRunning = False
        self.__fileManagerRunning = 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*)(?P<color>(?:;?\d*)*)(?P<action>[ABCDKm])')
        
        self.__populateDeviceTypeComboBox()
        
        self.replEdit.installEventFilter(self)
        
        self.replEdit.customContextMenuRequested.connect(
            self.__showContextMenu)
        self.__ui.preferencesChanged.connect(self.__handlePreferencesChanged)
        self.__ui.preferencesChanged.connect(
            self.__interface.handlePreferencesChanged)
        
        self.__handlePreferencesChanged()
        
        charFormat = self.replEdit.currentCharFormat()
        self.DefaultForeground = charFormat.foreground()
        self.DefaultBackground = charFormat.background()
    
    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)))
            
            index = 0
            for device in sorted(devices):
                index += 1
                self.deviceTypeComboBox.addItem(
                    self.tr("{0} at {1}".format(device[1], device[2])))
                self.deviceTypeComboBox.setItemData(
                    index, device[0], self.DeviceTypeRole)
                self.deviceTypeComboBox.setItemData(
                    index, device[2], self.DevicePortRole)
                
        else:
            self.deviceInfoLabel.setText(
                self.tr("No supported devices detected."))
        
        self.on_deviceTypeComboBox_activated(0)
    
    def __handlePreferencesChanged(self):
        """
        Private slot to handle a change in preferences.
        """
        self.__colorScheme = Preferences.getMicroPython("ColorScheme")
        
        self.__font = Preferences.getEditorOtherFonts("MonospacedFont")
        
        self.replEdit.setFontFamily(self.__font.family())
        self.replEdit.setFontPointSize(self.__font.pointSize())
    
    def commandsInterface(self):
        """
        Public method to get a reference to the commands interface object.
        
        @return reference to the commands interface object
        @rtype MicroPythonCommandsInterface
        """
        return self.__interface
    
    @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.DeviceTypeRole)
        self.deviceIconLabel.setPixmap(MicroPythonDevices.getDeviceIcon(
            deviceType, False))
        
        self.__device = MicroPythonDevices.getDevice(deviceType, self)
        self.__device.setButtons()
    
    @pyqtSlot()
    def on_checkButton_clicked(self):
        """
        Private slot to check for connected devices.
        """
        self.__populateDeviceTypeComboBox()
    
    def setActionButtons(self, **kwargs):
        """
        Public method to set the enabled state of the various action buttons.
        
        @keyparam kwargs keyword arguments containg the enabled states (keys
            are 'run', 'repl', 'files', 'chart', 'open', 'save'
        @type dict
        """
        if "open" in kwargs:
            self.openButton.setEnabled(kwargs["open"])
        if "save" in kwargs:
            self.saveButton.setEnabled(kwargs["save"])
        if "run" in kwargs:
            self.runButton.setEnabled(kwargs["run"])
        if "repl" in kwargs:
            self.replButton.setEnabled(kwargs["repl"])
        if "files" in kwargs:
            self.filesButton.setEnabled(kwargs["files"])
        if "chart" in kwargs:
            self.chartButton.setEnabled(kwargs["chart"])
    
    @pyqtSlot(QPoint)
    def __showContextMenu(self, pos):
        """
        Private 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("Clear"), self.__clear)
        menu.addSeparator()
        menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
        menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
        menu.addSeparator()
        if self.__device is not None:
            # allow device interface to add specific context menu entries
            self.__device.addActions(menu)
        menu.exec_(self.replEdit.mapToGlobal(pos))
    
    def setConnected(self, connected):
        """
        Public method to set the connection status LED.
        
        @param connected connection state
        @type bool
        """
        self.__connected = connected
        
        self.deviceConnectedLed.setOn(connected)
        if self.__fileManagerWidget:
            self.__fileManagerWidget.deviceConnectedLed.setOn(connected)
        
        self.deviceTypeComboBox.setEnabled(not connected)
        
        self.disconnectButton.setEnabled(connected)
    
    def __showNoDeviceMessage(self):
        """
        Private method to show a message dialog indicating a missing device.
        """
        E5MessageBox.critical(
            self,
            self.tr("No device attached"),
            self.tr("""Please ensure the device is plugged into your"""
                    """ computer and selected.\n\nIt must have a version"""
                    """ of MicroPython (or CircuitPython) flashed onto"""
                    """ it before anything will work.\n\nFinally press"""
                    """ the device's reset button and wait a few seconds"""
                    """ before trying again."""))
    
    @pyqtSlot(bool)
    def on_replButton_clicked(self, checked):
        """
        Private slot to connect to enable or disable the REPL widget connecting
        or disconnecting from the device.
        
        @param checked state of the button
        @type bool
        """
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        if checked:
            ok, reason = self.__device.canStartRepl()
            if not ok:
                E5MessageBox.warning(
                    self,
                    self.tr("Start REPL"),
                    self.tr("""<p>The REPL cannot be started.</p><p>Reason:"""
                            """ {0}</p>""").format(reason))
                return
            
            self.replEdit.clear()
            self.__interface.dataReceived.connect(self.__processData)
            
            if not self.__interface.isConnected():
                self.__connectToDevice()
                if self.__device.forceInterrupt():
                    # send a Ctrl-B (exit raw mode)
                    self.__interface.write(b'\x02')
                    # send Ctrl-C (keyboard interrupt)
                    self.__interface.write(b'\x03')
            
            self.__replRunning = True
            self.__device.setRepl(True)
            self.replEdit.setFocus(Qt.OtherFocusReason)
        else:
            self.__interface.dataReceived.disconnect(self.__processData)
            if not self.__plotterRunning and not self.__fileManagerRunning:
                self.__disconnectFromDevice()
            self.__replRunning = False
            self.__device.setRepl(False)
        self.replButton.setChecked(checked)
    
    @pyqtSlot()
    def on_disconnectButton_clicked(self):
        """
        Private slot to disconnect from the currently connected device.
        """
        self.__disconnectFromDevice()
    
    @pyqtSlot()
    def __clear(self):
        """
        Private slot to clear the REPL pane.
        """
        self.replEdit.clear()
        self.__interface.isConnected() and self.__interface.write(b"\r")
    
    @pyqtSlot()
    def __paste(self):
        """
        Private slot to perform a paste operation.
        """
        clipboard = QApplication.clipboard()
        if clipboard:
            pasteText = clipboard.text()
            if pasteText:
                pasteText = pasteText.replace('\n\r', '\r')
                pasteText = pasteText.replace('\n', '\r')
                self.__interface.isConnected() and self.__interface.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''
            elif key in (Qt.Key_Return, Qt.Key_Enter):
                tc = self.replEdit.textCursor()
                tc.movePosition(QTextCursor.EndOfLine)
                self.replEdit.setTextCursor(tc)
            self.__interface.isConnected() and self.__interface.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
        
        # set the font
        charFormat = tc.charFormat()
        charFormat.setFontFamily(self.__font.family())
        charFormat.setFontPointSize(self.__font.pointSize())
        tc.setCharFormat(charFormat)
        
        index = 0
        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:].decode("utf-8"))
                if match:
                    # move to last position in control sequence
                    # ++ will be done at end of loop
                    index += match.end() - 1
                    
                    action = match.group("action")
                    if action in "ABCD":
                        if match.group("count") == "":
                            count = 1
                        else:
                            count = int(match.group("count"))
                        
                        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") in ("", "0"):
                            # delete to end of line
                            tc.movePosition(QTextCursor.EndOfLine,
                                            mode=QTextCursor.KeepAnchor)
                            tc.removeSelectedText()
                            self.replEdit.setTextCursor(tc)
                        elif match.group("count") == "1":
                            # delete to beinning of line
                            tc.movePosition(QTextCursor.StartOfLine,
                                            mode=QTextCursor.KeepAnchor)
                            tc.removeSelectedText()
                            self.replEdit.setTextCursor(tc)
                        elif match.group("count") == "2":
                            # delete whole line
                            tc.movePosition(QTextCursor.EndOfLine)
                            tc.movePosition(QTextCursor.StartOfLine,
                                            mode=QTextCursor.KeepAnchor)
                            tc.removeSelectedText()
                            self.replEdit.setTextCursor(tc)
                    elif action == "m":
                        self.__setCharFormat(match.group(0)[:-1].split(";"),
                                             tc)
            else:
                tc.deleteChar()
                self.replEdit.setTextCursor(tc)
                self.replEdit.insertPlainText(chr(data[index]))
            
            index += 1
        
        self.replEdit.ensureCursorVisible()
    
    def __setCharFormat(self, formatCodes, textCursor):
        """
        Private method setting the current text format of the REPL pane based
        on the passed ANSI codes.
        
        Following codes are used:
        <ul>
        <li>0: Reset</li>
        <li>1: Bold font (weight 75)</li>
        <li>2: Light font (weight 25)</li>
        <li>3: Italic font</li>
        <li>4: Underlined font</li>
        <li>9: Strikeout font</li>
        <li>21: Bold off (weight 50)</li>
        <li>22: Light off (weight 50)</li>
        <li>23: Italic off</li>
        <li>24: Underline off</li>
        <li>29: Strikeout off</li>
        <li>30: foreground Black</li>
        <li>31: foreground Dark Red</li>
        <li>32: foreground Dark Green</li>
        <li>33: foreground Dark Yellow</li>
        <li>34: foreground Dark Blue</li>
        <li>35: foreground Dark Magenta</li>
        <li>36: foreground Dark Cyan</li>
        <li>37: foreground Light Gray</li>
        <li>39: reset foreground to default</li>
        <li>40: background Black</li>
        <li>41: background Dark Red</li>
        <li>42: background Dark Green</li>
        <li>43: background Dark Yellow</li>
        <li>44: background Dark Blue</li>
        <li>45: background Dark Magenta</li>
        <li>46: background Dark Cyan</li>
        <li>47: background Light Gray</li>
        <li>49: reset background to default</li>
        <li>53: Overlined font</li>
        <li>55: Overline off</li>
        <li>90: bright foreground Dark Gray</li>
        <li>91: bright foreground Red</li>
        <li>92: bright foreground Green</li>
        <li>93: bright foreground Yellow</li>
        <li>94: bright foreground Blue</li>
        <li>95: bright foreground Magenta</li>
        <li>96: bright foreground Cyan</li>
        <li>97: bright foreground White</li>
        <li>100: bright background Dark Gray</li>
        <li>101: bright background Red</li>
        <li>102: bright background Green</li>
        <li>103: bright background Yellow</li>
        <li>104: bright background Blue</li>
        <li>105: bright background Magenta</li>
        <li>106: bright background Cyan</li>
        <li>107: bright background White</li>
        </ul>
        
        @param formatCodes list of format codes
        @type list of str
        @param textCursor reference to the text cursor
        @type QTextCursor
        """
        if not formatCodes:
            # empty format codes list is treated as a reset
            formatCodes = ["0"]
        
        charFormat = textCursor.charFormat()
        for formatCode in formatCodes:
            try:
                formatCode = int(formatCode)
            except ValueError:
                # ignore non digit values
                continue
            
            if formatCode == 0:
                charFormat.setFontWeight(50)
                charFormat.setFontItalic(False)
                charFormat.setFontUnderline(False)
                charFormat.setFontStrikeOut(False)
                charFormat.setFontOverline(False)
                charFormat.setForeground(self.DefaultForeground)
                charFormat.setBackground(self.DefaultBackground)
            elif formatCode == 1:
                charFormat.setFontWeight(75)
            elif formatCode == 2:
                charFormat.setFontWeight(25)
            elif formatCode == 3:
                charFormat.setFontItalic(True)
            elif formatCode == 4:
                charFormat.setFontUnderline(True)
            elif formatCode == 9:
                charFormat.setFontStrikeOut(True)
            elif formatCode in (21, 22):
                charFormat.setFontWeight(50)
            elif formatCode == 23:
                charFormat.setFontItalic(False)
            elif formatCode == 24:
                charFormat.setFontUnderline(False)
            elif formatCode == 29:
                charFormat.setFontStrikeOut(False)
            elif formatCode == 53:
                charFormat.setFontOverline(True)
            elif formatCode == 55:
                charFormat.setFontOverline(False)
            elif formatCode in (30, 31, 32, 33, 34, 35, 36, 37):
                charFormat.setForeground(
                    AnsiColorSchemes[self.__colorScheme][formatCode - 30])
            elif formatCode in (40, 41, 42, 43, 44, 45, 46, 47):
                charFormat.setBackground(
                    AnsiColorSchemes[self.__colorScheme][formatCode - 40])
            elif formatCode in (90, 91, 92, 93, 94, 95, 96, 97):
                charFormat.setForeground(
                    AnsiColorSchemes[self.__colorScheme][formatCode - 80])
            elif formatCode in (100, 101, 102, 103, 104, 105, 106, 107):
                charFormat.setBackground(
                    AnsiColorSchemes[self.__colorScheme][formatCode - 90])
            elif formatCode == 39:
                charFormat.setForeground(self.DefaultForeground)
            elif formatCode == 49:
                charFormat.setBackground(self.DefaultBackground)
        
        textCursor.setCharFormat(charFormat)
    
    def __doZoom(self, value):
        """
        Private slot to zoom the REPL pane.
        
        @param value zoom value
        @type int
        """
        if value < self.__currentZoom:
            self.replEdit.zoomOut(self.__currentZoom - value)
        elif value > self.__currentZoom:
            self.replEdit.zoomIn(value - self.__currentZoom)
        self.__currentZoom = value
    
    def __getCurrentPort(self):
        """
        Private method to determine the port path of the selected device.
        
        @return path of the port of the selected device
        @rtype str
        """
        portName = self.deviceTypeComboBox.itemData(
            self.deviceTypeComboBox.currentIndex(),
            self.DevicePortRole)
        
        if Globals.isWindowsPlatform():
            # return it unchanged
            return portName
        else:
            # return with device path prepended
            return "/dev/{0}".format(portName)
    
    def __connectToDevice(self):
        """
        Private method to connect to the selected device.
        """
        port = self.__getCurrentPort()
        if self.__interface.connectToDevice(port):
            self.setConnected(True)
        else:
            E5MessageBox.warning(
                self,
                self.tr("Serial Device Connect"),
                self.tr("""<p>Cannot connect to device at serial port"""
                        """ <b>{0}</b>.</p>""").format(port))
    
    def __disconnectFromDevice(self):
        """
        Private method to disconnect from the device.
        """
        self.__interface.disconnectFromDevice()
        self.setConnected(False)
    
    @pyqtSlot()
    def on_runButton_clicked(self):
        """
        Private slot to execute the script of the active editor on the
        selected device.
        """
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        aw = e5App().getObject("ViewManager").activeWindow()
        if aw is None:
            E5MessageBox.critical(
                self,
                self.tr("Run Script"),
                self.tr("""There is no editor open. Abort..."""))
            return
        
        script = aw.text()
        if not script:
            E5MessageBox.critical(
                self,
                self.tr("Run Script"),
                self.tr("""The current editor does not contain a script."""
                        """ Abort..."""))
            return
        
        ok, reason = self.__device.canRunScript()
        if not ok:
            E5MessageBox.warning(
                self,
                self.tr("Run Script"),
                self.tr("""<p>Cannot run script.</p><p>Reason:"""
                        """ {0}</p>""").format(reason))
            return
        
        if not self.__replRunning:
            # switch on the REPL
            self.on_replButton_clicked(True)
        if self.__replRunning:
            self.__device.runScript(script)
    
    @pyqtSlot()
    def on_openButton_clicked(self):
        """
        Private slot to open a file of the connected device.
        """
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        workspace = self.__device.getWorkspace()
        fileName = E5FileDialog.getOpenFileName(
            self,
            self.tr("Open Python File"),
            workspace,
            self.tr("Python3 Files (*.py);;All Files (*)"))
        if fileName:
            e5App().getObject("ViewManager").openSourceFile(fileName)
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to save the current editor to the connected device.
        """
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        workspace = self.__device.getWorkspace()
        aw = e5App().getObject("ViewManager").activeWindow()
        if aw:
            aw.saveFileAs(workspace)
    
    @pyqtSlot(bool)
    def on_chartButton_clicked(self, checked):
        """
        Private slot to open a chart view to plot data received from the
        connected device.
        
        @param checked state of the button
        @type bool
        """
        if not HAS_QTCHART:
            # QtChart not available => fail silently
            return
        
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        if checked:
            ok, reason = self.__device.canStartPlotter()
            if not ok:
                E5MessageBox.warning(
                    self,
                    self.tr("Start Chart"),
                    self.tr("""<p>The Chart cannot be started.</p><p>Reason:"""
                            """ {0}</p>""").format(reason))
                return
            
            self.__chartWidget = MicroPythonGraphWidget(self)
            self.__interface.dataReceived.connect(
                self.__chartWidget.processData)
            self.__chartWidget.dataFlood.connect(
                self.handleDataFlood)
            
            self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget,
                                    UI.PixmapCache.getIcon("chart"),
                                    self.tr("μPy Chart"))
            self.__ui.showSideWidget(self.__chartWidget)
            
            if not self.__interface.isConnected():
                self.__connectToDevice()
                if self.__device.forceInterrupt():
                    # send a Ctrl-B (exit raw mode)
                    self.__interface.write(b'\x02')
                    # send Ctrl-C (keyboard interrupt)
                    self.__interface.write(b'\x03')
            
            self.__plotterRunning = True
            self.__device.setPlotter(True)
        else:
            if self.__chartWidget.isDirty():
                res = E5MessageBox.okToClearData(
                    self,
                    self.tr("Unsaved Chart Data"),
                    self.tr("""The chart contains unsaved data."""),
                    self.__chartWidget.saveData)
                if not res:
                    # abort
                    return
            
            self.__interface.dataReceived.disconnect(
                self.__chartWidget.processData)
            self.__chartWidget.dataFlood.disconnect(
                self.handleDataFlood)
            
            if not self.__replRunning and not self.__fileManagerRunning:
                self.__disconnectFromDevice()
            
            self.__plotterRunning = False
            self.__device.setPlotter(False)
            self.__ui.removeSideWidget(self.__chartWidget)
            
            self.__chartWidget.deleteLater()
            self.__chartWidget = None
        
        self.chartButton.setChecked(checked)
    
    @pyqtSlot()
    def handleDataFlood(self):
        """
        Public slot handling a data flood from the device.
        """
        self.on_disconnectButton_clicked()
        self.__device.handleDataFlood()
    
    @pyqtSlot(bool)
    def on_filesButton_clicked(self, checked):
        """
        Private slot to open a file manager window to the connected device.
        
        @param checked state of the button
        @type bool
        """
        if not self.__device:
            self.__showNoDeviceMessage()
            return
        
        if checked:
            ok, reason = self.__device.canStartFileManager()
            if not ok:
                E5MessageBox.warning(
                    self,
                    self.tr("Start File Manager"),
                    self.tr("""<p>The File Manager cannot be started.</p>"""
                            """<p>Reason: {0}</p>""").format(reason))
                return
            
            if not self.__interface.isConnected():
                self.__connectToDevice()
            self.__fileManagerWidget = MicroPythonFileManagerWidget(
                self.__interface, self)
            
            self.__ui.addSideWidget(self.__ui.BottomSide,
                                    self.__fileManagerWidget,
                                    UI.PixmapCache.getIcon("filemanager"),
                                    self.tr("μPy Files"))
            self.__ui.showSideWidget(self.__fileManagerWidget)

            self.__device.setFileManager(True)
            self.__fileManagerRunning = True
            
            self.__fileManagerWidget.start()
        else:
            self.__fileManagerWidget.stop()
            self.__ui.removeSideWidget(self.__fileManagerWidget)
            
            self.__device.setFileManager(False)
            self.__fileManagerRunning = False
            self.__fileManagerWidget.deleteLater()
            self.__fileManagerWidget = None

eric ide

mercurial