Tue, 20 Aug 2019 17:07:06 +0200
Merged with the 'micropython' branch to add widgets to support development for embedded controllers with MicroPython (ESP8266/ESP32, CircuitPython and BBC micro:bit).
docs/changelog | file | annotate | diff | comparison | revisions |
--- a/docs/changelog Tue Aug 20 00:37:14 2019 +0300 +++ b/docs/changelog Tue Aug 20 17:07:06 2019 +0200 @@ -1,5 +1,10 @@ Change Log ---------- +Version 19.9: +- bug fixes +- added widgets to support development for embedded controllers with + MicroPython (ESP8266/ESP32, CircuitPython and BBC micro:bit) + Version 19.8: - bug fixes - Third Party packages
--- a/eric6.e4p Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6.e4p Tue Aug 20 17:07:06 2019 +0200 @@ -129,6 +129,7 @@ <Source>eric6/E5Gui/E5ErrorMessage.py</Source> <Source>eric6/E5Gui/E5ErrorMessageFilterDialog.py</Source> <Source>eric6/E5Gui/E5FileDialog.py</Source> + <Source>eric6/E5Gui/E5FileSaveConfirmDialog.py</Source> <Source>eric6/E5Gui/E5GenericDiffHighlighter.py</Source> <Source>eric6/E5Gui/E5Led.py</Source> <Source>eric6/E5Gui/E5LineEdit.py</Source> @@ -144,6 +145,7 @@ <Source>eric6/E5Gui/E5PasswordMeter.py</Source> <Source>eric6/E5Gui/E5PathPicker.py</Source> <Source>eric6/E5Gui/E5PathPickerDialog.py</Source> + <Source>eric6/E5Gui/E5ProcessDialog.py</Source> <Source>eric6/E5Gui/E5ProgressDialog.py</Source> <Source>eric6/E5Gui/E5SideBar.py</Source> <Source>eric6/E5Gui/E5SimpleHelpDialog.py</Source> @@ -454,6 +456,21 @@ <Source>eric6/IconEditor/__init__.py</Source> <Source>eric6/IconEditor/cursors/__init__.py</Source> <Source>eric6/IconEditor/cursors/cursors_rc.py</Source> + <Source>eric6/MicroPython/CircuitPythonDevices.py</Source> + <Source>eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.py</Source> + <Source>eric6/MicroPython/EspDevices.py</Source> + <Source>eric6/MicroPython/EspFirmwareSelectionDialog.py</Source> + <Source>eric6/MicroPython/MicroPythonCommandsInterface.py</Source> + <Source>eric6/MicroPython/MicroPythonDevices.py</Source> + <Source>eric6/MicroPython/MicroPythonFileManager.py</Source> + <Source>eric6/MicroPython/MicroPythonFileManagerWidget.py</Source> + <Source>eric6/MicroPython/MicroPythonFileSystemUtilities.py</Source> + <Source>eric6/MicroPython/MicroPythonGraphWidget.py</Source> + <Source>eric6/MicroPython/MicroPythonProgressInfoDialog.py</Source> + <Source>eric6/MicroPython/MicroPythonSerialPort.py</Source> + <Source>eric6/MicroPython/MicroPythonWidget.py</Source> + <Source>eric6/MicroPython/MicrobitDevices.py</Source> + <Source>eric6/MicroPython/__init__.py</Source> <Source>eric6/MultiProject/AddProjectDialog.py</Source> <Source>eric6/MultiProject/MultiProject.py</Source> <Source>eric6/MultiProject/MultiProjectBrowser.py</Source> @@ -890,6 +907,7 @@ <Source>eric6/Preferences/ConfigurationPages/IrcPage.py</Source> <Source>eric6/Preferences/ConfigurationPages/LogViewerPage.py</Source> <Source>eric6/Preferences/ConfigurationPages/MasterPasswordEntryDialog.py</Source> + <Source>eric6/Preferences/ConfigurationPages/MicroPythonPage.py</Source> <Source>eric6/Preferences/ConfigurationPages/MimeTypesPage.py</Source> <Source>eric6/Preferences/ConfigurationPages/MultiProjectPage.py</Source> <Source>eric6/Preferences/ConfigurationPages/NetworkPage.py</Source> @@ -1762,6 +1780,7 @@ <Form>eric6/Debugger/VariablesFilterDialog.ui</Form> <Form>eric6/E5Gui/E5ErrorMessageFilterDialog.ui</Form> <Form>eric6/E5Gui/E5ListSelectionDialog.ui</Form> + <Form>eric6/E5Gui/E5ProcessDialog.ui</Form> <Form>eric6/E5Gui/E5SimpleHelpDialog.ui</Form> <Form>eric6/E5Gui/E5StringListEditWidget.ui</Form> <Form>eric6/E5Gui/E5ToolBarDialog.ui</Form> @@ -1832,6 +1851,11 @@ <Form>eric6/HexEdit/HexEditReplaceWidget.ui</Form> <Form>eric6/HexEdit/HexEditSearchWidget.ui</Form> <Form>eric6/IconEditor/IconSizeDialog.ui</Form> + <Form>eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.ui</Form> + <Form>eric6/MicroPython/EspFirmwareSelectionDialog.ui</Form> + <Form>eric6/MicroPython/MicroPythonFileManagerWidget.ui</Form> + <Form>eric6/MicroPython/MicroPythonProgressInfoDialog.ui</Form> + <Form>eric6/MicroPython/MicroPythonWidget.ui</Form> <Form>eric6/MultiProject/AddProjectDialog.ui</Form> <Form>eric6/MultiProject/PropertiesDialog.ui</Form> <Form>eric6/Network/IRC/IrcChannelEditDialog.ui</Form> @@ -2084,6 +2108,7 @@ <Form>eric6/Preferences/ConfigurationPages/IrcPage.ui</Form> <Form>eric6/Preferences/ConfigurationPages/LogViewerPage.ui</Form> <Form>eric6/Preferences/ConfigurationPages/MasterPasswordEntryDialog.ui</Form> + <Form>eric6/Preferences/ConfigurationPages/MicroPythonPage.ui</Form> <Form>eric6/Preferences/ConfigurationPages/MimeTypesPage.ui</Form> <Form>eric6/Preferences/ConfigurationPages/MultiProjectPage.ui</Form> <Form>eric6/Preferences/ConfigurationPages/NetworkPage.ui</Form>
--- a/eric6/Debugger/DebugUI.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Debugger/DebugUI.py Tue Aug 20 17:07:06 2019 +0200 @@ -563,8 +563,7 @@ act = E5Action( self.tr('Clear Breakpoints'), self.tr('Clear Breakpoints'), - QKeySequence( - self.tr("Ctrl+Shift+C", "Debug|Clear Breakpoints")), 0, + 0, 0, self.dbgSetBpActGrp, 'dbg_clear_breakpoint') act.setStatusTip(self.tr('Clear Breakpoints')) act.setWhatsThis(self.tr(
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/E5Gui/E5FileSaveConfirmDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2018 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter a file system path using a file picker. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel + +from .E5PathPicker import E5PathPicker, E5PathPickerModes +from .E5LineEdit import E5ClearableLineEdit + + +class E5FileSaveConfirmDialog(QDialog): + """ + Class implementing a dialog to enter a file system path using a file + picker. + """ + def __init__(self, filename, title, message="", picker=True, parent=None): + """ + Constructor + + @param filename file name to be shown + @type str + @param title title for the dialog + @type str + @param message message to be shown + @type str + @param picker flag indicating to use a path picker + @type bool + @param parent reference to the parent widget + @type QWidget + """ + super(E5FileSaveConfirmDialog, self).__init__(parent) + + self.setMinimumWidth(400) + + self.__selectedAction = "cancel" + self.__filename = filename + + self.__layout = QVBoxLayout(self) + + self.__label = QLabel(self) + self.__label.setWordWrap(True) + if message: + self.__label.setText(message) + else: + self.__label.setText(self.tr("The given file exists already.")) + + if picker: + self.__pathPicker = E5PathPicker(self) + self.__pathPicker.setMode(E5PathPickerModes.SaveFileMode) + else: + self.__pathPicker = E5ClearableLineEdit(self) + + self.__buttonBox = QDialogButtonBox(self) + self.__cancelButton = self.__buttonBox.addButton( + QDialogButtonBox.Cancel) + self.__overwriteButton = self.__buttonBox.addButton( + self.tr("Overwrite"), QDialogButtonBox.AcceptRole) + self.__renameButton = self.__buttonBox.addButton( + self.tr("Rename"), QDialogButtonBox.AcceptRole) + + self.__layout.addWidget(self.__label) + self.__layout.addWidget(self.__pathPicker) + self.__layout.addWidget(self.__buttonBox) + + # set values and states + self.__pathPicker.setText(filename) + if picker: + self.__pathPicker.setDefaultDirectory(os.path.dirname(filename)) + self.__renameButton.setEnabled(False) + self.__cancelButton.setDefault(True) + + self.__buttonBox.clicked.connect(self.__buttonBoxClicked) + self.__pathPicker.textChanged.connect(self.__filenameChanged) + + def __buttonBoxClicked(self, button): + """ + Private slot to handle the user clicking a button. + + @param button reference to the clicked button + @type QAbstractButton + """ + if button == self.__cancelButton: + self.__selectedAction = "cancel" + self.reject() + elif button == self.__renameButton: + self.__selectedAction = "rename" + self.accept() + elif button == self.__overwriteButton: + self.__selectedAction = "overwrite" + self.accept() + + def __filenameChanged(self, text): + """ + Private slot to handle a change of the file name. + + @param text new file name + @type str + """ + self.__renameButton.setEnabled(text != self.__filename) + + def selectedAction(self): + """ + Public method to get the selected action and associated data. + + @return tuple containing the selected action (cancel, rename, + overwrite) and the filename (in case of 'rename' or 'overwrite') + @rtype tuple of (str, str) + """ + if self.__selectedAction == "rename": + filename = self.__pathPicker.text() + elif self.__selectedAction == "overwrite": + filename = self.__filename + else: + filename = "" + return self.__selectedAction, filename + + +def confirmOverwrite(filename, title, message="", picker=True, parent=None): + """ + Function to confirm that a file shall be overwritten. + + @param filename file name to be shown + @type str + @param title title for the dialog + @type str + @param message message to be shown + @type str + @param picker flag indicating to use a path picker + @type bool + @param parent reference to the parent widget + @type QWidget + @return tuple containing the selected action (cancel, rename, + overwrite) and the filename (in case of 'rename' or 'overwrite') + @rtype tuple of (str, str) + """ + dlg = E5FileSaveConfirmDialog(filename, title, message=message, + picker=picker, parent=parent) + dlg.exec_() + return dlg.selectedAction()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/E5Gui/E5ProcessDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog starting a process and showing its output. +""" + +from __future__ import unicode_literals +try: + str = unicode +except NameError: + pass + +import os + +from PyQt5.QtCore import QProcess, QTimer, pyqtSlot, Qt, QCoreApplication, \ + QProcessEnvironment +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QLineEdit + +from E5Gui import E5MessageBox + +from .Ui_E5ProcessDialog import Ui_E5ProcessDialog + +from Globals import strToQByteArray +import Preferences + + +class E5ProcessDialog(QDialog, Ui_E5ProcessDialog): + """ + Class implementing a dialog starting a process and showing its output. + + It starts a QProcess and displays a dialog that + shows the output of the process. The dialog is modal, + which causes a synchronized execution of the process. + """ + def __init__(self, outputTitle="", windowTitle="", parent=None): + """ + Constructor + + @param outputTitle title for the output group + @type str + @param windowTitle title of the dialog + @type str + @param parent reference to the parent widget + @type QWidget + """ + super(E5ProcessDialog, self).__init__(parent) + self.setupUi(self) + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) + + if windowTitle: + self.setWindowTitle(windowTitle) + if outputTitle: + self.outputGroup.setTitle(outputTitle) + + self.__process = None + + self.show() + QCoreApplication.processEvents() + + def __finish(self): + """ + Private slot called when the process finished or the user pressed + the button. + """ + if self.__process is not None and \ + self.__process.state() != QProcess.NotRunning: + self.__process.terminate() + QTimer.singleShot(2000, self.__process.kill) + self.__process.waitForFinished(3000) + + self.inputGroup.setEnabled(False) + self.inputGroup.hide() + + self.__process = None + + self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) + self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) + self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) + self.buttonBox.button(QDialogButtonBox.Close).setFocus( + Qt.OtherFocusReason) + + def on_buttonBox_clicked(self, button): + """ + Private slot called by a button of the button box clicked. + + @param button button that was clicked + @type QAbstractButton + """ + if button == self.buttonBox.button(QDialogButtonBox.Close): + self.close() + elif button == self.buttonBox.button(QDialogButtonBox.Cancel): + self.statusLabel.setText(self.tr("Process canceled.")) + self.__finish() + + def __procFinished(self, exitCode, exitStatus): + """ + Private slot connected to the finished signal. + + @param exitCode exit code of the process + @type int + @param exitStatus exit status of the process + @type QProcess.ExitStatus + """ + self.__normal = (exitStatus == QProcess.NormalExit) and (exitCode == 0) + if self.__normal: + self.statusLabel.setText(self.tr("Process finished successfully.")) + elif exitStatus == QProcess.CrashExit: + self.statusLabel.setText(self.tr("Process crashed.")) + else: + self.statusLabel.setText( + self.tr("Process finished with exit code {0}") + .format(exitCode)) + self.__finish() + + def startProcess(self, program, args, workingDir=None, showArgs=True, + environment=None): + """ + Public slot used to start the process. + + @param program path of the program to be executed + @type str + @param args list of arguments for the process + @type list of str + @param workingDir working directory for the process + @type str + @param showArgs flag indicating to show the arguments + @type bool + @param environment dictionary of environment settings to add + or change for the process + @type dict + @return flag indicating a successful start of the process + @rtype bool + """ + self.errorGroup.hide() + self.__normal = False + self.__intercept = False + + if environment is None: + environment = {} + + if showArgs: + self.resultbox.append(program + ' ' + ' '.join(args)) + self.resultbox.append('') + + self.__process = QProcess() + if environment: + env = QProcessEnvironment.systemEnvironment() + for key, value in environment.items(): + env.insert(key, value) + self.__process.setProcessEnvironment(env) + + self.__process.finished.connect(self.__procFinished) + self.__process.readyReadStandardOutput.connect(self.__readStdout) + self.__process.readyReadStandardError.connect(self.__readStderr) + + if workingDir: + self.__process.setWorkingDirectory(workingDir) + + self.__process.start(program, args) + procStarted = self.__process.waitForStarted(10000) + if not procStarted: + self.buttonBox.setFocus() + self.inputGroup.setEnabled(False) + E5MessageBox.critical( + self, + self.tr('Process Generation Error'), + self.tr( + '<p>The process <b>{0}</b> could not be started.</p>' + ).format(program)) + else: + self.inputGroup.setEnabled(True) + self.inputGroup.show() + + return procStarted + + def normalExit(self): + """ + Public method to check for a normal process termination. + + @return flag indicating normal process termination + @rtype bool + """ + return self.__normal + + def normalExitWithoutErrors(self): + """ + Public method to check for a normal process termination without + error messages. + + @return flag indicating normal process termination + @rtype bool + """ + return self.__normal and self.errors.toPlainText() == "" + + def __readStdout(self): + """ + Private slot to handle the readyReadStandardOutput signal. + + It reads the output of the process and inserts it into the + output pane. + """ + if self.__process is not None: + s = str(self.__process.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace') + self.resultbox.insertPlainText(s) + self.resultbox.ensureCursorVisible() + + QCoreApplication.processEvents() + + def __readStderr(self): + """ + Private slot to handle the readyReadStandardError signal. + + It reads the error output of the process and inserts it into the + error pane. + """ + if self.__process is not None: + s = str(self.__process.readAllStandardError(), + Preferences.getSystem("IOEncoding"), + 'replace') + + self.errorGroup.show() + self.errors.insertPlainText(s) + self.errors.ensureCursorVisible() + + QCoreApplication.processEvents() + + def on_passwordCheckBox_toggled(self, isOn): + """ + Private slot to handle the password checkbox toggled. + + @param isOn flag indicating the status of the check box + @type bool + """ + if isOn: + self.input.setEchoMode(QLineEdit.Password) + else: + self.input.setEchoMode(QLineEdit.Normal) + + @pyqtSlot() + def on_sendButton_clicked(self): + """ + Private slot to send the input to the git process. + """ + inputTxt = self.input.text() + inputTxt += os.linesep + + if self.passwordCheckBox.isChecked(): + self.errors.insertPlainText(os.linesep) + self.errors.ensureCursorVisible() + else: + self.errors.insertPlainText(inputTxt) + self.errors.ensureCursorVisible() + + self.__process.write(strToQByteArray(inputTxt)) + + self.passwordCheckBox.setChecked(False) + self.input.clear() + + def on_input_returnPressed(self): + """ + Private slot to handle the press of the return key in the input field. + """ + self.__intercept = True + self.on_sendButton_clicked() + + def keyPressEvent(self, evt): + """ + Protected slot to handle a key press event. + + @param evt the key press event (QKeyEvent) + """ + if self.__intercept: + self.__intercept = False + evt.accept() + return + + super(E5ProcessDialog, self).keyPressEvent(evt)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/E5Gui/E5ProcessDialog.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>E5ProcessDialog</class> + <widget class="QDialog" name="E5ProcessDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>593</width> + <height>499</height> + </rect> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout"> + <item> + <widget class="QGroupBox" name="outputGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>2</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Output</string> + </property> + <layout class="QVBoxLayout"> + <item> + <widget class="QTextEdit" name="resultbox"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="acceptRichText"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QLabel" name="statusLabel"/> + </item> + <item> + <widget class="QGroupBox" name="errorGroup"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>1</verstretch> + </sizepolicy> + </property> + <property name="title"> + <string>Errors</string> + </property> + <layout class="QVBoxLayout"> + <item> + <widget class="QTextEdit" name="errors"> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="acceptRichText"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="inputGroup"> + <property name="title"> + <string>Input</string> + </property> + <layout class="QGridLayout"> + <item row="1" column="1"> + <spacer> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeType"> + <enum>QSizePolicy::Expanding</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>327</width> + <height>29</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="sendButton"> + <property name="toolTip"> + <string>Press to send the input to the running process</string> + </property> + <property name="text"> + <string>&Send</string> + </property> + <property name="shortcut"> + <string>Alt+S</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="3"> + <widget class="E5ClearableLineEdit" name="input"> + <property name="toolTip"> + <string>Enter data to be sent to the running process</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QCheckBox" name="passwordCheckBox"> + <property name="toolTip"> + <string>Select to switch the input field to password mode</string> + </property> + <property name="text"> + <string>&Password Mode</string> + </property> + <property name="shortcut"> + <string>Alt+P</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <pixmapfunction>qPixmapFromMimeSource</pixmapfunction> + <customwidgets> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>resultbox</tabstop> + <tabstop>errors</tabstop> + <tabstop>input</tabstop> + <tabstop>passwordCheckBox</tabstop> + <tabstop>sendButton</tabstop> + <tabstop>buttonBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- a/eric6/Globals/__init__.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Globals/__init__.py Tue Aug 20 17:07:06 2019 +0200 @@ -446,19 +446,23 @@ """ if size < 1024: return QCoreApplication.translate( - "Globals", "{0:.1f} Bytes").format(size) + "Globals", "{0:4.2f} Bytes").format(size) elif size < 1024 * 1024: size /= 1024 return QCoreApplication.translate( - "Globals", "{0:.1f} KiB").format(size) + "Globals", "{0:4.2f} KiB").format(size) elif size < 1024 * 1024 * 1024: size /= 1024 * 1024 return QCoreApplication.translate( - "Globals", "{0:.2f} MiB").format(size) - else: + "Globals", "{0:4.2f} MiB").format(size) + elif size < 1024 * 1024 * 1024 * 1024: size /= 1024 * 1024 * 1024 return QCoreApplication.translate( - "Globals", "{0:.2f} GiB").format(size) + "Globals", "{0:4.2f} GiB").format(size) + else: + size /= 1024 * 1024 * 1024 * 1024 + return QCoreApplication.translate( + "Globals", "{0:4.2f} TiB").format(size) ###############################################################################
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/CircuitPythonDevices.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the device interface class for CircuitPython boards. +""" + +from __future__ import unicode_literals + +import shutil +import os + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox, E5FileDialog + +from .MicroPythonDevices import MicroPythonDevice +from .MicroPythonWidget import HAS_QTCHART + +import Utilities + + +class CircuitPythonDevice(MicroPythonDevice): + """ + Class implementing the device for CircuitPython boards. + """ + DeviceVolumeName = "CIRCUITPY" + + def __init__(self, microPythonWidget, parent=None): + """ + Constructor + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param parent reference to the parent object + @type QObject + """ + super(CircuitPythonDevice, self).__init__(microPythonWidget, parent) + + def setButtons(self): + """ + Public method to enable the supported action buttons. + """ + super(CircuitPythonDevice, self).setButtons() + self.microPython.setActionButtons( + run=True, repl=True, files=True, chart=HAS_QTCHART) + + if self.__deviceVolumeMounted(): + self.microPython.setActionButtons(open=True, save=True) + + def forceInterrupt(self): + """ + Public method to determine the need for an interrupt when opening the + serial connection. + + @return flag indicating an interrupt is needed + @rtype bool + """ + return False + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("CircuitPython") + + def canStartRepl(self): + """ + Public method to determine, if a REPL can be started. + + @return tuple containing a flag indicating it is safe to start a REPL + and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canStartPlotter(self): + """ + Public method to determine, if a Plotter can be started. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canRunScript(self): + """ + Public method to determine, if a script can be executed. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def runScript(self, script): + """ + Public method to run the given Python script. + + @param script script to be executed + @type str + """ + pythonScript = script.split("\n") + self.sendCommands(pythonScript) + + def canStartFileManager(self): + """ + Public method to determine, if a File Manager can be started. + + @return tuple containing a flag indicating it is safe to start a + File Manager and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def supportsLocalFileAccess(self): + """ + Public method to indicate file access via a local directory. + + @return flag indicating file access via local directory + @type bool + """ + return self.__deviceVolumeMounted() + + def __deviceVolumeMounted(self): + """ + Private method to check, if the device volume is mounted. + + @return flag indicated a mounted device + @rtype bool + """ + return self.getWorkspace().endswith(self.DeviceVolumeName) + + def getWorkspace(self): + """ + Public method to get the workspace directory. + + @return workspace directory used for saving files + @rtype str + """ + # Attempts to find the path on the filesystem that represents the + # plugged in CIRCUITPY board. + deviceDirectory = Utilities.findVolume(self.DeviceVolumeName) + + if deviceDirectory: + return deviceDirectory + else: + # return the default workspace and give the user a warning + E5MessageBox.warning( + self.microPython, + self.tr("Workspace Directory"), + self.tr("Python files for CircuitPython devices are stored on" + " the device. Therefore, to edit these files you need" + " to have the device plugged in. Until you plug in a" + " device, the standard directory will be used.")) + + return super(CircuitPythonDevice, self).getWorkspace() + + def addDeviceMenuEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + connected = self.microPython.isConnected() + + act = menu.addAction(self.tr("Flash CircuitPython Firmware"), + self.__flashCircuitPython) + act.setEnabled(not connected) + menu.addSeparator() + act = menu.addAction(self.tr("Install Library Files"), + self.__installLibraryFiles) + act.setEnabled(self.__deviceVolumeMounted()) + + @pyqtSlot() + def __flashCircuitPython(self): + """ + Private slot to flash a CircuitPython firmware to the device. + """ + ok = E5MessageBox.information( + self.microPython, + self.tr("Flash CircuitPython Firmware"), + self.tr("Please reset the device to bootloader mode and confirm" + " when ready."), + E5MessageBox.StandardButtons( + E5MessageBox.Abort | + E5MessageBox.Ok)) + if ok: + from .CircuitPythonFirmwareSelectionDialog import ( + CircuitPythonFirmwareSelectionDialog) + dlg = CircuitPythonFirmwareSelectionDialog() + if dlg.exec_() == QDialog.Accepted: + cpyPath, devicePath = dlg.getData() + shutil.copy2(cpyPath, devicePath) + + @pyqtSlot() + def __installLibraryFiles(self): + """ + Private slot to install Python files into the onboard library. + """ + if not self.__deviceVolumeMounted(): + E5MessageBox.critical( + self.microPython, + self.tr("Install Library Files"), + self.tr("""The device volume "<b>{0}</b>" is not available.""" + """ Ensure it is mounted properly and try again.""")) + return + + target = os.path.join(self.getWorkspace(), "lib") + # ensure that the library directory exists on the device + if not os.path.isdir(target): + os.makedirs(target) + + libraryFiles = E5FileDialog.getOpenFileNames( + self.microPython, + self.tr("Install Library Files"), + os.path.expanduser("~"), + self.tr("Compiled Python Files (*.mpy);;" + "Python Files (*.py);;" + "All Files (*)")) + + for libraryFile in libraryFiles: + if os.path.exists(libraryFile): + shutil.copy2(libraryFile, target)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to enter the firmware flashing data. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog, QDialogButtonBox + +from E5Gui.E5PathPicker import E5PathPickerModes +from E5Gui import E5MessageBox + +from .Ui_CircuitPythonFirmwareSelectionDialog import ( + Ui_CircuitPythonFirmwareSelectionDialog +) + +import Utilities +import UI.PixmapCache + + +class CircuitPythonFirmwareSelectionDialog( + QDialog, Ui_CircuitPythonFirmwareSelectionDialog): + """ + Class implementing a dialog to enter the firmware flashing data. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(CircuitPythonFirmwareSelectionDialog, self).__init__(parent) + self.setupUi(self) + + self.retestButton.setIcon(UI.PixmapCache.getIcon("rescan")) + + self.firmwarePicker.setMode(E5PathPickerModes.OpenFileMode) + self.firmwarePicker.setFilters( + self.tr("CircuitPython Firmware Files (*.uf2);;" + "All Files (*)")) + + self.bootPicker.setMode(E5PathPickerModes.DirectoryShowFilesMode) + + boards = ( + ("", ""), # indicator for no selection + + ("Circuit Playground Express", "CPLAYBOOT"), + ("Feather M0 Express", "FEATHERBOOT"), + ("Feather M4 Express", "FEATHERBOOT"), + ("Gemma M0", "GEMMABOOT"), + ("Grand Central M4 Express", "GCM4BOOT"), + ("ItsyBitsy M0 Express", "ITSYBOOT"), + ("ItsyBitsy M4 Express", "ITSYM4BOOT"), + ("Metro M0 Express", "METROBOOT"), + ("Metro M4 Express", "METROM4BOOT"), + ("NeoTrelis M4 Express", "TRELM4BOOT"), + ("Trinket M0", "TRINKETBOOT"), + + ("Manual Select", "<manual>"), + ) + for boardName, bootVolume in boards: + self.boardComboBox.addItem(boardName, bootVolume) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def __updateOkButton(self): + """ + Private method to update the state of the OK button and the retest + button. + """ + firmwareFile = self.firmwarePicker.text() + self.retestButton.setEnabled(bool(firmwareFile) and + os.path.exists(firmwareFile)) + + if not bool(firmwareFile) or not os.path.exists(firmwareFile): + enable = False + else: + volumeName = self.boardComboBox.currentData() + if volumeName and volumeName != "<manual>": + # check if the user selected a board and the board is in + # bootloader mode + deviceDirectory = Utilities.findVolume(volumeName) + if deviceDirectory: + self.bootPicker.setText(deviceDirectory) + enable = True + else: + enable = False + E5MessageBox.warning( + self, + self.tr("Select Path to Device"), + self.tr("""<p>The device volume <b>{0}</b> could not""" + """ be found. Is the device in 'bootloader'""" + """ mode and mounted?</p> <p>Alternatively""" + """ select the Manual Select" entry and""" + """ enter the path to the device below.</p>""") + .format(volumeName) + ) + + elif volumeName == "<manual>": + # select the device path manually + deviceDirectory = self.bootPicker.text() + enable = (bool(deviceDirectory) and + os.path.exists(deviceDirectory)) + + else: + # illegal entry + enable = False + + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) + + @pyqtSlot(str) + def on_firmwarePicker_textChanged(self, firmware): + """ + Private slot handling a change of the firmware path. + + @param firmware path to the firmware + @type str + """ + self.__updateOkButton() + + @pyqtSlot(int) + def on_boardComboBox_currentIndexChanged(self, index): + """ + Private slot to handle the selection of a board type. + + @param index index of the selected board type + @type int + """ + if self.boardComboBox.itemData(index) == "<manual>": + self.bootPicker.clear() + self.bootPicker.setEnabled(True) + else: + self.bootPicker.setEnabled(False) + + self.__updateOkButton() + + @pyqtSlot() + def on_retestButton_clicked(self): + """ + Private slot to research for the selected volume. + """ + self.__updateOkButton() + + @pyqtSlot(str) + def on_bootPicker_textChanged(self, devicePath): + """ + Private slot handling a change of the device path. + + @param devicePath path to the device + @type str + """ + self.__updateOkButton() + + def getData(self): + """ + Public method to obtain the entered data. + + @return tuple containing the path to the CircuitPython firmware file + and the path to the device + @rtype tuple of (str, str) + """ + return self.firmwarePicker.text(), self.bootPicker.text()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/CircuitPythonFirmwareSelectionDialog.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CircuitPythonFirmwareSelectionDialog</class> + <widget class="QDialog" name="CircuitPythonFirmwareSelectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>124</height> + </rect> + </property> + <property name="windowTitle"> + <string>Flash CircuitPython Firmware</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="1"> + <widget class="QComboBox" name="boardComboBox"> + <property name="toolTip"> + <string>Select the board type or 'Manual'</string> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToContents</enum> + </property> + </widget> + </item> + <item row="0" column="1" colspan="3"> + <widget class="E5PathPicker" name="firmwarePicker" native="true"> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of the CircuitPython firmware file</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>339</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="0" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Firmware:</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Boot Path:</string> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Board Type:</string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="3"> + <widget class="E5PathPicker" name="bootPicker" native="true"> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path to the device in bootloader mode</string> + </property> + </widget> + </item> + <item row="3" column="0" colspan="4"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QToolButton" name="retestButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to search the selected volume</string> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>firmwarePicker</tabstop> + <tabstop>boardComboBox</tabstop> + <tabstop>retestButton</tabstop> + <tabstop>bootPicker</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>CircuitPythonFirmwareSelectionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>CircuitPythonFirmwareSelectionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/EspDevices.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some utility functions and the MicroPythonDevice base +class. +""" + +from __future__ import unicode_literals + +import sys + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtWidgets import QDialog + +from E5Gui import E5MessageBox +from E5Gui.E5ProcessDialog import E5ProcessDialog +from E5Gui.E5Application import e5App + +from .MicroPythonDevices import MicroPythonDevice +from .MicroPythonWidget import HAS_QTCHART + + +class EspDevice(MicroPythonDevice): + """ + Class implementing the device for ESP32 and ESP8266 based boards. + """ + def __init__(self, microPythonWidget, parent=None): + """ + Constructor + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param parent reference to the parent object + @type QObject + """ + super(EspDevice, self).__init__(microPythonWidget, parent) + + def setButtons(self): + """ + Public method to enable the supported action buttons. + """ + super(EspDevice, self).setButtons() + self.microPython.setActionButtons( + run=True, repl=True, files=True, chart=HAS_QTCHART) + + def forceInterrupt(self): + """ + Public method to determine the need for an interrupt when opening the + serial connection. + + @return flag indicating an interrupt is needed + @rtype bool + """ + return True + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("ESP8266, ESP32") + + def canStartRepl(self): + """ + Public method to determine, if a REPL can be started. + + @return tuple containing a flag indicating it is safe to start a REPL + and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canStartPlotter(self): + """ + Public method to determine, if a Plotter can be started. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canRunScript(self): + """ + Public method to determine, if a script can be executed. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return self.canStartRepl() + + def runScript(self, script): + """ + Public method to run the given Python script. + + @param script script to be executed + @type str + """ + pythonScript = script.split("\n") + self.sendCommands(pythonScript) + + def canStartFileManager(self): + """ + Public method to determine, if a File Manager can be started. + + @return tuple containing a flag indicating it is safe to start a + File Manager and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def addDeviceMenuEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + connected = self.microPython.isConnected() + + act = menu.addAction(self.tr("Erase Flash"), + self.__eraseFlash) + act.setEnabled(not connected) + act = menu.addAction(self.tr("Flash MicroPython Firmware"), + self.__flashMicroPython) + act.setEnabled(not connected) + menu.addSeparator() + act = menu.addAction(self.tr("Flash Additional Firmware"), + self.__flashAddons) + act.setEnabled(not connected) + menu.addSeparator() + menu.addAction(self.tr("Install 'esptool.py'"), self.__installEspTool) + + @pyqtSlot() + def __eraseFlash(self): + """ + Private slot to erase the device flash memory. + """ + ok = E5MessageBox.yesNo( + self, + self.tr("Erase Flash"), + self.tr("""Shall the flash of the selected device really be""" + """ erased?""")) + if ok: + flashArgs = [ + "-u", + "-m", "esptool", + "--port", self.microPython.getCurrentPort(), + "erase_flash", + ] + dlg = E5ProcessDialog(self.tr("'esptool erase_flash' Output"), + self.tr("Erase Flash")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __flashMicroPython(self): + """ + Private slot to flash a MicroPython firmware to the device. + + @exception ValueError raised to indicate an unsupported chip type + """ + from .EspFirmwareSelectionDialog import EspFirmwareSelectionDialog + dlg = EspFirmwareSelectionDialog() + if dlg.exec_() == QDialog.Accepted: + chip, firmware, _ = dlg.getData() + if chip == "esp8266": + flashAddress = "0x0000" + elif chip == "esp32": + flashAddress = "0x1000" + else: + raise ValueError(self.tr("Unsupported chip type '{0}'.") + .format(chip)) + flashArgs = [ + "-u", + "-m", "esptool", + "--chip", chip, + "--port", self.microPython.getCurrentPort(), + "write_flash", + flashAddress, + firmware, + ] + dlg = E5ProcessDialog(self.tr("'esptool write_flash' Output"), + self.tr("Flash MicroPython Firmware")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __flashAddons(self): + """ + Private slot to flash some additional firmware images. + """ + from .EspFirmwareSelectionDialog import EspFirmwareSelectionDialog + dlg = EspFirmwareSelectionDialog(addon=True) + if dlg.exec_() == QDialog.Accepted: + chip, firmware, flashAddress = dlg.getData() + flashArgs = [ + "-u", + "-m", "esptool", + "--chip", chip, + "--port", self.microPython.getCurrentPort(), + "write_flash", + flashAddress.lower(), + firmware, + ] + dlg = E5ProcessDialog(self.tr("'esptool write_flash' Output"), + self.tr("Flash Additional Firmware")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __installEspTool(self): + """ + Private slot to install the esptool package via pip. + """ + pip = e5App().getObject("Pip") + pip.installPackages(["esptool"], interpreter=sys.executable)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/EspFirmwareSelectionDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to select the ESP chip type and the firmware to +be flashed. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot, QRegularExpression +from PyQt5.QtGui import QRegularExpressionValidator +from PyQt5.QtWidgets import QDialog, QDialogButtonBox + +from E5Gui.E5PathPicker import E5PathPickerModes + +from .Ui_EspFirmwareSelectionDialog import Ui_EspFirmwareSelectionDialog + + +class EspFirmwareSelectionDialog(QDialog, Ui_EspFirmwareSelectionDialog): + """ + Class implementing a dialog to select the ESP chip type and the firmware to + be flashed. + """ + def __init__(self, addon=False, parent=None): + """ + Constructor + + @param addon flag indicating an addon firmware + @type bool + @param parent reference to the parent widget + @type QWidget + """ + super(EspFirmwareSelectionDialog, self).__init__(parent) + self.setupUi(self) + + self.__addon = addon + + self.firmwarePicker.setMode(E5PathPickerModes.OpenFileMode) + self.firmwarePicker.setFilters( + self.tr("Firmware Files (*.bin);;All Files (*)")) + + self.espComboBox.addItems(["", "ESP32", "ESP8266"]) + + if addon: + self.__validator = QRegularExpressionValidator( + QRegularExpression(r"[0-9a-fA-F]{0,4}") + ) + self.addressEdit.setValidator(self.__validator) + else: + self.addressLabel.hide() + self.addressEdit.hide() + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + def __updateOkButton(self): + """ + Private method to update the state of the OK button. + """ + firmwareFile = self.firmwarePicker.text() + enable = (bool(self.espComboBox.currentText()) and + bool(firmwareFile) and os.path.exists(firmwareFile)) + if self.__addon: + enable &= bool(self.addressEdit.text()) + self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(enable) + + @pyqtSlot(str) + def on_espComboBox_currentTextChanged(self, chip): + """ + Private slot to handle the selection of a chip type. + + @param chip selected chip type + @type str + """ + self.__updateOkButton() + + @pyqtSlot(str) + def on_firmwarePicker_textChanged(self, firmware): + """ + Private slot handling a change of the firmware path. + + @param firmware path to the firmware + @type str + """ + self.__updateOkButton() + + def getData(self): + """ + Public method to get the entered data. + + @return tuple containing the selected chip type, the path of the + firmware file and the flash address + @rtype tuple of (str, str, str) + """ + if self.__addon: + address = self.addressEdit.text() + else: + address = "" + + return ( + self.espComboBox.currentText().lower(), + self.firmwarePicker.text(), + address, + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/EspFirmwareSelectionDialog.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EspFirmwareSelectionDialog</class> + <widget class="QDialog" name="EspFirmwareSelectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>114</height> + </rect> + </property> + <property name="windowTitle"> + <string>Flash MicroPython Firmware</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>ESP Chip Type:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="espComboBox"> + <property name="toolTip"> + <string>Select the ESP chip type</string> + </property> + <property name="sizeAdjustPolicy"> + <enum>QComboBox::AdjustToContents</enum> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Firmware:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="E5PathPicker" name="firmwarePicker" native="true"> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of the firmware file</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="addressLabel"> + <property name="text"> + <string>Address:</string> + </property> + </widget> + </item> + <item row="2" column="1" colspan="2"> + <widget class="E5ClearableLineEdit" name="addressEdit"> + <property name="toolTip"> + <string>Enter the flash addres in the hexadecimal form</string> + </property> + <property name="maxLength"> + <number>4</number> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + <customwidget> + <class>E5ClearableLineEdit</class> + <extends>QLineEdit</extends> + <header>E5Gui/E5LineEdit.h</header> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>espComboBox</tabstop> + <tabstop>firmwarePicker</tabstop> + <tabstop>addressEdit</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EspFirmwareSelectionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EspFirmwareSelectionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonCommandsInterface.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,825 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some file system commands for MicroPython. +""" + +from __future__ import unicode_literals + +import ast +import time +import os + +from PyQt5.QtCore import ( + pyqtSlot, pyqtSignal, QObject, QThread, QTimer, QCoreApplication, + QEventLoop +) + +from .MicroPythonSerialPort import MicroPythonSerialPort + +import Preferences + + +class MicroPythonCommandsInterface(QObject): + """ + Class implementing some file system commands for MicroPython. + + Commands are provided to perform operations on the file system of a + connected MicroPython device. Supported commands are: + <ul> + <li>ls: directory listing</li> + <li>lls: directory listing with meta data</li> + <li>cd: change directory</li> + <li>pwd: get the current directory</li> + <li>put: copy a file to the connected device</li> + <li>get: get a file from the connected device</li> + <li>rm: remove a file from the connected device</li> + <li>rmrf: remove a file/directory recursively (like 'rm -rf' in bash) + <li>mkdir: create a new directory</li> + <li>rmdir: remove an empty directory</li> + </ul> + + There are additional commands related to time and version. + <ul> + <li>version: get version info about MicroPython</li> + <li>getImplementation: get some implementation information</li> + <li>syncTime: synchronize the time of the connected device</li> + <li>showTime: show the current time of the connected device</li> + </ul> + + @signal executeAsyncFinished() emitted to indicate the end of an + asynchronously executed list of commands (e.g. a script) + @signal dataReceived(data) emitted to send data received via the serial + connection for further processing + """ + executeAsyncFinished = pyqtSignal() + dataReceived = pyqtSignal(bytes) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object + @type QObject + """ + super(MicroPythonCommandsInterface, self).__init__(parent) + + self.__repl = parent + + self.__blockReadyRead = False + + self.__serial = MicroPythonSerialPort( + timeout=Preferences.getMicroPython("SerialTimeout"), + parent=self) + self.__serial.readyRead.connect(self.__readSerial) + + @pyqtSlot() + def __readSerial(self): + """ + Private slot to read all available serial data and emit it with the + "dataReceived" signal for further processing. + """ + if not self.__blockReadyRead: + data = bytes(self.__serial.readAll()) + self.dataReceived.emit(data) + + @pyqtSlot() + def connectToDevice(self, port): + """ + Public slot to start the manager. + + @param port name of the port to be used + @type str + @return flag indicating success + @rtype bool + """ + return self.__serial.openSerialLink(port) + + @pyqtSlot() + def disconnectFromDevice(self): + """ + Public slot to stop the thread. + """ + self.__serial.closeSerialLink() + + def isConnected(self): + """ + Public method to get the connection status. + + @return flag indicating the connection status + @rtype bool + """ + return self.__serial.isConnected() + + @pyqtSlot() + def handlePreferencesChanged(self): + """ + Public slot to handle a change of the preferences. + """ + self.__serial.setTimeout(Preferences.getMicroPython("SerialTimeout")) + + def write(self, data): + """ + Public method to write data to the connected device. + + @param data data to be written + @type bytes or bytearray + """ + self.__serial.isConnected() and self.__serial.write(data) + + def __rawOn(self): + """ + Private method to switch the connected device to 'raw' mode. + + Note: switching to raw mode is done with synchronous writes. + + @return flag indicating success + @@rtype bool + """ + if not self.__serial: + return False + + rawReplMessage = b"raw REPL; CTRL-B to exit\r\n>" + + self.__serial.write(b"\x02") # end raw mode if required + self.__serial.waitForBytesWritten() + for _i in range(3): + # CTRL-C three times to break out of loops + self.__serial.write(b"\r\x03") + self.__serial.waitForBytesWritten() + QThread.msleep(10) + self.__serial.readAll() # read all data and discard it + self.__serial.write(b"\r\x01") # send CTRL-A to enter raw mode + self.__serial.readUntil(rawReplMessage) + if self.__serial.hasTimedOut(): + # it timed out; try it again and than fail + self.__serial.write(b"\r\x01") # send CTRL-A again + self.__serial.readUntil(rawReplMessage) + if self.__serial.hasTimedOut(): + return False + + QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) + self.__serial.readAll() # read all data and discard it + return True + + def __rawOff(self): + """ + Private method to switch 'raw' mode off. + """ + if self.__serial: + self.__serial.write(b"\x02") # send CTRL-B to cancel raw mode + self.__serial.readUntil(b">>> ") # read until Python prompt + self.__serial.readAll() # read all data and discard it + + def execute(self, commands): + """ + Public method to send commands to the connected device and return the + result. + + If no serial connection is available, empty results will be returned. + + @param commands list of commands to be executed + @type str + @return tuple containing stdout and stderr output of the device + @rtype tuple of (bytes, bytes) + """ + if not self.__serial: + return b"", b"" + + if not self.__serial.isConnected(): + return b"", b"Device not connected or not switched on." + + result = bytearray() + err = b"" + + # switch on raw mode + self.__blockReadyRead = True + ok = self.__rawOn() + if not ok: + self.__blockReadyRead = False + return ( + b"", + b"Could not switch to raw mode. Is the device switched on?" + ) + + # send commands + QThread.msleep(10) + for command in commands: + if command: + commandBytes = command.encode("utf-8") + self.__serial.write(commandBytes + b"\x04") + QCoreApplication.processEvents( + QEventLoop.ExcludeUserInputEvents) + ok = self.__serial.readUntil(b"OK") + if ok != b"OK": + return ( + b"", + "Expected 'OK', got '{0}', followed by '{1}'".format( + ok, self.__serial.readAll()).encode("utf-8") + ) + + # read until prompt + response = self.__serial.readUntil(b"\x04>") + if self.__serial.hasTimedOut(): + self.__blockReadyRead = False + return b"", b"Timeout while processing commands." + if b"\x04" in response[:-2]: + # split stdout, stderr + out, err = response[:-2].split(b"\x04") + result += out + else: + err = b"invalid response received: " + response + if err: + self.__blockReadyRead = False + return b"", err + + # switch off raw mode + QThread.msleep(10) + self.__rawOff() + self.__blockReadyRead = False + + return bytes(result), err + + def executeAsync(self, commandsList): + """ + Public method to execute a series of commands over a period of time + without returning any result (asynchronous execution). + + @param commandsList list of commands to be execute on the device + @type list of bytes + """ + def remainingTask(commands): + self.executeAsync(commands) + + if commandsList: + command = commandsList[0] + self.__serial.write(command) + remainder = commandsList[1:] + QTimer.singleShot(2, lambda: remainingTask(remainder)) + else: + self.executeAsyncFinished.emit() + + def __shortError(self, error): + """ + Private method to create a shortened error message. + + @param error verbose error message + @type bytes + @return shortened error message + @rtype str + """ + if error: + decodedError = error.decode("utf-8") + try: + return decodedError.split["\r\n"][-2] + except Exception: + return decodedError + return self.tr("Detected an error without indications.") + + ################################################################## + ## Methods below implement the file system commands + ################################################################## + + def ls(self, dirname=""): + """ + Public method to get a directory listing of the connected device. + + @param dirname name of the directory to be listed + @type str + @return tuple containg the directory listing + @rtype tuple of str + @exception IOError raised to indicate an issue with the device + """ + if self.__repl.isMicrobit(): + # BBC micro:bit does not support directories + commands = [ + "import os as __os_", + "print(__os_.listdir())", + "del __os_", + ] + else: + commands = [ + "import os as __os_", + "print(__os_.listdir('{0}'))".format(dirname), + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return ast.literal_eval(out.decode("utf-8")) + + def lls(self, dirname="", fullstat=False, showHidden=False): + """ + Public method to get a long directory listing of the connected device + including meta data. + + @param dirname name of the directory to be listed + @type str + @param fullstat flag indicating to return the full stat() tuple + @type bool + @param showHidden flag indicating to show hidden files as well + @type bool + @return list containing the directory listing with tuple entries of + the name and and a tuple of mode, size and time (if fullstat is + false) or the complete stat() tuple. 'None' is returned in case the + directory doesn't exist. + @rtype tuple of (str, tuple) + @exception IOError raised to indicate an issue with the device + """ + if self.__repl.isMicrobit(): + # BBC micro:bit does not support directories + commands = [ + "import os as __os_", + "\n".join([ + "def is_visible(filename, showHidden):", + " return showHidden or " + "(filename[0] != '.' and filename[-1] != '~')", + ]), + "\n".join([ + "def stat(filename):", + " size = __os_.size(filename)", + " return (0, 0, 0, 0, 0, 0, size, 0, 0, 0)" + ]), + "\n".join([ + "def listdir_stat(showHidden):", + " files = __os_.listdir()", + " return list((f, stat(f)) for f in files if" + " is_visible(f,showHidden))", + ]), + "print(listdir_stat({0}))".format(showHidden), + "del __os_, stat, listdir_stat, is_visible", + ] + else: + commands = [ + "import os as __os_", + "\n".join([ + "def is_visible(filename, showHidden):", + " return showHidden or " + "(filename[0] != '.' and filename[-1] != '~')", + ]), + "\n".join([ + "def stat(filename):", + " try:", + " rstat = __os_.lstat(filename)", + " except:", + " rstat = __os_.stat(filename)", + " return tuple(rstat)", + ]), + "\n".join([ + "def listdir_stat(dirname, showHidden):", + " try:", + " files = __os_.listdir(dirname)", + " except OSError:", + " return None", + " if dirname in ('', '/'):", + " return list((f, stat(f)) for f in files if" + " is_visible(f, showHidden))", + " return list((f, stat(dirname + '/' + f))" + " for f in files if is_visible(f, showHidden))", + ]), + "print(listdir_stat('{0}', {1}))".format(dirname, showHidden), + "del __os_, stat, listdir_stat, is_visible", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + fileslist = ast.literal_eval(out.decode("utf-8")) + if fileslist is None: + return None + else: + if fullstat: + return fileslist + else: + return [(f, (s[0], s[6], s[8])) for f, s in fileslist] + + def cd(self, dirname): + """ + Public method to change the current directory on the connected device. + + @param dirname directory to change to + @type str + @exception IOError raised to indicate an issue with the device + """ + assert dirname + + commands = [ + "import os as __os_", + "__os_.chdir('{0}')".format(dirname), + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + def pwd(self): + """ + Public method to get the current directory of the connected device. + + @return current directory + @rtype str + @exception IOError raised to indicate an issue with the device + """ + if self.__repl.isMicrobit(): + # BBC micro:bit does not support directories + return "" + + commands = [ + "import os as __os_", + "print(__os_.getcwd())", + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return out.decode("utf-8").strip() + + def rm(self, filename): + """ + Public method to remove a file from the connected device. + + @param filename name of the file to be removed + @type str + @exception IOError raised to indicate an issue with the device + """ + assert filename + + commands = [ + "import os as __os_", + "__os_.remove('{0}')".format(filename), + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + def rmrf(self, name, recursive=False, force=False): + """ + Public method to remove a file or directory recursively. + + @param name of the file or directory to remove + @type str + @param recursive flag indicating a recursive deletion + @type bool + @param force flag indicating to ignore errors + @type bool + @return flag indicating success + @rtype bool + @exception IOError raised to indicate an issue with the device + """ + assert name + + commands = [ + "import os as __os_", + "\n".join([ + "def remove_file(name, recursive=False, force=False):", + " try:", + " mode = __os_.stat(name)[0]", + " if mode & 0x4000 != 0:", + " if recursive:", + " for file in __os_.listdir(name):", + " success = remove_file(name + '/' + file," + " recursive, force)", + " if not success and not force:", + " return False", + " __os_.rmdir(name)", + " else:", + " if not force:", + " return False", + " else:", + " __os_.remove(name)", + " except:", + " if not force:", + " return False", + " return True", + ]), + "print(remove_file('{0}', {1}, {2}))".format(name, recursive, + force), + "del __os_, remove_file", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return ast.literal_eval(out.decode("utf-8")) + + def mkdir(self, dirname): + """ + Public method to create a new directory. + + @param dirname name of the directory to create + @type str + @exception IOError raised to indicate an issue with the device + """ + assert dirname + + commands = [ + "import os as __os_", + "__os_.mkdir('{0}')".format(dirname), + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + def rmdir(self, dirname): + """ + Public method to remove a directory. + + @param dirname name of the directory to be removed + @type str + @exception IOError raised to indicate an issue with the device + """ + assert dirname + + commands = [ + "import os as __os_", + "__os_.rmdir('{0}')".format(dirname), + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + def put(self, hostFileName, deviceFileName=None): + """ + Public method to copy a local file to the connected device. + + @param hostFileName name of the file to be copied + @type str + @param deviceFileName name of the file to copy to + @type str + @return flag indicating success + @rtype bool + @exception IOError raised to indicate an issue with the device + """ + if not os.path.isfile(hostFileName): + raise IOError("No such file: {0}".format(hostFileName)) + + with open(hostFileName, "rb") as hostFile: + content = hostFile.read() + # convert eol '\r' + content = content.replace(b"\r\n", b"\r") + content = content.replace(b"\n", b"\r") + + if not deviceFileName: + deviceFileName = os.path.basename(hostFileName) + + commands = [ + "fd = open('{0}', 'wb')".format(deviceFileName), + "f = fd.write", + ] + while content: + chunk = content[:64] + commands.append("f(" + repr(chunk) + ")") + content = content[64:] + commands.extend([ + "fd.close()", + "del f, fd", + ]) + + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return True + + def get(self, deviceFileName, hostFileName=None): + """ + Public method to copy a file from the connected device. + + @param deviceFileName name of the file to copy + @type str + @param hostFileName name of the file to copy to + @type str + @return flag indicating success + @rtype bool + @exception IOError raised to indicate an issue with the device + """ + if not hostFileName: + hostFileName = deviceFileName + + commands = [ + "\n".join([ + "def send_data():", + " try:", + " from microbit import uart as u", + " except ImportError:", + " try:", + " from machine import UART", + " u = UART(0, {0})".format(115200), + " except Exception:", + " try:", + " from sys import stdout as u", + " except Exception:", + " raise Exception('Could not find UART module" + " in device.')", + " f = open('{0}', 'rb')".format(deviceFileName), + " r = f.read", + " result = True", + " while result:", + " result = r(32)", + " if result:", + " u.write(result)", + " f.close()", + ]), + "send_data()", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + # write the received bytes to the local file + # convert eol to "\n" + out = out.replace(b"\r\n", b"\n") + out = out.replace(b"\r", b"\n") + with open(hostFileName, "wb") as hostFile: + hostFile.write(out) + return True + + def fileSystemInfo(self): + """ + Public method to obtain information about the currently mounted file + systems. + + @return tuple of tuples containing the file system name, the total + size, the used size and the free size + @rtype tuple of tuples of (str, int, int, int) + @exception IOError raised to indicate an issue with the device + """ + commands = [ + "import os as __os_", + "\n".join([ + "def fsinfo():", + " infolist = []", + " info = __os_.statvfs('/')", + " if info[0] == 0:", + # assume it is just mount points + " fsnames = __os_.listdir('/')", + " for fs in fsnames:", + " fs = '/' + fs", + " infolist.append((fs, __os_.statvfs(fs)))", + " else:", + " infolist.append(('/', info))", + " return infolist", + ]), + "print(fsinfo())", + "del __os_, fsinfo", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + infolist = ast.literal_eval(out.decode("utf-8")) + if infolist is None: + return None + else: + filesystemInfos = [] + for fs, info in infolist: + totalSize = info[2] * info[1] + freeSize = info[4] * info[1] + usedSize = totalSize - freeSize + filesystemInfos.append((fs, totalSize, usedSize, freeSize)) + + return tuple(filesystemInfos) + + ################################################################## + ## non-filesystem related methods below + ################################################################## + + def version(self): + """ + Public method to get the MicroPython version information of the + connected device. + + @return dictionary containing the version information + @rtype dict + @exception IOError raised to indicate an issue with the device + """ + commands = [ + "import os as __os_", + "print(__os_.uname())", + "del __os_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + rawOutput = out.decode("utf-8").strip() + rawOutput = rawOutput[1:-1] + items = rawOutput.split(",") + result = {} + for item in items: + key, value = item.strip().split("=") + result[key.strip()] = value.strip()[1:-1] + return result + + def getImplementation(self): + """ + Public method to get some implementation information of the connected + device. + + @return dictionary containing the implementation information + @rtype dict + @exception IOError raised to indicate an issue with the device + """ + commands = [ + "import sys as __sys_", + "res = {}", # __IGNORE_WARNING_M613__ + "\n".join([ + "try:", + " res['name'] = __sys_.implementation.name", + "except AttributeError:", + " res['name'] = 'unknown'", + ]), + "\n".join([ + "try:", + " res['version'] = '.'.join((str(i) for i in" + " __sys_.implementation.version))", + "except AttributeError:", + " res['version'] = 'unknown'", + ]), + "print(res)", + "del res, __sys_", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return ast.literal_eval(out.decode("utf-8")) + + def syncTime(self): + """ + Public method to set the time of the connected device to the local + computer's time. + + @exception IOError raised to indicate an issue with the device + """ + now = time.localtime(time.time()) + commands = [ + "\n".join([ + "def set_time(rtc_time):", + " rtc = None", + " try:", # Pyboard (it doesn't have machine.RTC()) + " import pyb as __pyb_", + " rtc = __pyb_.RTC()", + " clock_time = rtc_time[:6] + (rtc_time[6] + 1, 0)", + " rtc.datetime(clock_time)", + " del __pyb_", + " except Exception:", + " try:", + " import machine as __machine_", + " rtc = __machine_.RTC()", + " try:", # ESP8266 may use rtc.datetime() + " clock_time = rtc_time[:6] +" + " (rtc_time[6] + 1, 0)", + " rtc.datetime(clock_time)", + " except Exception:", # ESP32 uses rtc.init() + " rtc.init(rtc_time[:6])", + " del __machine_", + " except:", + " try:", + " import rtc as __rtc_", + " import time as __time_", + " clock=__rtc_.RTC()", + " clock.datetime = __time_.struct_time(" + "rtc_time + (-1, -1))", + " del __rtc_, __time_", + " except:", + " pass", + ]), + "set_time({0})".format((now.tm_year, now.tm_mon, now.tm_mday, + now.tm_hour, now.tm_min, now.tm_sec, + now.tm_wday)), + "del set_time", + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + + def getTime(self): + """ + Public method to get the current time of the device. + + @return time of the device + @rtype str + @exception IOError raised to indicate an issue with the device + """ + commands = [ + "import time as __time_", + "\n".join([ + "try:", + " print(__time_.strftime('%Y-%m-%d %H:%M:%S'," + # __IGNORE_WARNING_M601__ + " __time_.localtime()))", + "except AttributeError:", + " tm = __time_.localtime()", + " print('{0:04d}-{1:02d}-{2:02d} {3:02d}:{4:02d}:{5:02d}'" + ".format(tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour," + " tm.tm_min, tm.tm_sec))", + " del tm", + ]), + "del __time_" + ] + out, err = self.execute(commands) + if err: + raise IOError(self.__shortError(err)) + return out.decode("utf-8").strip()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonDevices.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some utility functions and the MicroPythonDevice base +class. +""" + +from __future__ import unicode_literals + +import logging +import os + +from PyQt5.QtCore import pyqtSlot, QObject + +import UI.PixmapCache +import Preferences + + +SupportedBoards = { + "esp": { + "ids": [ + (0x1A86, 0x7523), # HL-340 + (0x10C4, 0xEA60), # CP210x + (0x0403, 0x6015), # Sparkfun ESP32 + ], + "description": "ESP8266, ESP32", + "icon": "esp32Device", + }, + + "circuitpython": { + "ids": [ + (0x2B04, 0xC00C), # Particle Argon + (0x2B04, 0xC00D), # Particle Boron + (0x2B04, 0xC00E), # Particle Xenon + (0x239A, None), # Any Adafruit Boards + (0x1209, 0xBAB1), # Electronic Cats Meow Meow + (0x1209, 0xBAB2), # Electronic Cats CatWAN USBStick + (0x1209, 0xBAB3), # Electronic Cats Bast Pro Mini M0 + (0x1B4F, 0x8D22), # SparkFun SAMD21 Mini Breakout + (0x1B4F, 0x8D23), # SparkFun SAMD21 Dev Breakout + (0x1209, 0x2017), # Mini SAM M4 + (0x1209, 0x7102), # Mini SAM M0 + ], + "description": "CircuitPython Boards", + "icon": "circuitPythonDevice", + }, + + "bbc_microbit": { + "ids": [ + (0x0D28, 0x0204), # micro:bit + ], + "description": "BBC micro:bit", + "icon": "microbitDevice", + }, +} + + +def getSupportedDevices(): + """ + Function to get a list of supported MicroPython devices. + + @return set of tuples with the board type and description + @rtype set of tuples of (str, str) + """ + boards = [] + for board in SupportedBoards: + boards.append( + (board, SupportedBoards[board]["description"])) + return boards + + +def getFoundDevices(): + """ + Function to check the serial ports for supported MicroPython devices. + + @return set of tuples with the board type, a description and the serial + port it is connected at + @rtype set of tuples of (str, str, str) + """ + from PyQt5.QtSerialPort import QSerialPortInfo + + foundDevices = [] + + availablePorts = QSerialPortInfo.availablePorts() + for port in availablePorts: + vid = port.vendorIdentifier() + pid = port.productIdentifier() + for board in SupportedBoards: + if ((vid, pid) in SupportedBoards[board]["ids"] or + (vid, None) in SupportedBoards[board]["ids"]): + foundDevices.append( + (board, SupportedBoards[board]["description"], + port.portName())) + break + else: + logging.debug("Unknown device: (0x%04x:0x%04x)", vid, pid) + + return foundDevices + + +def getDeviceIcon(boardName, iconFormat=True): + """ + Function to get the icon for the given board. + + @param boardName name of the board + @type str + @param iconFormat flag indicating to get an icon or a pixmap + @type bool + @return icon for the board (iconFormat == True) or + a pixmap (iconFormat == False) + @rtype QIcon or QPixmap + """ + if boardName in SupportedBoards: + iconName = SupportedBoards[boardName]["icon"] + else: + # return a generic MicroPython icon + iconName = "micropython48" + + if iconFormat: + return UI.PixmapCache.getIcon(iconName) + else: + return UI.PixmapCache.getPixmap(iconName) + + +def getDevice(deviceType, microPythonWidget): + """ + Public method to instantiate a specific MicroPython device interface. + + @param deviceType type of the device interface + @type str + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @return instantiated device interface + @rtype MicroPythonDevice + """ + if deviceType == "esp": + from .EspDevices import EspDevice + return EspDevice(microPythonWidget) + elif deviceType == "circuitpython": + from .CircuitPythonDevices import CircuitPythonDevice + return CircuitPythonDevice(microPythonWidget) + elif deviceType == "bbc_microbit": + from .MicrobitDevices import MicrobitDevice + return MicrobitDevice(microPythonWidget) + else: + # nothing specific requested + return MicroPythonDevice(microPythonWidget) + + +class MicroPythonDevice(QObject): + """ + Base class for the more specific MicroPython devices. + """ + def __init__(self, microPythonWidget, parent=None): + """ + Constructor + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param parent reference to the parent object + @type QObject + """ + super(MicroPythonDevice, self).__init__(parent) + + self.microPython = microPythonWidget + + def setButtons(self): + """ + Public method to enable the supported action buttons. + """ + self.microPython.setActionButtons( + open=False, save=False, + run=False, repl=False, files=False, chart=False) + + def forceInterrupt(self): + """ + Public method to determine the need for an interrupt when opening the + serial connection. + + @return flag indicating an interrupt is needed + @rtype bool + """ + return True + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("Unsupported Device") + + def canStartRepl(self): + """ + Public method to determine, if a REPL can be started. + + @return tuple containing a flag indicating it is safe to start a REPL + and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return False, self.tr("REPL is not supported by this device.") + + def setRepl(self, on): + """ + Public method to set the REPL status and dependent status. + + @param on flag indicating the active status + @type bool + """ + pass + + def canStartPlotter(self): + """ + Public method to determine, if a Plotter can be started. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return False, self.tr("Plotter is not supported by this device.") + + def setPlotter(self, on): + """ + Public method to set the Plotter status and dependent status. + + @param on flag indicating the active status + @type bool + """ + pass + + def canRunScript(self): + """ + Public method to determine, if a script can be executed. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return False, self.tr("Running scripts is not supported by this" + " device.") + + def runScript(self, script): + """ + Public method to run the given Python script. + + @param script script to be executed + @type str + """ + pass + + def canStartFileManager(self): + """ + Public method to determine, if a File Manager can be started. + + @return tuple containing a flag indicating it is safe to start a + File Manager and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return False, self.tr("File Manager is not supported by this device.") + + def setFileManager(self, on): + """ + Public method to set the File Manager status and dependent status. + + @param on flag indicating the active status + @type bool + """ + pass + + def supportsLocalFileAccess(self): + """ + Public method to indicate file access via a local directory. + + @return flag indicating file access via local directory + @type bool + """ + return False # default + + def getWorkspace(self): + """ + Public method to get the workspace directory. + + @return workspace directory used for saving files + @rtype str + """ + return (Preferences.getMultiProject("Workspace") or + os.path.expanduser("~")) + + def sendCommands(self, commandsList): + """ + Public method to send a list of commands to the device. + + @param commandsList list of commands to be sent to the device + @type list of str + """ + rawOn = [ # sequence of commands to enter raw mode + b'\x02', # Ctrl-B: exit raw repl (just in case) + b'\r\x03\x03\x03', # Ctrl-C three times: interrupt any running + # program + b'\r\x01', # Ctrl-A: enter raw REPL + ] + newLine = [b'print("\\n")\r', ] + commands = [c.encode("utf-8)") + b'\r' for c in commandsList] + commands.append(b'\r') + commands.append(b'\x04') + rawOff = [b'\x02'] + commandSequence = rawOn + newLine + commands + rawOff + self.microPython.commandsInterface().executeAsync(commandSequence) + + @pyqtSlot() + def handleDataFlood(self): + """ + Public slot handling a data floof from the device. + """ + pass + + def addDeviceMenuEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + pass + + def hasTimeCommands(self): + """ + Public method to check, if the device supports time commands. + + The default returns True. + + @return flag indicating support for time commands + @rtype bool + """ + return True
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonFileManager.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,459 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some file system commands for MicroPython. +""" + +from __future__ import unicode_literals + +import os +import stat +import shutil + +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject + +from .MicroPythonFileSystemUtilities import ( + mtime2string, mode2string, decoratedName, listdirStat +) + + +class MicroPythonFileManager(QObject): + """ + Class implementing an interface to the device file system commands with + some additional sugar. + + @signal longListFiles(result) emitted with a tuple of tuples containing the + name, mode, size and time for each directory entry + @signal currentDir(dirname) emitted to report the current directory of the + device + @signal currentDirChanged(dirname) emitted to report back a change of the + current directory + @signal getFileDone(deviceFile, localFile) emitted after the file was + fetched from the connected device and written to the local file system + @signal putFileDone(localFile, deviceFile) emitted after the file was + copied to the connected device + @signal deleteFileDone(deviceFile) emitted after the file has been deleted + on the connected device + @signal rsyncDone(localName, deviceName) emitted after the rsync operation + has been completed + @signal rsyncProgressMessage(msg) emitted to send a message about what + rsync is doing + @signal removeDirectoryDone() emitted after a directory has been deleted + @signal createDirectoryDone() emitted after a directory was created + @signal fsinfoDone(fsinfo) emitted after the file system information was + obtained + + @signal error(exc) emitted with a failure message to indicate a failure + during the most recent operation + """ + longListFiles = pyqtSignal(tuple) + currentDir = pyqtSignal(str) + currentDirChanged = pyqtSignal(str) + getFileDone = pyqtSignal(str, str) + putFileDone = pyqtSignal(str, str) + deleteFileDone = pyqtSignal(str) + rsyncDone = pyqtSignal(str, str) + rsyncProgressMessage = pyqtSignal(str) + removeDirectoryDone = pyqtSignal() + createDirectoryDone = pyqtSignal() + fsinfoDone = pyqtSignal(tuple) + + error = pyqtSignal(str, str) + + def __init__(self, commandsInterface, parent=None): + """ + Constructor + + @param commandsInterface reference to the commands interface object + @type MicroPythonCommandsInterface + @param parent reference to the parent object + @type QObject + """ + super(MicroPythonFileManager, self).__init__(parent) + + self.__commandsInterface = commandsInterface + + @pyqtSlot(str) + def lls(self, dirname, showHidden=False): + """ + Public slot to get a long listing of the given directory. + + @param dirname name of the directory to list + @type str + @param showHidden flag indicating to show hidden files as well + @type bool + """ + try: + filesList = self.__commandsInterface.lls( + dirname, showHidden=showHidden) + result = [(decoratedName(name, mode), + mode2string(mode), + str(size), + mtime2string(mtime)) for + name, (mode, size, mtime) in filesList] + self.longListFiles.emit(tuple(result)) + except Exception as exc: + self.error.emit("lls", str(exc)) + + @pyqtSlot() + def pwd(self): + """ + Public slot to get the current directory of the device. + """ + try: + pwd = self.__commandsInterface.pwd() + self.currentDir.emit(pwd) + except Exception as exc: + self.error.emit("pwd", str(exc)) + + @pyqtSlot(str) + def cd(self, dirname): + """ + Public slot to change the current directory of the device. + + @param dirname name of the desired current directory + @type str + """ + try: + self.__commandsInterface.cd(dirname) + self.currentDirChanged.emit(dirname) + except Exception as exc: + self.error.emit("cd", str(exc)) + + @pyqtSlot(str) + @pyqtSlot(str, str) + def get(self, deviceFileName, hostFileName=""): + """ + Public slot to get a file from the connected device. + + @param deviceFileName name of the file on the device + @type str + @param hostFileName name of the local file + @type str + """ + if hostFileName and os.path.isdir(hostFileName): + # only a local directory was given + hostFileName = os.path.join(hostFileName, + os.path.basename(deviceFileName)) + try: + self.__commandsInterface.get(deviceFileName, hostFileName) + self.getFileDone.emit(deviceFileName, hostFileName) + except Exception as exc: + self.error.emit("get", str(exc)) + + @pyqtSlot(str) + @pyqtSlot(str, str) + def put(self, hostFileName, deviceFileName=""): + """ + Public slot to put a file onto the device. + + @param hostFileName name of the local file + @type str + @param deviceFileName name of the file on the connected device + @type str + """ + try: + self.__commandsInterface.put(hostFileName, deviceFileName) + self.putFileDone.emit(hostFileName, deviceFileName) + except Exception as exc: + self.error.emit("put", str(exc)) + + @pyqtSlot(str) + def delete(self, deviceFileName): + """ + Public slot to delete a file on the device. + + @param deviceFileName name of the file on the connected device + @type str + """ + try: + self.__commandsInterface.rm(deviceFileName) + self.deleteFileDone.emit(deviceFileName) + except Exception as exc: + self.error.emit("delete", str(exc)) + + def __rsync(self, hostDirectory, deviceDirectory, mirror=True, + localDevice=False): + """ + Private method to synchronize a local directory to the device. + + @param hostDirectory name of the local directory + @type str + @param deviceDirectory name of the directory on the device + @type str + @param mirror flag indicating to mirror the local directory to + the device directory + @type bool + @param localDevice flag indicating device access via local file system + @type bool + @return list of errors + @rtype list of str + """ + errors = [] + + if not os.path.isdir(hostDirectory): + return [self.tr( + "The given name '{0}' is not a directory or does not exist.") + .format(hostDirectory) + ] + + self.rsyncProgressMessage.emit( + self.tr("Synchronizing <b>{0}</b>.").format(deviceDirectory) + ) + + doneMessage = self.tr("Done synchronizing <b>{0}</b>.").format( + deviceDirectory) + + sourceDict = {} + sourceFiles = listdirStat(hostDirectory) + for name, nstat in sourceFiles: + sourceDict[name] = nstat + + destinationDict = {} + if localDevice: + if not os.path.isdir(deviceDirectory): + # simply copy destination to source + shutil.copytree(hostDirectory, deviceDirectory) + self.rsyncProgressMessage.emit(doneMessage) + return errors + else: + destinationFiles = listdirStat(deviceDirectory) + for name, nstat in destinationFiles: + destinationDict[name] = nstat + else: + try: + destinationFiles = self.__commandsInterface.lls( + deviceDirectory, fullstat=True) + except Exception as exc: + return [str(exc)] + if destinationFiles is None: + # the destination directory does not exist + try: + self.__commandsInterface.mkdir(deviceDirectory) + except Exception as exc: + return [str(exc)] + else: + for name, nstat in destinationFiles: + destinationDict[name] = nstat + + destinationSet = set(destinationDict.keys()) + sourceSet = set(sourceDict.keys()) + toAdd = sourceSet - destinationSet # add to dev + toDelete = destinationSet - sourceSet # delete from dev + toUpdate = destinationSet.intersection(sourceSet) # update files + + if localDevice: + for sourceBasename in toAdd: + # name exists in source but not in device + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = os.path.join(deviceDirectory, sourceBasename) + self.rsyncProgressMessage.emit( + self.tr("Adding <b>{0}</b>...").format(destFilename)) + if os.path.isfile(sourceFilename): + shutil.copy2(sourceFilename, destFilename) + elif os.path.isdir(sourceFilename): + # recurse + errs = self.__rsync(sourceFilename, destFilename, + mirror=mirror, localDevice=localDevice) + # just note issues but ignore them otherwise + errors.extend(errs) + + if mirror: + for destBasename in toDelete: + # name exists in device but not local, delete + destFilename = os.path.join(deviceDirectory, destBasename) + if os.path.isdir(destFilename): + shutil.rmtree(destFilename, ignore_errors=True) + elif os.path.isfile(destFilename): + os.remove(destFilename) + + for sourceBasename in toUpdate: + # names exist in both; do an update + sourceStat = sourceDict[sourceBasename] + destStat = destinationDict[sourceBasename] + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = os.path.join(deviceDirectory, sourceBasename) + destMode = destStat[0] + if os.path.isdir(sourceFilename): + if os.path.isdir(destFilename): + # both are directories => recurs + errs = self.__rsync(sourceFilename, destFilename, + mirror=mirror, + localDevice=localDevice) + # just note issues but ignore them otherwise + errors.extend(errs) + else: + self.rsyncProgressMessage.emit( + self.tr("Source <b>{0}</b> is a directory and" + " destination <b>{1}</b> is a file." + " Ignoring it.") + .format(sourceFilename, destFilename) + ) + else: + if os.path.isdir(destFilename): + self.rsyncProgressMessage.emit( + self.tr("Source <b>{0}</b> is a file and" + " destination <b>{1}</b> is a directory." + " Ignoring it.") + .format(sourceFilename, destFilename) + ) + else: + if sourceStat[8] > destStat[8]: # mtime + self.rsyncProgressMessage.emit( + self.tr("Updating <b>{0}</b>...") + .format(destFilename) + ) + shutil.copy2(sourceFilename, destFilename) + else: + for sourceBasename in toAdd: + # name exists in source but not in device + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = deviceDirectory + "/" + sourceBasename + self.rsyncProgressMessage.emit( + self.tr("Adding <b>{0}</b>...").format(destFilename)) + if os.path.isfile(sourceFilename): + try: + self.__commandsInterface.put(sourceFilename, + destFilename) + except Exception as exc: + # just note issues but ignore them otherwise + errors.append(str(exc)) + elif os.path.isdir(sourceFilename): + # recurse + errs = self.__rsync(sourceFilename, destFilename, + mirror=mirror) + # just note issues but ignore them otherwise + errors.extend(errs) + + if mirror: + for destBasename in toDelete: + # name exists in device but not local, delete + destFilename = deviceDirectory + "/" + destBasename + self.rsyncProgressMessage.emit( + self.tr("Removing <b>{0}</b>...").format(destFilename)) + try: + self.__commandsInterface.rmrf(destFilename, + recursive=True, + force=True) + except Exception as exc: + # just note issues but ignore them otherwise + errors.append(str(exc)) + + for sourceBasename in toUpdate: + # names exist in both; do an update + sourceStat = sourceDict[sourceBasename] + destStat = destinationDict[sourceBasename] + sourceFilename = os.path.join(hostDirectory, sourceBasename) + destFilename = deviceDirectory + "/" + sourceBasename + destMode = destStat[0] + if os.path.isdir(sourceFilename): + if stat.S_ISDIR(destMode): + # both are directories => recurs + errs = self.__rsync(sourceFilename, destFilename, + mirror=mirror) + # just note issues but ignore them otherwise + errors.extend(errs) + else: + self.rsyncProgressMessage.emit( + self.tr("Source <b>{0}</b> is a directory and" + " destination <b>{1}</b> is a file." + " Ignoring it.") + .format(sourceFilename, destFilename) + ) + else: + if stat.S_ISDIR(destMode): + self.rsyncProgressMessage.emit( + self.tr("Source <b>{0}</b> is a file and" + " destination <b>{1}</b> is a directory." + " Ignoring it.") + .format(sourceFilename, destFilename) + ) + else: + if sourceStat[8] > destStat[8]: # mtime + self.rsyncProgressMessage.emit( + self.tr("Updating <b>{0}</b>...") + .format(destFilename) + ) + try: + self.__commandsInterface.put(sourceFilename, + destFilename) + except Exception as exc: + errors.append(str(exc)) + + self.rsyncProgressMessage.emit(doneMessage) + + return errors + + @pyqtSlot(str, str) + @pyqtSlot(str, str, bool) + @pyqtSlot(str, str, bool, bool) + def rsync(self, hostDirectory, deviceDirectory, mirror=True, + localDevice=False): + """ + Public slot to synchronize a local directory to the device. + + @param hostDirectory name of the local directory + @type str + @param deviceDirectory name of the directory on the device + @type str + @param mirror flag indicating to mirror the local directory to + the device directory + @type bool + @param localDevice flag indicating device access via local file system + @type bool + """ + errors = self.__rsync(hostDirectory, deviceDirectory, mirror=mirror, + localDevice=localDevice) + if errors: + self.error.emit("rsync", "\n".join(errors)) + + self.rsyncDone.emit(hostDirectory, deviceDirectory) + + @pyqtSlot(str) + def mkdir(self, dirname): + """ + Public slot to create a new directory. + + @param dirname name of the directory to create + @type str + """ + try: + self.__commandsInterface.mkdir(dirname) + self.createDirectoryDone.emit() + except Exception as exc: + self.error.emit("mkdir", str(exc)) + + @pyqtSlot(str) + @pyqtSlot(str, bool) + def rmdir(self, dirname, recursive=False): + """ + Public slot to (recursively) remove a directory. + + @param dirname name of the directory to be removed + @type str + @param recursive flag indicating a recursive removal + @type bool + """ + try: + if recursive: + self.__commandsInterface.rmrf(dirname, recursive=True, + force=True) + else: + self.__commandsInterface.rmdir(dirname) + self.removeDirectoryDone.emit() + except Exception as exc: + self.error.emit("rmdir", str(exc)) + + def fileSystemInfo(self): + """ + Public method to obtain information about the currently mounted file + systems. + """ + try: + fsinfo = self.__commandsInterface.fileSystemInfo() + self.fsinfoDone.emit(fsinfo) + except Exception as exc: + self.error.emit("fileSystemInfo", str(exc))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,954 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a file manager for MicroPython devices. +""" + +from __future__ import unicode_literals + +import os +import shutil + +from PyQt5.QtCore import pyqtSlot, Qt, QPoint +from PyQt5.QtWidgets import ( + QWidget, QTreeWidgetItem, QHeaderView, QMenu, QInputDialog, QLineEdit, + QDialog +) + +from E5Gui import E5MessageBox, E5PathPickerDialog +from E5Gui.E5PathPicker import E5PathPickerModes +from E5Gui.E5FileSaveConfirmDialog import confirmOverwrite +from E5Gui.E5Application import e5App + +from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget + +from .MicroPythonFileManager import MicroPythonFileManager +from .MicroPythonFileSystemUtilities import ( + mtime2string, mode2string, decoratedName, listdirStat +) + +from UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog + +import UI.PixmapCache +import Preferences +import Utilities +import Globals + + +class MicroPythonFileManagerWidget(QWidget, Ui_MicroPythonFileManagerWidget): + """ + Class implementing a file manager for MicroPython devices. + """ + def __init__(self, commandsInterface, deviceWithLocalAccess, parent=None): + """ + Constructor + + @param commandsInterface reference to the commands interface object + @type MicroPythonCommandsInterface + @param deviceWithLocalAccess flag indicating the device supports file + access via a local directory + @type bool + @param parent reference to the parent widget + @type QWidget + """ + super(MicroPythonFileManagerWidget, self).__init__(parent) + self.setupUi(self) + + self.__repl = parent + self.__deviceWithLocalAccess = deviceWithLocalAccess + + self.syncButton.setIcon(UI.PixmapCache.getIcon("2rightarrow")) + self.putButton.setIcon(UI.PixmapCache.getIcon("1rightarrow")) + self.putAsButton.setIcon(UI.PixmapCache.getIcon("putAs")) + self.getButton.setIcon(UI.PixmapCache.getIcon("1leftarrow")) + self.getAsButton.setIcon(UI.PixmapCache.getIcon("getAs")) + self.localUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) + self.localReloadButton.setIcon(UI.PixmapCache.getIcon("reload")) + self.deviceUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) + self.deviceReloadButton.setIcon(UI.PixmapCache.getIcon("reload")) + + self.deviceUpButton.setEnabled(not self.__repl.isMicrobit()) + + self.putButton.setEnabled(False) + self.putAsButton.setEnabled(False) + self.getButton.setEnabled(False) + self.getAsButton.setEnabled(False) + + self.localFileTreeWidget.header().setSortIndicator( + 0, Qt.AscendingOrder) + self.deviceFileTreeWidget.header().setSortIndicator( + 0, Qt.AscendingOrder) + + self.__progressInfoDialog = None + self.__fileManager = MicroPythonFileManager(commandsInterface, self) + + self.__fileManager.longListFiles.connect(self.__handleLongListFiles) + self.__fileManager.currentDir.connect(self.__handleCurrentDir) + self.__fileManager.currentDirChanged.connect(self.__handleCurrentDir) + self.__fileManager.putFileDone.connect(self.__newDeviceList) + self.__fileManager.getFileDone.connect(self.__handleGetDone) + self.__fileManager.rsyncDone.connect(self.__handleRsyncDone) + self.__fileManager.rsyncProgressMessage.connect( + self.__handleRsyncProgressMessage) + self.__fileManager.removeDirectoryDone.connect(self.__newDeviceList) + self.__fileManager.createDirectoryDone.connect(self.__newDeviceList) + self.__fileManager.deleteFileDone.connect(self.__newDeviceList) + self.__fileManager.fsinfoDone.connect(self.__fsInfoResultReceived) + + self.__fileManager.error.connect(self.__handleError) + + self.localFileTreeWidget.customContextMenuRequested.connect( + self.__showLocalContextMenu) + self.deviceFileTreeWidget.customContextMenuRequested.connect( + self.__showDeviceContextMenu) + + self.__localMenu = QMenu(self) + self.__localMenu.addAction(self.tr("Change Directory"), + self.__changeLocalDirectory) + self.__localMenu.addAction( + self.tr("Create Directory"), self.__createLocalDirectory) + self.__localDelDirTreeAct = self.__localMenu.addAction( + self.tr("Delete Directory Tree"), self.__deleteLocalDirectoryTree) + self.__localMenu.addSeparator() + self.__localDelFileAct = self.__localMenu.addAction( + self.tr("Delete File"), self.__deleteLocalFile) + self.__localMenu.addSeparator() + act = self.__localMenu.addAction(self.tr("Show Hidden Files")) + act.setCheckable(True) + act.setChecked(Preferences.getMicroPython("ShowHiddenLocal")) + act.triggered[bool].connect(self.__localHiddenChanged) + + self.__deviceMenu = QMenu(self) + if not self.__repl.isMicrobit(): + self.__deviceMenu.addAction( + self.tr("Change Directory"), self.__changeDeviceDirectory) + self.__deviceMenu.addAction( + self.tr("Create Directory"), self.__createDeviceDirectory) + if not self.__deviceWithLocalAccess: + self.__devDelDirAct = self.__deviceMenu.addAction( + self.tr("Delete Directory"), self.__deleteDeviceDirectory) + self.__devDelDirTreeAct = self.__deviceMenu.addAction( + self.tr("Delete Directory Tree"), + self.__deleteDeviceDirectoryTree) + self.__deviceMenu.addSeparator() + self.__devDelFileAct = self.__deviceMenu.addAction( + self.tr("Delete File"), self.__deleteDeviceFile) + self.__deviceMenu.addSeparator() + act = self.__deviceMenu.addAction(self.tr("Show Hidden Files")) + act.setCheckable(True) + act.setChecked(Preferences.getMicroPython("ShowHiddenDevice")) + act.triggered[bool].connect(self.__deviceHiddenChanged) + if not parent.isMicrobit(): + self.__deviceMenu.addSeparator() + self.__deviceMenu.addAction( + self.tr("Show Filesystem Info"), self.__showFileSystemInfo) + + def start(self): + """ + Public method to start the widget. + """ + dirname = "" + vm = e5App().getObject("ViewManager") + aw = vm.activeWindow() + if aw: + dirname = os.path.dirname(aw.getFileName()) + if not dirname: + dirname = (Preferences.getMultiProject("Workspace") or + os.path.expanduser("~")) + self.__listLocalFiles(dirname) + + if self.__deviceWithLocalAccess: + dirname = self.__repl.getDeviceWorkspace() + if dirname: + self.__listLocalFiles(dirname, True) + else: + self.__fileManager.pwd() + + def stop(self): + """ + Public method to stop the widget. + """ + pass + + @pyqtSlot(str, str) + def __handleError(self, method, error): + """ + Private slot to handle errors. + + @param method name of the method the error occured in + @type str + @param error error message + @type str + """ + E5MessageBox.warning( + self, + self.tr("Error handling device"), + self.tr("<p>There was an error communicating with the connected" + " device.</p><p>Method: {0}</p><p>Message: {1}</p>") + .format(method, error)) + + @pyqtSlot(str) + def __handleCurrentDir(self, dirname): + """ + Private slot to handle a change of the current directory of the device. + + @param dirname name of the current directory + @type str + """ + self.deviceCwd.setText(dirname) + self.__newDeviceList() + + @pyqtSlot(tuple) + def __handleLongListFiles(self, filesList): + """ + Private slot to receive a long directory listing. + + @param filesList tuple containing tuples with name, mode, size and time + for each directory entry + @type tuple of (str, str, str, str) + """ + self.deviceFileTreeWidget.clear() + for name, mode, size, dateTime in filesList: + itm = QTreeWidgetItem(self.deviceFileTreeWidget, + [name, mode, size, dateTime]) + itm.setTextAlignment(1, Qt.AlignHCenter) + itm.setTextAlignment(2, Qt.AlignRight) + self.deviceFileTreeWidget.header().resizeSections( + QHeaderView.ResizeToContents) + + def __listLocalFiles(self, dirname="", localDevice=False): + """ + Private method to populate the local files list. + + @param dirname name of the local directory to be listed + @type str + @param localDevice flag indicating device access via local file system + @type bool + """ + if not dirname: + dirname = os.getcwd() + if dirname.endswith(os.sep): + dirname = dirname[:-1] + if localDevice: + self.deviceCwd.setText(dirname) + showHidden = Preferences.getMicroPython("ShowHiddenDevice") + else: + self.localCwd.setText(dirname) + showHidden = Preferences.getMicroPython("ShowHiddenLocal") + + filesStatList = listdirStat(dirname, showHidden=showHidden) + filesList = [( + decoratedName(f, s[0], os.path.isdir(os.path.join(dirname, f))), + mode2string(s[0]), + str(s[6]), + mtime2string(s[8])) for f, s in filesStatList] + if localDevice: + fileTreeWidget = self.deviceFileTreeWidget + else: + fileTreeWidget = self.localFileTreeWidget + fileTreeWidget.clear() + for item in filesList: + itm = QTreeWidgetItem(fileTreeWidget, item) + itm.setTextAlignment(1, Qt.AlignHCenter) + itm.setTextAlignment(2, Qt.AlignRight) + fileTreeWidget.header().resizeSections( + QHeaderView.ResizeToContents) + + @pyqtSlot(QTreeWidgetItem, int) + def on_localFileTreeWidget_itemActivated(self, item, column): + """ + Private slot to handle the activation of a local item. + + If the item is a directory, the list will be re-populated for this + directory. + + @param item reference to the activated item + @type QTreeWidgetItem + @param column column of the activation + @type int + """ + name = os.path.join(self.localCwd.text(), item.text(0)) + if name.endswith("/"): + # directory names end with a '/' + self.__listLocalFiles(name[:-1]) + elif Utilities.MimeTypes.isTextFile(name): + e5App().getObject("ViewManager").getEditor(name) + + @pyqtSlot() + def on_localFileTreeWidget_itemSelectionChanged(self): + """ + Private slot handling a change of selection in the local pane. + """ + enable = bool(len(self.localFileTreeWidget.selectedItems())) + if enable: + enable &= not ( + self.localFileTreeWidget.selectedItems()[0].text(0) + .endswith("/")) + self.putButton.setEnabled(enable) + self.putAsButton.setEnabled(enable) + + @pyqtSlot() + def on_localUpButton_clicked(self): + """ + Private slot to go up one directory level. + """ + cwd = self.localCwd.text() + dirname = os.path.dirname(cwd) + self.__listLocalFiles(dirname) + + @pyqtSlot() + def on_localReloadButton_clicked(self): + """ + Private slot to reload the local list. + """ + dirname = self.localCwd.text() + self.__listLocalFiles(dirname) + + @pyqtSlot(QTreeWidgetItem, int) + def on_deviceFileTreeWidget_itemActivated(self, item, column): + """ + Private slot to handle the activation of a device item. + + If the item is a directory, the current working directory is changed + and the list will be re-populated for this directory. + + @param item reference to the activated item + @type QTreeWidgetItem + @param column column of the activation + @type int + """ + name = os.path.join(self.deviceCwd.text(), item.text(0)) + if self.__deviceWithLocalAccess: + if name.endswith("/"): + # directory names end with a '/' + self.__listLocalFiles(name[:-1], True) + elif Utilities.MimeTypes.isTextFile(name): + e5App().getObject("ViewManager").getEditor(name) + else: + if name.endswith("/"): + # directory names end with a '/' + self.__fileManager.cd(name[:-1]) + + @pyqtSlot() + def on_deviceFileTreeWidget_itemSelectionChanged(self): + """ + Private slot handling a change of selection in the local pane. + """ + enable = bool(len(self.deviceFileTreeWidget.selectedItems())) + if enable: + enable &= not ( + self.deviceFileTreeWidget.selectedItems()[0].text(0) + .endswith("/")) + self.getButton.setEnabled(enable) + self.getAsButton.setEnabled(enable) + + @pyqtSlot() + def on_deviceUpButton_clicked(self): + """ + Private slot to go up one directory level on the device. + """ + cwd = self.deviceCwd.text() + dirname = os.path.dirname(cwd) + if self.__deviceWithLocalAccess: + self.__listLocalFiles(dirname, True) + else: + self.__fileManager.cd(dirname) + + @pyqtSlot() + def on_deviceReloadButton_clicked(self): + """ + Private slot to reload the device list. + """ + dirname = self.deviceCwd.text() + if self.__deviceWithLocalAccess: + self.__listLocalFiles(dirname, True) + else: + if dirname: + self.__newDeviceList() + else: + self.__fileManager.pwd() + + def __isFileInList(self, filename, treeWidget): + """ + Private method to check, if a file name is contained in a tree widget. + + @param filename name of the file to check + @type str + @param treeWidget reference to the tree widget to be checked against + @return flag indicating that the file name is present + @rtype bool + """ + itemCount = treeWidget.topLevelItemCount() + if itemCount: + for row in range(itemCount): + if treeWidget.topLevelItem(row).text(0) == filename: + return True + + return False + + @pyqtSlot() + def on_putButton_clicked(self, putAs=False): + """ + Private slot to copy the selected file to the connected device. + + @param putAs flag indicating to give it a new name + @type bool + """ + selectedItems = self.localFileTreeWidget.selectedItems() + if selectedItems: + filename = selectedItems[0].text(0).strip() + if not filename.endswith("/"): + # it is really a file + if putAs: + deviceFilename, ok = QInputDialog.getText( + self, + self.tr("Put File As"), + self.tr("Enter a new name for the file"), + QLineEdit.Normal, + filename) + if not ok or not filename: + return + else: + deviceFilename = filename + + if self.__isFileInList(deviceFilename, + self.deviceFileTreeWidget): + # ask for overwrite permission + action, resultFilename = confirmOverwrite( + deviceFilename, self.tr("Copy File to Device"), + self.tr("The given file exists already" + " (Enter file name only)."), + False, self) + if action == "cancel": + return + elif action == "rename": + deviceFilename = os.path.basename(resultFilename) + + if self.__deviceWithLocalAccess: + shutil.copy2( + os.path.join(self.localCwd.text(), filename), + os.path.join(self.deviceCwd.text(), deviceFilename) + ) + self.__listLocalFiles(self.deviceCwd.text(), + localDevice=True) + else: + deviceCwd = self.deviceCwd.text() + if deviceCwd: + if deviceCwd != "/": + deviceFilename = deviceCwd + "/" + deviceFilename + else: + deviceFilename = "/" + deviceFilename + self.__fileManager.put( + os.path.join(self.localCwd.text(), filename), + deviceFilename + ) + + @pyqtSlot() + def on_putAsButton_clicked(self): + """ + Private slot to copy the selected file to the connected device + with a different name. + """ + self.on_putButton_clicked(putAs=True) + + @pyqtSlot() + def on_getButton_clicked(self, getAs=False): + """ + Private slot to copy the selected file from the connected device. + + @param getAs flag indicating to give it a new name + @type bool + """ + selectedItems = self.deviceFileTreeWidget.selectedItems() + if selectedItems: + filename = selectedItems[0].text(0).strip() + if not filename.endswith("/"): + # it is really a file + if getAs: + localFilename, ok = QInputDialog.getText( + self, + self.tr("Get File As"), + self.tr("Enter a new name for the file"), + QLineEdit.Normal, + filename) + if not ok or not filename: + return + else: + localFilename = filename + + if self.__isFileInList(localFilename, + self.localFileTreeWidget): + # ask for overwrite permission + action, resultFilename = confirmOverwrite( + localFilename, self.tr("Copy File from Device"), + self.tr("The given file exists already."), + True, self) + if action == "cancel": + return + elif action == "rename": + localFilename = resultFilename + + if self.__deviceWithLocalAccess: + shutil.copy2( + os.path.join(self.deviceCwd.text(), filename), + os.path.join(self.localCwd.text(), localFilename) + ) + self.__listLocalFiles(self.localCwd.text()) + else: + deviceCwd = self.deviceCwd.text() + if deviceCwd: + filename = deviceCwd + "/" + filename + self.__fileManager.get( + filename, + os.path.join(self.localCwd.text(), localFilename) + ) + + @pyqtSlot() + def on_getAsButton_clicked(self): + """ + Private slot to copy the selected file from the connected device + with a different name. + """ + self.on_getButton_clicked(getAs=True) + + @pyqtSlot(str, str) + def __handleGetDone(self, deviceFile, localFile): + """ + Private slot handling a successful copy of a file from the device. + + @param deviceFile name of the file on the device + @type str + @param localFile name of the local file + @type str + """ + self.__listLocalFiles(self.localCwd.text()) + + @pyqtSlot() + def on_syncButton_clicked(self): + """ + Private slot to synchronize the local directory to the device. + """ + self.__fileManager.rsync( + self.localCwd.text(), + self.deviceCwd.text(), + mirror=True, + localDevice=self.__deviceWithLocalAccess, + ) + + @pyqtSlot(str, str) + def __handleRsyncDone(self, localDir, deviceDir): + """ + Private method to handle the completion of the rsync operation. + + @param localDir name of the local directory + @type str + @param deviceDir name of the device directory + @type str + """ + self.__listLocalFiles(self.localCwd.text()) + self.__newDeviceList() + + @pyqtSlot(str) + def __handleRsyncProgressMessage(self, message): + """ + Private slot handling progress messages sent by the file manager. + + @param message message to be shown + @type str + """ + if self.__progressInfoDialog is None: + from .MicroPythonProgressInfoDialog import ( + MicroPythonProgressInfoDialog + ) + self.__progressInfoDialog = MicroPythonProgressInfoDialog(self) + self.__progressInfoDialog.finished.connect( + self.__progressInfoDialogFinished) + self.__progressInfoDialog.show() + self.__progressInfoDialog.addMessage(message) + + @pyqtSlot() + def __progressInfoDialogFinished(self): + """ + Private slot handling the closing of the progress info dialog. + """ + self.__progressInfoDialog.deleteLater() + self.__progressInfoDialog = None + + @pyqtSlot() + def __newDeviceList(self): + """ + Private slot to initiate a new long list of the device directory. + """ + self.__fileManager.lls( + self.deviceCwd.text(), + showHidden=Preferences.getMicroPython("ShowHiddenDevice") + ) + + ################################################################## + ## Context menu methods for the local files below + ################################################################## + + @pyqtSlot(QPoint) + def __showLocalContextMenu(self, pos): + """ + Private slot to show the REPL context menu. + + @param pos position to show the menu at + @type QPoint + """ + hasSelection = bool(len(self.localFileTreeWidget.selectedItems())) + if hasSelection: + name = self.localFileTreeWidget.selectedItems()[0].text(0) + isDir = name.endswith("/") + isFile = not isDir + else: + isDir = False + isFile = False + self.__localDelDirTreeAct.setEnabled(isDir) + self.__localDelFileAct.setEnabled(isFile) + + self.__localMenu.exec_(self.localFileTreeWidget.mapToGlobal(pos)) + + @pyqtSlot() + def __changeLocalDirectory(self, localDevice=False): + """ + Private slot to change the local directory. + + @param localDevice flag indicating device access via local file system + @type bool + """ + if localDevice: + cwdWidget = self.deviceCwd + else: + cwdWidget = self.localCwd + + dirPath, ok = E5PathPickerDialog.getPath( + self, + self.tr("Change Directory"), + self.tr("Select Directory"), + E5PathPickerModes.DirectoryShowFilesMode, + path=cwdWidget.text(), + defaultDirectory=cwdWidget.text(), + ) + if ok and dirPath: + if not os.path.isabs(dirPath): + dirPath = os.path.join(cwdWidget.text(), dirPath) + cwdWidget.setText(dirPath) + self.__listLocalFiles(dirPath, localDevice=localDevice) + + @pyqtSlot() + def __createLocalDirectory(self, localDevice=False): + """ + Private slot to create a local directory. + + @param localDevice flag indicating device access via local file system + @type bool + """ + if localDevice: + cwdWidget = self.deviceCwd + else: + cwdWidget = self.localCwd + + dirPath, ok = QInputDialog.getText( + self, + self.tr("Create Directory"), + self.tr("Enter directory name:"), + QLineEdit.Normal) + if ok and dirPath: + dirPath = os.path.join(cwdWidget.text(), dirPath) + try: + os.mkdir(dirPath) + self.__listLocalFiles(cwdWidget.text(), + localDevice=localDevice) + except (OSError, IOError) as exc: + E5MessageBox.critical( + self, + self.tr("Create Directory"), + self.tr("""<p>The directory <b>{0}</b> could not be""" + """ created.</p><p>Reason: {1}</p>""").format( + dirPath, str(exc)) + ) + + @pyqtSlot() + def __deleteLocalDirectoryTree(self, localDevice=False): + """ + Private slot to delete a local directory tree. + + @param localDevice flag indicating device access via local file system + @type bool + """ + if localDevice: + cwdWidget = self.deviceCwd + fileTreeWidget = self.deviceFileTreeWidget + else: + cwdWidget = self.localCwd + fileTreeWidget = self.localFileTreeWidget + + if bool(len(fileTreeWidget.selectedItems())): + name = fileTreeWidget.selectedItems()[0].text(0) + dirname = os.path.join(cwdWidget.text(), name[:-1]) + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete Directory Tree"), + self.tr( + "Do you really want to delete this directory tree?"), + [dirname]) + if dlg.exec_() == QDialog.Accepted: + try: + shutil.rmtree(dirname) + self.__listLocalFiles(cwdWidget.text(), + localDevice=localDevice) + except Exception as exc: + E5MessageBox.critical( + self, + self.tr("Delete Directory Tree"), + self.tr("""<p>The directory <b>{0}</b> could not be""" + """ deleted.</p><p>Reason: {1}</p>""").format( + dirname, str(exc)) + ) + + @pyqtSlot() + def __deleteLocalFile(self, localDevice=False): + """ + Private slot to delete a local file. + + @param localDevice flag indicating device access via local file system + @type bool + """ + if localDevice: + cwdWidget = self.deviceCwd + fileTreeWidget = self.deviceFileTreeWidget + else: + cwdWidget = self.localCwd + fileTreeWidget = self.localFileTreeWidget + + if bool(len(fileTreeWidget.selectedItems())): + name = fileTreeWidget.selectedItems()[0].text(0) + filename = os.path.join(cwdWidget.text(), name) + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete File"), + self.tr( + "Do you really want to delete this file?"), + [filename]) + if dlg.exec_() == QDialog.Accepted: + try: + os.remove(filename) + self.__listLocalFiles(cwdWidget.text(), + localDevice=localDevice) + except (OSError, IOError) as exc: + E5MessageBox.critical( + self, + self.tr("Delete File"), + self.tr("""<p>The file <b>{0}</b> could not be""" + """ deleted.</p><p>Reason: {1}</p>""").format( + filename, str(exc)) + ) + + @pyqtSlot(bool) + def __localHiddenChanged(self, checked): + """ + Private slot handling a change of the local show hidden menu entry. + + @param checked new check state of the action + @type bool + """ + Preferences.setMicroPython("ShowHiddenLocal", checked) + self.on_localReloadButton_clicked() + + ################################################################## + ## Context menu methods for the device files below + ################################################################## + + @pyqtSlot(QPoint) + def __showDeviceContextMenu(self, pos): + """ + Private slot to show the REPL context menu. + + @param pos position to show the menu at + @type QPoint + """ + hasSelection = bool(len(self.deviceFileTreeWidget.selectedItems())) + if hasSelection: + name = self.deviceFileTreeWidget.selectedItems()[0].text(0) + isDir = name.endswith("/") + isFile = not isDir + else: + isDir = False + isFile = False + if not self.__repl.isMicrobit(): + if not self.__deviceWithLocalAccess: + self.__devDelDirAct.setEnabled(isDir) + self.__devDelDirTreeAct.setEnabled(isDir) + self.__devDelFileAct.setEnabled(isFile) + + self.__deviceMenu.exec_(self.deviceFileTreeWidget.mapToGlobal(pos)) + + @pyqtSlot() + def __changeDeviceDirectory(self): + """ + Private slot to change the current directory of the device. + + Note: This triggers a re-population of the device list for the new + current directory. + """ + if self.__deviceWithLocalAccess: + self.__changeLocalDirectory(True) + else: + dirPath, ok = QInputDialog.getText( + self, + self.tr("Change Directory"), + self.tr("Enter the directory path on the device:"), + QLineEdit.Normal, + self.deviceCwd.text()) + if ok and dirPath: + if not dirPath.startswith("/"): + dirPath = self.deviceCwd.text() + "/" + dirPath + self.__fileManager.cd(dirPath) + + @pyqtSlot() + def __createDeviceDirectory(self): + """ + Private slot to create a directory on the device. + """ + if self.__deviceWithLocalAccess: + self.__createLocalDirectory(True) + else: + dirPath, ok = QInputDialog.getText( + self, + self.tr("Create Directory"), + self.tr("Enter directory name:"), + QLineEdit.Normal) + if ok and dirPath: + self.__fileManager.mkdir(dirPath) + + @pyqtSlot() + def __deleteDeviceDirectory(self): + """ + Private slot to delete an empty directory on the device. + """ + if self.__deviceWithLocalAccess: + self.__deleteLocalDirectoryTree(True) + else: + if bool(len(self.deviceFileTreeWidget.selectedItems())): + name = self.deviceFileTreeWidget.selectedItems()[0].text(0) + cwd = self.deviceCwd.text() + if cwd: + if cwd != "/": + dirname = cwd + "/" + name[:-1] + else: + dirname = "/" + name[:-1] + else: + dirname = name[:-1] + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete Directory"), + self.tr( + "Do you really want to delete this directory?"), + [dirname]) + if dlg.exec_() == QDialog.Accepted: + self.__fileManager.rmdir(dirname) + + @pyqtSlot() + def __deleteDeviceDirectoryTree(self): + """ + Private slot to delete a directory and all its subdirectories + recursively. + """ + if self.__deviceWithLocalAccess: + self.__deleteLocalDirectoryTree(True) + else: + if bool(len(self.deviceFileTreeWidget.selectedItems())): + name = self.deviceFileTreeWidget.selectedItems()[0].text(0) + cwd = self.deviceCwd.text() + if cwd: + if cwd != "/": + dirname = cwd + "/" + name[:-1] + else: + dirname = "/" + name[:-1] + else: + dirname = name[:-1] + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete Directory Tree"), + self.tr( + "Do you really want to delete this directory tree?"), + [dirname]) + if dlg.exec_() == QDialog.Accepted: + self.__fileManager.rmdir(dirname, recursive=True) + + @pyqtSlot() + def __deleteDeviceFile(self): + """ + Private slot to delete a file. + """ + if self.__deviceWithLocalAccess: + self.__deleteLocalFile(True) + else: + if bool(len(self.deviceFileTreeWidget.selectedItems())): + name = self.deviceFileTreeWidget.selectedItems()[0].text(0) + dirname = self.deviceCwd.text() + if dirname: + if dirname != "/": + filename = dirname + "/" + name + else: + filename = "/" + name + else: + filename = name + dlg = DeleteFilesConfirmationDialog( + self, + self.tr("Delete File"), + self.tr( + "Do you really want to delete this file?"), + [filename]) + if dlg.exec_() == QDialog.Accepted: + self.__fileManager.delete(filename) + + @pyqtSlot(bool) + def __deviceHiddenChanged(self, checked): + """ + Private slot handling a change of the device show hidden menu entry. + + @param checked new check state of the action + @type bool + """ + Preferences.setMicroPython("ShowHiddenDevice", checked) + self.on_deviceReloadButton_clicked() + + @pyqtSlot() + def __showFileSystemInfo(self): + """ + Private slot to show some file system information. + """ + self.__fileManager.fileSystemInfo() + + @pyqtSlot(tuple) + def __fsInfoResultReceived(self, fsinfo): + """ + Private slot to show the file system information of the device. + + @param fsinfo tuple of tuples containing the file system name, the + total size, the used size and the free size + @type tuple of tuples of (str, int, int, int) + """ + msg = self.tr("<h3>Filesystem Information</h3>") + for name, totalSize, usedSize, freeSize in fsinfo: + msg += self.tr( + "<h4>{0}</h4" + "<table>" + "<tr><td>Total Size: </td><td align='right'>{1}</td></tr>" + "<tr><td>Used Size: </td><td align='right'>{2}</td></tr>" + "<tr><td>Free Size: </td><td align='right'>{3}</td></tr>" + "</table>" + ).format(name, + Globals.dataString(totalSize), + Globals.dataString(usedSize), + Globals.dataString(freeSize), + ) + E5MessageBox.information( + self, + self.tr("Filesystem Information"), + msg)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonFileManagerWidget.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,279 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MicroPythonFileManagerWidget</class> + <widget class="QWidget" name="MicroPythonFileManagerWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>675</width> + <height>338</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Local Files</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QLabel" name="label_2"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Device Files</string> + </property> + </widget> + </item> + <item> + <widget class="E5Led" name="deviceConnectedLed" native="true"/> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QTreeWidget" name="localFileTreeWidget"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Mode</string> + </property> + </column> + <column> + <property name="text"> + <string>Size</string> + </property> + </column> + <column> + <property name="text"> + <string>Time</string> + </property> + </column> + </widget> + </item> + <item row="1" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <spacer name="verticalSpacer_2"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>26</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="syncButton"> + <property name="toolTip"> + <string>Press to sync the local directory to the device directory</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="putButton"> + <property name="toolTip"> + <string>Press to copy the selected file to the device</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="putAsButton"> + <property name="toolTip"> + <string>Press to copy the selected file to the device with a new name</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="getButton"> + <property name="toolTip"> + <string>Press to copy the selected file from the device</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="getAsButton"> + <property name="toolTip"> + <string>Press to copy the selected file from the device with a new name</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>26</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item row="1" column="2"> + <widget class="QTreeWidget" name="deviceFileTreeWidget"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="headerShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Mode</string> + </property> + </column> + <column> + <property name="text"> + <string>Size</string> + </property> + </column> + <column> + <property name="text"> + <string>Time</string> + </property> + </column> + </widget> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <widget class="QLineEdit" name="localCwd"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="localUpButton"> + <property name="toolTip"> + <string>Press to move one directory level up</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="localReloadButton"> + <property name="toolTip"> + <string>Press to reload the list</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="2" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <property name="spacing"> + <number>2</number> + </property> + <item> + <widget class="QLineEdit" name="deviceCwd"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="deviceUpButton"> + <property name="toolTip"> + <string>Press to move one directory level up</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="deviceReloadButton"> + <property name="toolTip"> + <string>Press to reload the list</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5Led</class> + <extends>QWidget</extends> + <header>E5Gui/E5Led.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>localFileTreeWidget</tabstop> + <tabstop>deviceFileTreeWidget</tabstop> + <tabstop>syncButton</tabstop> + <tabstop>putButton</tabstop> + <tabstop>putAsButton</tabstop> + <tabstop>getButton</tabstop> + <tabstop>getAsButton</tabstop> + <tabstop>localCwd</tabstop> + <tabstop>localUpButton</tabstop> + <tabstop>localReloadButton</tabstop> + <tabstop>deviceCwd</tabstop> + <tabstop>deviceUpButton</tabstop> + <tabstop>deviceReloadButton</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonFileSystemUtilities.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing some file system utility functions. +""" + +from __future__ import unicode_literals + +import time +import stat +import os + + +def mtime2string(mtime): + """ + Function to convert a time value to a string representation. + + @param mtime time value + @type int + @return string representation of the given time + @rtype str + """ + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)) + + +def mode2string(mode): + """ + Function to convert a mode value to a string representation. + + @param mode mode value + @type int + @return string representation of the given mode value + @rtype str + """ + return stat.filemode(mode) + + +def decoratedName(name, mode, isDir=False): + """ + Function to decorate the given name according to the given mode. + + @param name file or directory name + @type str + @param mode mode value + @type int + @param isDir flag indicating that name is a directory + @type bool + @return decorated file or directory name + @rtype str + """ + if stat.S_ISDIR(mode) or isDir: + # append a '/' for directories + return name + "/" + elif stat.S_ISLNK(mode): + # append a '@' for links + return name + "@" + else: + # no change + return name + + +def isVisible(name, showHidden): + """ + Function to check, if a filesystem entry is a hidden file or directory. + + @param name name to be checked + @type str + @param showHidden flag indicating to show hidden files as well + @type bool + @return flag indicating a visible filesystem entry + @rtype bool + """ + return ( + showHidden or + (not name.startswith(".") and not name.endswith("~")) + ) + + +def fstat(filename): + """ + Function to get the stat() of file. + + @param filename name of the file + @type str + @return tuple containing the stat() result + @rtype tuple + """ + try: + rstat = os.lstat(filename) + except Exception: + rstat = os.stat(filename) + return tuple(rstat) + + +def listdirStat(dirname, showHidden=False): + """ + Function to get a list of directory entries and associated stat() tuples. + + @param dirname name of the directory to list + @type str + @param showHidden flag indicating to show hidden files as well + @type bool + @return list of tuples containing the entry name and the associated + stat() tuple + @rtype list of tuple of (str, tuple) + """ + try: + if dirname: + files = os.listdir(dirname) + else: + files = os.listdir() + except OSError: + return [] + + if dirname in ('', '/'): + return [(f, fstat(f)) for f in files if isVisible(f, showHidden)] + + return [(f, fstat(os.path.join(dirname, f))) for f in files + if isVisible(f, showHidden)]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonGraphWidget.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the MicroPython graph widget. +""" + +from __future__ import unicode_literals + +from collections import deque +import bisect +import os +import time +import csv + +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt +from PyQt5.QtGui import QPainter +from PyQt5.QtWidgets import ( + QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem, + QLabel, QSpinBox +) +from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis + +from E5Gui import E5MessageBox + +import UI.PixmapCache +import Preferences + + +class MicroPythonGraphWidget(QWidget): + """ + Class implementing the MicroPython graph widget. + + @signal dataFlood emitted to indicate, that too much data is received + """ + dataFlood = pyqtSignal() + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(MicroPythonGraphWidget, self).__init__(parent) + + self.__layout = QHBoxLayout() + self.__layout.setContentsMargins(2, 2, 2, 2) + self.setLayout(self.__layout) + + self.__chartView = QChartView(self) + self.__chartView.setSizePolicy( + QSizePolicy.Expanding, QSizePolicy.Expanding) + self.__layout.addWidget(self.__chartView) + + self.__verticalLayout = QVBoxLayout() + self.__verticalLayout.setContentsMargins(0, 0, 0, 0) + self.__layout.addLayout(self.__verticalLayout) + + self.__saveButton = QToolButton(self) + self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave")) + self.__saveButton.setToolTip(self.tr("Press to save the raw data")) + self.__saveButton.clicked.connect(self.on_saveButton_clicked) + self.__verticalLayout.addWidget(self.__saveButton) + self.__verticalLayout.setAlignment(self.__saveButton, Qt.AlignHCenter) + + spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum, + QSizePolicy.Expanding) + self.__verticalLayout.addItem(spacerItem) + + label = QLabel(self.tr("max. X:")) + self.__verticalLayout.addWidget(label) + self.__verticalLayout.setAlignment(label, Qt.AlignHCenter) + + self.__maxX = 100 + self.__maxXSpinBox = QSpinBox() + self.__maxXSpinBox.setMinimum(100) + self.__maxXSpinBox.setMaximum(1000) + self.__maxXSpinBox.setSingleStep(100) + self.__maxXSpinBox.setToolTip(self.tr( + "Enter the maximum number of data points to be plotted.")) + self.__maxXSpinBox.setValue(self.__maxX) + self.__maxXSpinBox.setAlignment(Qt.AlignRight) + self.__verticalLayout.addWidget(self.__maxXSpinBox) + + # holds the data to be checked for plotable data + self.__inputBuffer = [] + # holds the raw data + self.__rawData = [] + self.__dirty = False + + self.__maxY = 1000 + self.__flooded = False # flag indicating a data flood + + self.__data = [deque([0] * self.__maxX)] + self.__series = [QLineSeries()] + + # Y-axis ranges + self.__yRanges = [1, 5, 10, 25, 50, 100, 250, 500, 1000] + + # setup the chart + self.__chart = QChart() + self.__chart.legend().hide() + self.__chart.addSeries(self.__series[0]) + self.__axisX = QValueAxis() + self.__axisX.setRange(0, self.__maxX) + self.__axisX.setLabelFormat("time") + self.__axisY = QValueAxis() + self.__axisY.setRange(-self.__maxY, self.__maxY) + self.__axisY.setLabelFormat("%d") + self.__chart.setAxisX(self.__axisX, self.__series[0]) + self.__chart.setAxisY(self.__axisY, self.__series[0]) + self.__chartView.setChart(self.__chart) + self.__chartView.setRenderHint(QPainter.Antialiasing) + + self.__maxXSpinBox.valueChanged.connect(self.__handleMaxXChanged) + + @pyqtSlot(bytes) + def processData(self, data): + """ + Public slot to process the raw data. + + It takes raw bytes, checks the data for a valid tuple of ints or + floats and adds the data to the graph. If the the length of the bytes + data is greater than 1024 then a dataFlood signal is emitted to ensure + eric can take action to remain responsive. + + @param data raw data received from the connected device via the main + device widget + @type bytes + """ + # flooding guard + if self.__flooded: + return + + if len(data) > 1024: + self.__flooded = True + self.dataFlood.emit() + return + + # disable the inputs while processing data + self.__saveButton.setEnabled(False) + self.__maxXSpinBox.setEnabled(False) + + data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + self.__inputBuffer.append(data) + + # check if the data contains a Python tuple containing numbers (int + # or float) on a single line + inputBytes = b"".join(self.__inputBuffer) + lines = inputBytes.splitlines(True) + for line in lines: + if not line.endswith(b"\n"): + # incomplete line (last line); skip it + break + + line = line.strip() + if line.startswith(b"(") and line.endswith(b")"): + # it may be a tuple we are interested in + rawValues = [val.strip() for val in line[1:-1].split(b",")] + values = [] + for raw in rawValues: + try: + values.append(int(raw)) + # ok, it is an integer + continue + except ValueError: + # test for a float + pass + try: + values.append(float(raw)) + except ValueError: + # it is not an int or float, ignore it + continue + if values: + self.__addData(tuple(values)) + + self.__inputBuffer = [] + if lines[-1] and not lines[-1].endswith(b"\n"): + # Append any left over bytes for processing next time data is + # received. + self.__inputBuffer.append(lines[-1]) + + # re-enable the inputs + self.__saveButton.setEnabled(True) + self.__maxXSpinBox.setEnabled(True) + + def __addData(self, values): + """ + Private method to add a tuple of values to the graph. + + It ensures there are the required number of line series, adds the data + to the line series and updates the range of the chart so the chart + displays nicely. + + @param values tuple containing the data to be added + @type tuple of int or float + """ + # store incoming data to be able to dump it as CSV upon request + self.__rawData.append(values) + self.__dirty = True + + # check number of incoming values and adjust line series accordingly + if len(values) != len(self.__series): + valuesLen = len(values) + seriesLen = len(self.__series) + if valuesLen > seriesLen: + # add a nwe line series + for _index in range(valuesLen - seriesLen): + newSeries = QLineSeries() + self.__chart.addSeries(newSeries) + self.__chart.setAxisX(self.__axisX, newSeries) + self.__chart.setAxisY(self.__axisY, newSeries) + self.__series.append(newSeries) + self.__data.append(deque([0] * self.__maxX)) + else: + # remove obsolete line series + for oldSeries in self.__series[valuesLen:]: + self.__chart.removeSeries(oldSeries) + self.__series = self.__series[:valuesLen] + self.__data = self.__data[:valuesLen] + + # add the new values to the display and compute the maximum range + maxRanges = [] + for index, value in enumerate(values): + self.__data[index].appendleft(value) + maxRanges.append(max([max(self.__data[index]), + abs(min(self.__data[index]))])) + if len(self.__data[index]) > self.__maxX: + self.__data[index].pop() + + # re-scale the y-axis + maxYRange = max(maxRanges) + yRange = bisect.bisect_left(self.__yRanges, maxYRange) + if yRange < len(self.__yRanges): + self.__maxY = self.__yRanges[yRange] + elif maxYRange > self.__maxY: + self.__maxY += self.__maxY + elif maxYRange < self.__maxY / 2: + self.__maxY /= 2 + self.__axisY.setRange(-self.__maxY, self.__maxY) + + # ensure that floats are used to label the y-axis if the range is small + if self.__maxY <= 5: + self.__axisY.setLabelFormat("%2.2f") + else: + self.__axisY.setLabelFormat("%d") + + # update the line series + for index, series in enumerate(self.__series): + series.clear() + xyValues = [] + for x in range(self.__maxX): + value = self.__data[index][self.__maxX - 1 - x] + xyValues.append((x, value)) + for xy in xyValues: + series.append(*xy) + + @pyqtSlot() + def on_saveButton_clicked(self): + """ + Private slot to save the raw data to a CSV file. + """ + self.saveData() + + def hasData(self): + """ + Public method to check, if the chart contains some valid data. + + @return flag indicating valid data + @rtype bool + """ + return len(self.__rawData) > 0 + + def isDirty(self): + """ + Public method to check, if the chart contains unsaved data. + + @return flag indicating unsaved data + @rtype bool + """ + return self.hasData() and self.__dirty + + def saveData(self): + """ + Public method to save the dialog's raw data. + + @return flag indicating success + @rtype bool + """ + baseDir = (Preferences.getMultiProject("Workspace") or + os.path.expanduser("~")) + dataDir = os.path.join(baseDir, "data_capture") + + if not os.path.exists(dataDir): + os.makedirs(dataDir) + + # save the raw data as a CSV file + fileName = "{0}.csv".format(time.strftime("%Y%m%d-%H%M%S")) + fullPath = os.path.join(dataDir, fileName) + try: + csvFile = open(fullPath, "w") + csvWriter = csv.writer(csvFile) + csvWriter.writerows(self.__rawData) + csvFile.close() + + self.__dirty = False + return True + except (IOError, OSError) as err: + E5MessageBox.critical( + self, + self.tr("Save Chart Data"), + self.tr( + """<p>The chart data could not be saved into file""" + """ <b>{0}</b>.</p><p>Reason: {1}</p>""").format( + fullPath, str(err))) + return False + + @pyqtSlot(int) + def __handleMaxXChanged(self, value): + """ + Private slot handling a change of the max. X spin box. + + @param value value of the spin box + @type int + """ + delta = value - self.__maxX + if delta == 0: + # nothing to change + return + elif delta > 0: + # range must be increased + for deq in self.__data: + deq.extend([0] * delta) + else: + # range must be decreased + data = [] + for deq in self.__data: + data.append(deque(list(deq)[:value])) + self.__data = data + + self.__maxX = value + self.__axisX.setRange(0, self.__maxX)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonProgressInfoDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to show progress messages. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import pyqtSlot +from PyQt5.QtGui import QTextCursor +from PyQt5.QtWidgets import QDialog + +from .Ui_MicroPythonProgressInfoDialog import Ui_MicroPythonProgressInfoDialog + + +class MicroPythonProgressInfoDialog(QDialog, Ui_MicroPythonProgressInfoDialog): + """ + Class implementing a dialog to show progress messages. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(MicroPythonProgressInfoDialog, self).__init__(parent) + self.setupUi(self) + + @pyqtSlot(str) + def addMessage(self, message): + """ + Public slot to add a message to the progress display. + + @param message progress information to be shown + @type str + """ + tc = self.progressEdit.textCursor() + tc.movePosition(QTextCursor.End) + self.progressEdit.setTextCursor(tc) + self.progressEdit.appendHtml(message) + self.progressEdit.ensureCursorVisible()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonProgressInfoDialog.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MicroPythonProgressInfoDialog</class> + <widget class="QDialog" name="MicroPythonProgressInfoDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>500</width> + <height>400</height> + </rect> + </property> + <property name="windowTitle"> + <string>Progress Information</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPlainTextEdit" name="progressEdit"> + <property name="tabChangesFocus"> + <bool>true</bool> + </property> + <property name="lineWrapMode"> + <enum>QPlainTextEdit::NoWrap</enum> + </property> + <property name="readOnly"> + <bool>true</bool> + </property> + <property name="textInteractionFlags"> + <set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set> + </property> + </widget> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>MicroPythonProgressInfoDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>MicroPythonProgressInfoDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonSerialPort.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a QSerialPort with additional functionality for +MicroPython devices. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import QIODevice, QTime, QCoreApplication, QEventLoop +from PyQt5.QtSerialPort import QSerialPort + + +class MicroPythonSerialPort(QSerialPort): + """ + Class implementing a QSerialPort with additional functionality for + MicroPython devices. + """ + def __init__(self, timeout=10000, parent=None): + """ + Constructor + + @param timeout timout in milliseconds to be set + @type int + @param parent reference to the parent object + @type QObject + """ + super(MicroPythonSerialPort, self).__init__(parent) + + self.__connected = False + self.__timeout = timeout # 10s default timeout + self.__timedOut = False + + def setTimeout(self, timeout): + """ + Public method to set the timeout for device operations. + + @param timeout timout in milliseconds to be set + @type int + """ + self.__timeout = timeout + + def openSerialLink(self, port): + """ + Public method to open a serial link to a given serial port. + + @param port port name to connect to + @type str + @return flag indicating success + @rtype bool + """ + self.setPortName(port) + if self.open(QIODevice.ReadWrite): + self.setDataTerminalReady(True) + # 115.200 baud, 8N1 + self.setBaudRate(115200) + self.setDataBits(QSerialPort.Data8) + self.setParity(QSerialPort.NoParity) + self.setStopBits(QSerialPort.OneStop) + + self.__connected = True + return True + else: + return False + + def closeSerialLink(self): + """ + Public method to close the open serial connection. + """ + if self.__connected: + self.close() + + self.__connected = False + + def isConnected(self): + """ + Public method to get the connection state. + + @return flag indicating the connection state + @rtype bool + """ + return self.__connected + + def hasTimedOut(self): + """ + Public method to check, if the last 'readUntil' has timed out. + + @return flag indicating a timeout + @@rtype bool + """ + return self.__timedOut + + def readUntil(self, expected=b"\n", size=None): + r""" + Public method to read data until an expected sequence is found + (default: \n) or a specific size is exceeded. + + @param expected expected bytes sequence + @type bytes + @param size maximum data to be read + @type int + @return bytes read from the device including the expected sequence + @rtype bytes + """ + data = bytearray() + self.__timedOut = False + + t = QTime() + t.start() + while True: + QCoreApplication.processEvents(QEventLoop.ExcludeUserInputEvents) + c = bytes(self.read(1)) + if c: + data += c + if data.endswith(expected): + break + if size is not None and len(data) >= size: + break + if t.elapsed() > self.__timeout: + self.__timedOut = True + break + + return bytes(data)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonWidget.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,1359 @@ +# -*- 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 +import time +import os + +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, + QTextEdit, QToolButton +) + +from E5Gui.E5ZoomWidget import E5ZoomWidget +from E5Gui import E5MessageBox, E5FileDialog +from E5Gui.E5Application import e5App +from E5Gui.E5ProcessDialog import E5ProcessDialog + +from .Ui_MicroPythonWidget import Ui_MicroPythonWidget + +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 +import Utilities + +# 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)), + }, +} + + +class MicroPythonWidget(QWidget, Ui_MicroPythonWidget): + """ + 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(MicroPythonWidget, self).__init__(parent) + self.setupUi(self) + + self.__ui = parent + + self.__superMenu = QMenu(self) + self.__superMenu.aboutToShow.connect(self.__aboutToShowSuperMenu) + + self.menuButton.setObjectName( + "micropython_supermenu_button") + self.menuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) + self.menuButton.setToolTip(self.tr("pip Menu")) + self.menuButton.setPopupMode(QToolButton.InstantPopup) + self.menuButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.menuButton.setFocusPolicy(Qt.NoFocus) + self.menuButton.setAutoRaise(True) + self.menuButton.setShowMenuInside(True) + self.menuButton.setMenu(self.__superMenu) + + 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.connectButton.setIcon(UI.PixmapCache.getIcon("linkConnect")) + + 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) + + 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. + """ + currentDevice = self.deviceTypeComboBox.currentText() + + 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.")) + + index = self.deviceTypeComboBox.findText(currentDevice, + Qt.MatchExactly) + if index == -1: + # entry is no longer present + index = 0 + if self.__connected: + # we are still connected, so disconnect + self.on_connectButton_clicked() + + self.on_deviceTypeComboBox_activated(index) + self.deviceTypeComboBox.setCurrentIndex(index) + + 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()) + + if Preferences.getMicroPython("ReplLineWrap"): + self.replEdit.setLineWrapMode(QTextEdit.WidgetWidth) + else: + self.replEdit.setLineWrapMode(QTextEdit.NoWrap) + + 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 + + def isMicrobit(self): + """ + Public method to check, if the connected/selected device is a + BBC micro:bit. + + @return flag indicating a micro:bit device + rtype bool + """ + if self.__device and "micro:bit" in self.__device.deviceName(): + return True + + return False + + @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() + + self.connectButton.setEnabled(bool(deviceType)) + + @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() + menu.exec_(self.replEdit.mapToGlobal(pos)) + + def __setConnected(self, connected): + """ + Private 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) + + if connected: + self.connectButton.setIcon( + UI.PixmapCache.getIcon("linkDisconnect")) + self.connectButton.setToolTip(self.tr( + "Press to disconnect the current device")) + else: + self.connectButton.setIcon( + UI.PixmapCache.getIcon("linkConnect")) + self.connectButton.setToolTip(self.tr( + "Press to connect the selected device")) + + def isConnected(self): + """ + Public method to get the connection state. + + @return connection state + @rtype bool + """ + return self.__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. + + If the selected device is not connected yet, this will be done now. + + @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.__device.setRepl(True) + self.replEdit.setFocus(Qt.OtherFocusReason) + else: + self.__interface.dataReceived.disconnect(self.__processData) + if (not self.chartButton.isChecked() and + not self.filesButton.isChecked()): + self.__disconnectFromDevice() + self.__device.setRepl(False) + self.replButton.setChecked(checked) + + @pyqtSlot() + def on_connectButton_clicked(self): + """ + Private slot to connect to the selected device or disconnect from the + currently connected device. + """ + if self.__connected: + self.__disconnectFromDevice() + + if self.replButton.isChecked(): + self.on_replButton_clicked(False) + if self.filesButton.isChecked(): + self.on_filesButton_clicked(False) + if self.chartButton.isChecked(): + self.on_chartButton_clicked(False) + else: + self.__connectToDevice() + + @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(MicroPythonWidget, 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): + """ + Public 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 getDeviceWorkspace(self): + """ + Public method to get the workspace directory of the device. + + @return workspace directory of the device + @rtype str + """ + if self.__device: + return self.__device.getWorkspace() + else: + return "" + + def __connectToDevice(self): + """ + Private method to connect to the selected device. + """ + port = self.getCurrentPort() + if self.__interface.connectToDevice(port): + self.__setConnected(True) + + if Preferences.getMicroPython("SyncTimeAfterConnect"): + self.__synchronizeTime(quiet=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 the REPL is not active yet, it will be activated, which might cause + an unconnected device to be connected. + """ + 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.replButton.isChecked(): + # activate on the REPL + self.on_replButton_clicked(True) + if self.replButton.isChecked(): + 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. + + If the selected device is not connected yet, this will be done now. + + @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.__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.replButton.isChecked() and + not self.filesButton.isChecked()): + self.__disconnectFromDevice() + + 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_connectButton_clicked() + self.__device.handleDataFlood() + + @pyqtSlot(bool) + def on_filesButton_clicked(self, checked): + """ + Private slot to open a file manager window to the connected device. + + If the selected device is not connected yet, this will be done now. + + @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.__device.supportsLocalFileAccess(), + 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.__fileManagerWidget.start() + else: + self.__fileManagerWidget.stop() + + if (not self.replButton.isChecked() and + not self.chartButton.isChecked()): + self.__disconnectFromDevice() + + self.__device.setFileManager(False) + self.__ui.removeSideWidget(self.__fileManagerWidget) + + self.__fileManagerWidget.deleteLater() + self.__fileManagerWidget = None + + self.filesButton.setChecked(checked) + + ################################################################## + ## Super Menu related methods below + ################################################################## + + def __aboutToShowSuperMenu(self): + """ + Private slot to populate the Super Menu before showing it. + """ + self.__superMenu.clear() + if self.__device: + hasTime = self.__device.hasTimeCommands() + else: + hasTime = False + + act = self.__superMenu.addAction( + self.tr("Show Version"), self.__showDeviceVersion) + act.setEnabled(self.__connected) + act = self.__superMenu.addAction( + self.tr("Show Implementation"), self.__showImplementation) + act.setEnabled(self.__connected) + self.__superMenu.addSeparator() + if hasTime: + act = self.__superMenu.addAction( + self.tr("Synchronize Time"), self.__synchronizeTime) + act.setEnabled(self.__connected) + act = self.__superMenu.addAction( + self.tr("Show Device Time"), self.__showDeviceTime) + act.setEnabled(self.__connected) + self.__superMenu.addAction( + self.tr("Show Local Time"), self.__showLocalTime) + self.__superMenu.addSeparator() + if not Globals.isWindowsPlatform(): + self.__superMenu.addAction( + self.tr("Compile Python File"), self.__compileFile2Mpy) + act = self.__superMenu.addAction( + self.tr("Compile Current Editor"), self.__compileEditor2Mpy) + aw = e5App().getObject("ViewManager").activeWindow() + act.setEnabled(bool(aw)) + self.__superMenu.addSeparator() + if self.__device: + self.__device.addDeviceMenuEntries(self.__superMenu) + + @pyqtSlot() + def __showDeviceVersion(self): + """ + Private slot to show some version info about MicroPython of the device. + """ + try: + versionInfo = self.__interface.version() + if versionInfo: + msg = self.tr( + "<h3>Device Version Information</h3>" + ) + msg += "<table>" + for key, value in versionInfo.items(): + msg += "<tr><td><b>{0}</b></td><td>{1}</td></tr>".format( + key.capitalize(), value) + msg += "</table>" + else: + msg = self.tr("No version information available.") + + E5MessageBox.information( + self, + self.tr("Device Version Information"), + msg) + except Exception as exc: + self.__showError("version()", str(exc)) + + @pyqtSlot() + def __showImplementation(self): + """ + Private slot to show some implementation related information. + """ + try: + impInfo = self.__interface.getImplementation() + if impInfo["name"] == "micropython": + name = "MicroPython" + elif impInfo["name"] == "circuitpython": + name = "CircuitPython" + elif impInfo["name"] == "unknown": + name = self.tr("unknown") + else: + name = impInfo["name"] + if impInfo["version"] == "unknown": + version = self.tr("unknown") + else: + version = impInfo["version"] + + E5MessageBox.information( + self, + self.tr("Device Implementation Information"), + self.tr( + "<h3>Device Implementation Information</h3>" + "<p>This device contains <b>{0} {1}</b>.</p>" + ).format(name, version) + ) + except Exception as exc: + self.__showError("getImplementation()", str(exc)) + + @pyqtSlot() + def __synchronizeTime(self, quiet=False): + """ + Private slot to set the time of the connected device to the local + computer's time. + + @param quiet flag indicating to not show a message + @type bool + """ + try: + self.__interface.syncTime() + + if not quiet: + E5MessageBox.information( + self, + self.tr("Synchronize Time"), + self.tr("<p>The time of the connected device was" + " synchronized with the local time.</p>") + + self.__getDeviceTime() + ) + except Exception as exc: + self.__showError("syncTime()", str(exc)) + + def __getDeviceTime(self): + """ + Private method to get a string containing the date and time of the + connected device. + + @return date and time of the connected device + @rtype str + """ + try: + dateTimeString = self.__interface.getTime() + try: + date, time = dateTimeString.strip().split(None, 1) + return self.tr( + "<h3>Device Date and Time</h3>" + "<table>" + "<tr><td><b>Date</b></td><td>{0}</td></tr>" + "<tr><td><b>Time</b></td><td>{1}</td></tr>" + "</table>" + ).format(date, time) + except ValueError: + return self.tr( + "<h3>Device Date and Time</h3>" + "<p>{0}</p>" + ).format(dateTimeString.strip()) + except Exception as exc: + self.__showError("getTime()", str(exc)) + return "" + + @pyqtSlot() + def __showDeviceTime(self): + """ + Private slot to show the date and time of the connected device. + """ + msg = self.__getDeviceTime() + E5MessageBox.information( + self, + self.tr("Device Date and Time"), + msg) + + @pyqtSlot() + def __showLocalTime(self): + """ + Private slot to show the local date and time. + """ + localdatetime = time.localtime() + loacldate = time.strftime('%Y-%m-%d', localdatetime) + localtime = time.strftime('%H:%M:%S', localdatetime) + E5MessageBox.information( + self, + self.tr("Local Date and Time"), + self.tr("<h3>Local Date and Time</h3>" + "<table>" + "<tr><td><b>Date</b></td><td>{0}</td></tr>" + "<tr><td><b>Time</b></td><td>{1}</td></tr>" + "</table>" + ).format(loacldate, localtime) + ) + + def __showError(self, method, error): + """ + Private method to show some error message. + + @param method name of the method the error occured in + @type str + @param error error message + @type str + """ + E5MessageBox.warning( + self, + self.tr("Error handling device"), + self.tr("<p>There was an error communicating with the connected" + " device.</p><p>Method: {0}</p><p>Message: {1}</p>") + .format(method, error)) + + def __crossCompile(self, pythonFile="", title=""): + """ + Private method to cross compile a Python file to a .mpy file. + + @param pythonFile name of the Python file to be compiled + @type str + @param title title for the various dialogs + @type str + """ + program = Preferences.getMicroPython("MpyCrossCompiler") + if not program: + program = "mpy-cross" + if not Utilities.isinpath(program): + E5MessageBox.critical( + self, + title, + self.tr("""The MicroPython cross compiler""" + """ <b>mpy-cross</b> cannot be found. Ensure it""" + """ is in the search path or configure it on""" + """ the MicroPython configuration page.""")) + return + + if not pythonFile: + defaultDirectory = "" + aw = e5App().getObject("ViewManager").activeWindow() + if aw: + fn = aw.getFileName() + if fn: + defaultDirectory = os.path.dirname(fn) + if not defaultDirectory: + defaultDirectory = Preferences.getMultiProject("Workspace") + pythonFile = E5FileDialog.getOpenFileName( + self, + title, + defaultDirectory, + self.tr("Python Files (*.py);;All Files (*)")) + if not pythonFile: + # user cancelled + return + + if not os.path.exists(pythonFile): + E5MessageBox.critical( + self, + title, + self.tr("""The Python file <b>{0}</b> does not exist.""" + """ Aborting...""").format(pythonFile)) + return + + compileArgs = [ + pythonFile, + ] + dlg = E5ProcessDialog(self.tr("'mpy-cross' Output"), title) + res = dlg.startProcess(program, compileArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __compileFile2Mpy(self): + """ + Private slot to cross compile a Python file (*.py) to a .mpy file. + """ + self.__crossCompile(title=self.tr("Compile Python File")) + + @pyqtSlot() + def __compileEditor2Mpy(self): + """ + Private slot to cross compile the current editor to a .mpy file. + """ + aw = e5App().getObject("ViewManager").activeWindow() + if not aw.checkDirty(): + # editor still has unsaved changes, abort... + return + if not aw.isPyFile(): + # no Python file + E5MessageBox.critical( + self, + self.tr("Compile Current Editor"), + self.tr("""The current editor does not contain a Python""" + """ file. Aborting...""")) + return + + self.__crossCompile( + pythonFile=aw.getFileName(), + title=self.tr("Compile Current Editor") + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicroPythonWidget.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,205 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MicroPythonWidget</class> + <widget class="QWidget" name="MicroPythonWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>456</width> + <height>548</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="deviceIconLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0" colspan="4"> + <widget class="QLabel" name="deviceInfoLabel"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="E5Led" name="deviceConnectedLed" native="true"/> + </item> + <item row="0" column="0"> + <widget class="QComboBox" name="deviceTypeComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QToolButton" name="checkButton"> + <property name="toolTip"> + <string>Press to check for connected devices</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="E5ToolButton" name="menuButton"/> + </item> + </layout> + </item> + </layout> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QToolButton" name="openButton"> + <property name="toolTip"> + <string>Press to open a file of the connected device</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="saveButton"> + <property name="toolTip"> + <string>Press to save the current editor to the connected device</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="lineWidth"> + <number>2</number> + </property> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="runButton"> + <property name="toolTip"> + <string>Press to run the current script on the selected device</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="replButton"> + <property name="toolTip"> + <string>Press to open a terminal (REPL) on the selected device</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="filesButton"> + <property name="toolTip"> + <string>Press to open a file manager on the selected device (REPL must be disconnected first)</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="chartButton"> + <property name="toolTip"> + <string>Press to open a chart window to display data receive from the selected device</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QToolButton" name="connectButton"> + <property name="toolTip"> + <string>Press to connect the selected device</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTextEdit" name="replEdit"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="undoRedoEnabled"> + <bool>false</bool> + </property> + <property name="lineWrapMode"> + <enum>QTextEdit::NoWrap</enum> + </property> + <property name="acceptRichText"> + <bool>false</bool> + </property> + </widget> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5ToolButton</class> + <extends>QToolButton</extends> + <header>E5Gui/E5ToolButton.h</header> + </customwidget> + <customwidget> + <class>E5Led</class> + <extends>QWidget</extends> + <header>E5Gui/E5Led.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>deviceTypeComboBox</tabstop> + <tabstop>checkButton</tabstop> + <tabstop>menuButton</tabstop> + <tabstop>openButton</tabstop> + <tabstop>saveButton</tabstop> + <tabstop>runButton</tabstop> + <tabstop>replButton</tabstop> + <tabstop>filesButton</tabstop> + <tabstop>chartButton</tabstop> + <tabstop>connectButton</tabstop> + <tabstop>replEdit</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/MicrobitDevices.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the device interface class for BBC micro:bit boards. +""" + +from __future__ import unicode_literals + +import sys +import os + +from PyQt5.QtCore import pyqtSlot, QStandardPaths + +from .MicroPythonDevices import MicroPythonDevice +from .MicroPythonWidget import HAS_QTCHART + +from E5Gui import E5MessageBox, E5FileDialog +from E5Gui.E5Application import e5App +from E5Gui.E5ProcessDialog import E5ProcessDialog + +import Utilities + + +class MicrobitDevice(MicroPythonDevice): + """ + Class implementing the device for BBC micro:bit boards. + """ + def __init__(self, microPythonWidget, parent=None): + """ + Constructor + + @param microPythonWidget reference to the main MicroPython widget + @type MicroPythonWidget + @param parent reference to the parent object + @type QObject + """ + super(MicrobitDevice, self).__init__(microPythonWidget, parent) + + def setButtons(self): + """ + Public method to enable the supported action buttons. + """ + super(MicrobitDevice, self).setButtons() + self.microPython.setActionButtons( + run=True, repl=True, files=True, chart=HAS_QTCHART) + + def forceInterrupt(self): + """ + Public method to determine the need for an interrupt when opening the + serial connection. + + @return flag indicating an interrupt is needed + @rtype bool + """ + return True + + def deviceName(self): + """ + Public method to get the name of the device. + + @return name of the device + @rtype str + """ + return self.tr("BBC micro:bit") + + def canStartRepl(self): + """ + Public method to determine, if a REPL can be started. + + @return tuple containing a flag indicating it is safe to start a REPL + and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canStartPlotter(self): + """ + Public method to determine, if a Plotter can be started. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def canRunScript(self): + """ + Public method to determine, if a script can be executed. + + @return tuple containing a flag indicating it is safe to start a + Plotter and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def runScript(self, script): + """ + Public method to run the given Python script. + + @param script script to be executed + @type str + """ + pythonScript = script.split("\n") + self.sendCommands(pythonScript) + + def canStartFileManager(self): + """ + Public method to determine, if a File Manager can be started. + + @return tuple containing a flag indicating it is safe to start a + File Manager and a reason why it cannot. + @rtype tuple of (bool, str) + """ + return True, "" + + def getWorkspace(self): + """ + Public method to get the workspace directory. + + @return workspace directory used for saving files + @rtype str + """ + # Attempts to find the path on the filesystem that represents the + # plugged in MICROBIT board. + deviceDirectory = Utilities.findVolume("MICROBIT") + + if deviceDirectory: + return deviceDirectory + else: + # return the default workspace and give the user a warning + E5MessageBox.warning( + self.microPython, + self.tr("Workspace Directory"), + self.tr("Could not find an attached BBC micro:bit.\n\n" + "Please make sure the device is plugged " + "into this computer.")) + + return super(MicrobitDevice, self).getWorkspace() + + def hasTimeCommands(self): + """ + Public method to check, if the device supports time commands. + + The default returns True. + + @return flag indicating support for time commands + @rtype bool + """ + return False + + def addDeviceMenuEntries(self, menu): + """ + Public method to add device specific entries to the given menu. + + @param menu reference to the context menu + @type QMenu + """ + connected = self.microPython.isConnected() + + act = menu.addAction(self.tr("Flash Default MicroPython Firmware"), + self.__flashMicroPython) + act.setEnabled(not connected) + act = menu.addAction(self.tr("Flash Custom MicroPython Firmware"), + self.__flashCustomMicroPython) + act.setEnabled(not connected) + menu.addSeparator() + act = menu.addAction(self.tr("Flash Script"), self.__flashScript) + act.setToolTip(self.tr( + "Flash the current script to the selected device.")) + act.setEnabled(not connected) + act = menu.addAction(self.tr("Save Script as 'main.py'"), + self.__saveMain) + act.setToolTip(self.tr( + "Save the current script as 'main.py' on the connected device")) + act.setEnabled(connected) + menu.addSeparator() + act = menu.addAction(self.tr("Reset micro:bit"), self.__resetDevice) + act.setEnabled(connected) + menu.addSeparator() + menu.addAction(self.tr("Install 'uflash'"), self.__installUflashTool) + + @pyqtSlot() + def __flashMicroPython(self): + """ + Private slot to flash the default MicroPython firmware to the device. + """ + flashArgs = [ + "-u", + "-m", "uflash", + ] + dlg = E5ProcessDialog(self.tr("'uflash' Output"), + self.tr("Flash Default MicroPython Firmware")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __flashCustomMicroPython(self): + """ + Private slot to flash a custom MicroPython firmware to the device. + """ + downloadsPath = QStandardPaths.standardLocations( + QStandardPaths.DownloadLocation)[0] + firmware = E5FileDialog.getOpenFileName( + None, + self.tr("Flash Custom MicroPython Firmware"), + downloadsPath, + self.tr("MicroPython Firmware Files (*.hex);;All Files (*)")) + if firmware and os.path.exists(firmware): + flashArgs = [ + "-u", + "-m", "uflash", + "--runtime", firmware, + ] + dlg = E5ProcessDialog( + self.tr("'uflash' Output"), + self.tr("Flash Default MicroPython Firmware")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __flashScript(self): + """ + Private slot to flash the current script onto the selected device. + """ + aw = e5App().getObject("ViewManager").activeWindow() + if not aw: + return + + if not (aw.isPy3File() or aw.isPy2File()): + yes = E5MessageBox.yesNo( + None, + self.tr("Flash Script"), + self.tr("""The current editor does not contain a Python""" + """ script. Flash it anyway?""")) + if not yes: + return + + script = aw.text().strip() + if not script: + E5MessageBox.warning( + self, + self.tr("Flash Script"), + self.tr("""The script is empty. Aborting.""")) + return + + if aw.checkDirty(): + filename = aw.getFileName() + flashArgs = [ + "-u", + "-m", "uflash", + filename, + ] + dlg = E5ProcessDialog(self.tr("'uflash' Output"), + self.tr("Flash Script")) + res = dlg.startProcess(sys.executable, flashArgs) + if res: + dlg.exec_() + + @pyqtSlot() + def __saveMain(self): + """ + Private slot to copy the current script as 'main.py' onto the + connected device. + """ + aw = e5App().getObject("ViewManager").activeWindow() + if not aw: + return + + if not (aw.isPy3File() or aw.isPy2File()): + yes = E5MessageBox.yesNo( + None, + self.tr("Save Script as 'main.py'"), + self.tr("""The current editor does not contain a Python""" + """ script. Write it anyway?""")) + if not yes: + return + + script = aw.text().strip() + if not script: + E5MessageBox.warning( + self, + self.tr("Save Script as 'main.py'"), + self.tr("""The script is empty. Aborting.""")) + return + + commands = [ + "fd = open('main.py', 'wb')", + "f = fd.write", + ] + for line in script.splitlines(): + commands.append("f(" + repr(line + "\n") + ")") + commands.append("fd.close()") + out, err = self.microPython.commandsInterface().execute(commands) + if err: + E5MessageBox.critical( + self, + self.tr("Save Script as 'main.py'"), + self.tr("""<p>The script could not be saved to the""" + """ device.</p><p>Reason: {0}</p>""") + .format(err.decode("utf-8"))) + + # reset the device + self.microPython.commandsInterface().execute([ + "import microbit", + "microbit.reset()", + ]) + + @pyqtSlot() + def __resetDevice(self): + """ + Private slot to reset the connected device. + """ + self.microPython.commandsInterface().execute([ + "import microbit", + "microbit.reset()", + ]) + + @pyqtSlot() + def __installUflashTool(self): + """ + Private slot to install the uflash package via pip. + """ + pip = e5App().getObject("Pip") + pip.installPackages(["uflash"], interpreter=sys.executable)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/MicroPython/__init__.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing an interface to MicroPython devices like esp32. +""" + +from __future__ import unicode_literals
--- a/eric6/Preferences/ConfigurationDialog.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Preferences/ConfigurationDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -172,6 +172,9 @@ "logViewerPage": [self.tr("Log-Viewer"), "preferences-logviewer.png", "LogViewerPage", None, None], + "microPythonPage": + [self.tr("MicroPython"), "micropython", + "MicroPythonPage", None, None], "mimeTypesPage": [self.tr("Mimetypes"), "preferences-mimetypes.png", "MimeTypesPage", None, None],
--- a/eric6/Preferences/ConfigurationPages/InterfacePage.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Preferences/ConfigurationPages/InterfacePage.py Tue Aug 20 17:07:06 2019 +0200 @@ -86,6 +86,8 @@ # right side self.codeDocumentationViewerCheckBox.setChecked( Preferences.getUI("ShowCodeDocumentationViewer")) + self.microPythonCheckBox.setChecked( + Preferences.getUI("ShowMicroPython")) self.pypiCheckBox.setChecked( Preferences.getUI("ShowPyPIPackageManager")) self.condaCheckBox.setChecked( @@ -175,6 +177,9 @@ "ShowCodeDocumentationViewer", self.codeDocumentationViewerCheckBox.isChecked()) Preferences.setUI( + "ShowMicroPython", + self.microPythonCheckBox.isChecked()) + Preferences.setUI( "ShowPyPIPackageManager", self.pypiCheckBox.isChecked()) Preferences.setUI(
--- a/eric6/Preferences/ConfigurationPages/InterfacePage.ui Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Preferences/ConfigurationPages/InterfacePage.ui Tue Aug 20 17:07:06 2019 +0200 @@ -445,6 +445,16 @@ </property> </widget> </item> + <item row="0" column="1"> + <widget class="QCheckBox" name="microPythonCheckBox"> + <property name="toolTip"> + <string>Select to activate the MicroPython widget</string> + </property> + <property name="text"> + <string>MicroPython</string> + </property> + </widget> + </item> </layout> </widget> </item> @@ -518,6 +528,7 @@ <tabstop>fileBrowserCheckBox</tabstop> <tabstop>symbolsCheckBox</tabstop> <tabstop>codeDocumentationViewerCheckBox</tabstop> + <tabstop>microPythonCheckBox</tabstop> <tabstop>pypiCheckBox</tabstop> <tabstop>condaCheckBox</tabstop> <tabstop>cooperationCheckBox</tabstop>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Preferences/ConfigurationPages/MicroPythonPage.py Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the MicroPython configuration page. +""" + +from __future__ import unicode_literals + +from E5Gui.E5PathPicker import E5PathPickerModes + +from .ConfigurationPageBase import ConfigurationPageBase +from .Ui_MicroPythonPage import Ui_MicroPythonPage + +import Preferences + +from MicroPython.MicroPythonWidget import AnsiColorSchemes + + +class MicroPythonPage(ConfigurationPageBase, Ui_MicroPythonPage): + """ + Class implementing the MicroPython configuration page. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget + @type QWidget + """ + super(MicroPythonPage, self).__init__() + self.setupUi(self) + self.setObjectName("MicroPythonPage") + + self.colorSchemeComboBox.addItems(sorted(AnsiColorSchemes.keys())) + + self.mpyCrossPicker.setMode(E5PathPickerModes.OpenFileMode) + self.mpyCrossPicker.setFilters(self.tr("All Files (*)")) + + # set initial values + self.timeoutSpinBox.setValue( + Preferences.getMicroPython("SerialTimeout") / 1000) + # converted to seconds + self.syncTimeCheckBox.setChecked( + Preferences.getMicroPython("SyncTimeAfterConnect")) + self.colorSchemeComboBox.setCurrentIndex( + self.colorSchemeComboBox.findText( + Preferences.getMicroPython("ColorScheme"))) + self.replWrapCheckBox.setChecked( + Preferences.getMicroPython("ReplLineWrap")) + self.mpyCrossPicker.setText( + Preferences.getMicroPython("MpyCrossCompiler")) + + def save(self): + """ + Public slot to save the MicroPython configuration. + """ + Preferences.setMicroPython( + "SerialTimeout", self.timeoutSpinBox.value() * 1000) + # converted to milliseconds + Preferences.setMicroPython( + "SyncTimeAfterConnect", self.syncTimeCheckBox.isChecked()) + Preferences.setMicroPython( + "ColorScheme", self.colorSchemeComboBox.currentText()) + Preferences.setMicroPython( + "ReplLineWrap", self.replWrapCheckBox.isChecked()) + Preferences.setMicroPython( + "MpyCrossCompiler", self.mpyCrossPicker.text()) + + +def create(dlg): + """ + Module function to create the configuration page. + + @param dlg reference to the configuration dialog + @return reference to the instantiated page (ConfigurationPageBase) + """ + return MicroPythonPage()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Preferences/ConfigurationPages/MicroPythonPage.ui Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,200 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MicroPythonPage</class> + <widget class="QWidget" name="MicroPythonPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>476</width> + <height>356</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure MicroPython</b></string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line9_3"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Serial Link</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Timeout for Serial Link Communication:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QSpinBox" name="timeoutSpinBox"> + <property name="toolTip"> + <string>Enter the timout value</string> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>1</number> + </property> + <property name="maximum"> + <number>30</number> + </property> + </widget> + </item> + <item row="0" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>195</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="1" column="0" colspan="3"> + <widget class="QCheckBox" name="syncTimeCheckBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select to synchronize the time after connection is established</string> + </property> + <property name="text"> + <string>Synchronize Time at Connect</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>REPL Pane</string> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Color Scheme for ANSI Escape Codes:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QComboBox" name="colorSchemeComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the color scheme to be applied for ANSI color escape codes</string> + </property> + </widget> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QCheckBox" name="replWrapCheckBox"> + <property name="toolTip"> + <string>Select to wrap long line in the REPL pane</string> + </property> + <property name="text"> + <string>Wrap long lines</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>MPY Cross Compiler</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Program:</string> + </property> + </widget> + </item> + <item> + <widget class="E5PathPicker" name="mpyCrossPicker" native="true"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="focusPolicy"> + <enum>Qt::WheelFocus</enum> + </property> + <property name="toolTip"> + <string>Enter the path of the cross compiler executable</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>252</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <customwidgets> + <customwidget> + <class>E5PathPicker</class> + <extends>QWidget</extends> + <header>E5Gui/E5PathPicker.h</header> + <container>1</container> + </customwidget> + </customwidgets> + <tabstops> + <tabstop>timeoutSpinBox</tabstop> + <tabstop>syncTimeCheckBox</tabstop> + <tabstop>colorSchemeComboBox</tabstop> + <tabstop>replWrapCheckBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- a/eric6/Preferences/__init__.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Preferences/__init__.py Tue Aug 20 17:07:06 2019 +0200 @@ -47,9 +47,7 @@ from E5Network.E5Ftp import E5FtpProxyType -from Globals import settingsNameOrganization, settingsNameGlobal, \ - settingsNameRecent, isWindowsPlatform, getPyQt5ModulesDirectory, \ - qVersionTuple +import Globals from Project.ProjectBrowserFlags import SourcesBrowserFlag, FormsBrowserFlag, \ ResourcesBrowserFlag, TranslationsBrowserFlag, InterfacesBrowserFlag, \ @@ -168,6 +166,7 @@ "ShowCondaPackageManager": True, # right side "ShowCooperation": True, # right side "ShowIrc": True, # right side + "ShowMicroPython": True, # right side "ShowNumbersViewer": True, # bottom side "ViewProfiles2": { "edit": [ @@ -626,7 +625,7 @@ "YAMLFoldComment": False, } - if isWindowsPlatform(): + if Globals.isWindowsPlatform(): editorDefaults["EOLMode"] = QsciScintilla.EolWindows else: editorDefaults["EOLMode"] = QsciScintilla.EolUnix @@ -1558,7 +1557,7 @@ "AddressAreaForeGround": QColor(Qt.black), "RecentNumber": 9, } - if isWindowsPlatform(): + if Globals.isWindowsPlatform(): hexEditorDefaults["Font"] = "Courier,10,-1,5,50,0,0,0,0,0" else: hexEditorDefaults["Font"] = "Monospace,10,-1,5,50,0,0,0,0,0" @@ -1590,6 +1589,22 @@ pipDefaults = { "PipSearchIndex": "", # used by the search command } + + # defaults for MicroPython + microPythonDefaults = { + "SerialTimeout": 2000, # timeout in milliseconds + "ReplLineWrap": True, # wrap the REPL lines + "SyncTimeAfterConnect": True, + "ShowHiddenLocal": True, + "ShowHiddenDevice": True, + "MpyCrossCompiler": "", # path of the mpy cross compiler + } + if Globals.isWindowsPlatform(): + microPythonDefaults["ColorScheme"] = "Windows 10" + elif Globals.isMacPlatform(): + microPythonDefaults["ColorScheme"] = "xterm" + else: + microPythonDefaults["ColorScheme"] = "Ubuntu" def readToolGroups(prefClass=Prefs): @@ -1693,13 +1708,13 @@ """ Prefs.settings = QSettings( QSettings.IniFormat, QSettings.UserScope, - settingsNameOrganization, settingsNameGlobal) - if not isWindowsPlatform(): + Globals.settingsNameOrganization, Globals.settingsNameGlobal) + if not Globals.isWindowsPlatform(): hp = QDir.homePath() dn = QDir(hp) dn.mkdir(".eric6") - QCoreApplication.setOrganizationName(settingsNameOrganization) - QCoreApplication.setApplicationName(settingsNameGlobal) + QCoreApplication.setOrganizationName(Globals.settingsNameOrganization) + QCoreApplication.setApplicationName(Globals.settingsNameGlobal) try: Prefs.settings.setAtomicSyncRequired(False) except AttributeError: @@ -1789,7 +1804,7 @@ """ Prefs.rsettings = QSettings( QSettings.IniFormat, QSettings.UserScope, - settingsNameOrganization, settingsNameRecent) + Globals.settingsNameOrganization, Globals.settingsNameRecent) def getVarFilters(prefClass=Prefs): @@ -1995,7 +2010,7 @@ "ShowCodeDocumentationViewer", "ShowPyPIPackageManager", "ShowCondaPackageManager", "ShowCooperation", "ShowIrc", "ShowTemplateViewer", "ShowFileBrowser", "ShowSymbolsViewer", - "ShowNumbersViewer", "UseNativeMenuBar"]: + "ShowNumbersViewer", "ShowMicroPython", "UseNativeMenuBar"]: return toBool(prefClass.settings.value( "UI/" + key, prefClass.uiDefaults[key])) elif key in ["TabViewManagerFilenameLength", "CaptionFilenameLength", @@ -3201,7 +3216,7 @@ @param prefClass preferences class used as the storage area @return the requested setting (string) """ - if qVersionTuple() < (5, 0, 0): + if Globals.qVersionTuple() < (5, 0, 0): s = prefClass.settings.value( "Qt/Qt4TranslationsDir", prefClass.qtDefaults["Qt4TranslationsDir"]) @@ -3212,14 +3227,15 @@ if s == "": s = os.getenv("QTTRANSLATIONSDIR", "") if s == "": - if qVersionTuple() < (5, 0, 0): + if Globals.qVersionTuple() < (5, 0, 0): s = os.getenv("QT4TRANSLATIONSDIR", "") else: s = os.getenv("QT5TRANSLATIONSDIR", "") if s == "": s = QLibraryInfo.location(QLibraryInfo.TranslationsPath) - if s == "" and isWindowsPlatform(): - transPath = os.path.join(getPyQt5ModulesDirectory(), "translations") + if s == "" and Globals.isWindowsPlatform(): + transPath = os.path.join(Globals.getPyQt5ModulesDirectory(), + "translations") if os.path.exists(transPath): s = transPath return s @@ -3803,6 +3819,40 @@ prefClass.settings.setValue("Pip/" + key, value) +def getMicroPython(key, prefClass=Prefs): + """ + Module function to retrieve the MicroPython related settings. + + @param key the key of the value to get + @param prefClass preferences class used as the storage area + @return the requested MicroPython value + """ + if key in ("SerialTimeout"): + return int(prefClass.settings.value( + "MicroPython/" + key, + prefClass.microPythonDefaults[key])) + elif key in ["ReplLineWrap", "SyncTimeAfterConnect", "ShowHiddenLocal", + "ShowHiddenDevice"]: + return toBool(prefClass.settings.value( + "MicroPython/" + key, + prefClass.microPythonDefaults[key])) + else: + return prefClass.settings.value( + "MicroPython/" + key, + prefClass.microPythonDefaults[key]) + + +def setMicroPython(key, value, prefClass=Prefs): + """ + Module function to store the pip MicroPython settings. + + @param key the key of the setting to be set + @param value the value to be set + @param prefClass preferences class used as the storage area + """ + prefClass.settings.setValue("MicroPython/" + key, value) + + def getGeometry(key, prefClass=Prefs): """ Module function to retrieve the display geometry.
--- a/eric6/UI/FindFileDialog.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/UI/FindFileDialog.py Tue Aug 20 17:07:06 2019 +0200 @@ -242,10 +242,10 @@ @pyqtSlot() def on_openFilesButton_clicked(self): - """ - Private slot to handle the selection of the 'Open Files' radio button. - """ - self.__enableFindButton() + """ + Private slot to handle the selection of the 'Open Files' radio button. + """ + self.__enableFindButton() @pyqtSlot() def on_filterCheckBox_clicked(self):
--- a/eric6/UI/UserInterface.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/UI/UserInterface.py Tue Aug 20 17:07:06 2019 +0200 @@ -907,6 +907,15 @@ UI.PixmapCache.getIcon("irc.png"), self.tr("IRC")) + if Preferences.getUI("ShowMicroPython"): + # Create the MicroPython part of the user interface + logging.debug("Creating MicroPython Widget...") + from MicroPython.MicroPythonWidget import MicroPythonWidget + self.microPythonWidget = MicroPythonWidget(self) + self.rToolbox.addItem(self.microPythonWidget, + UI.PixmapCache.getIcon("micropython"), + self.tr("MicroPython")) + #################################################### ## Populate the bottom toolbox #################################################### @@ -1095,6 +1104,15 @@ self.irc, UI.PixmapCache.getIcon("irc.png"), self.tr("IRC")) + if Preferences.getUI("ShowMicroPython"): + # Create the MicroPython part of the user interface + logging.debug("Creating MicroPython Widget...") + from MicroPython.MicroPythonWidget import MicroPythonWidget + self.microPythonWidget = MicroPythonWidget(self) + self.rightSidebar.addTab( + self.microPythonWidget, UI.PixmapCache.getIcon("micropython"), + self.tr("MicroPython")) + #################################################### ## Populate the bottom side bar #################################################### @@ -1193,11 +1211,15 @@ """ Public method to add a widget to the sides. - @param side side to add the widget to (UserInterface.LeftSide, - UserInterface.BottomSide) - @param widget reference to the widget to add (QWidget) - @param icon icon to be used (QIcon) - @param label label text to be shown (string) + @param side side to add the widget to + @type int (one of UserInterface.LeftSide, UserInterface.BottomSide, + UserInterface.RightSide) + @param widget reference to the widget to add + @type QWidget + @param icon icon to be used + @type QIcon + @param label label text to be shown + @type str """ assert side in [UserInterface.LeftSide, UserInterface.BottomSide, UserInterface.RightSide] @@ -1216,12 +1238,13 @@ self.bottomSidebar.addTab(widget, icon, label) elif side == UserInterface.RightSide: self.rightSidebar.addTab(widget, icon, label) - + def removeSideWidget(self, widget): """ Public method to remove a widget added using addSideWidget(). - @param widget reference to the widget to remove (QWidget) + @param widget reference to the widget to remove + @type QWidget """ if self.__layoutType == "Toolboxes": for container in [self.lToolbox, self.hToolbox, self.rToolbox]: @@ -1234,7 +1257,34 @@ index = container.indexOf(widget) if index != -1: container.removeTab(index) - + + def showSideWidget(self, widget): + """ + Public method to show a specific widget placed in the side widgets. + + @param widget reference to the widget to be shown + @type QWidget + """ + if self.__layoutType == "Toolboxes": + for dock in [self.lToolboxDock, self.hToolboxDock, + self.rToolboxDock]: + container = dock.widget() + index = container.indexOf(widget) + if index != -1: + dock.show() + container.setCurrentIndex(index) + dock.raise_() + elif self.__layoutType == "Sidebars": + for container in [self.leftSidebar, self.bottomSidebar, + self.rightSidebar]: + index = container.indexOf(widget) + if index != -1: + container.show() + container.setCurrentIndex(index) + container.raise_() + if container.isAutoHiding(): + container.setFocus() + def showLogViewer(self): """ Public method to show the Log-Viewer.
--- a/eric6/Utilities/__init__.py Tue Aug 20 00:37:14 2019 +0300 +++ b/eric6/Utilities/__init__.py Tue Aug 20 17:07:06 2019 +0200 @@ -32,6 +32,8 @@ import fnmatch import glob import getpass +import ctypes +import subprocess def __showwarning(message, category, filename, lineno, file=None, line=""): @@ -1277,6 +1279,70 @@ return dirs +def findVolume(volumeName): + """ + Function to find the directory belonging to a given volume name. + + @param volumeName name of the volume to search for + @type str + @return directory path of the given volume name + @rtype str + """ + volumeDirectory = None + + if isWindowsPlatform(): + # we are on a Windows platform + def getVolumeName(diskName): + """ + Local function to determine the volume of a disk or device. + + Each disk or external device connected to windows has an + attribute called "volume name". This function returns the + volume name for the given disk/device. + + Code from http://stackoverflow.com/a/12056414 + """ + volumeNameBuffer = ctypes.create_unicode_buffer(1024) + ctypes.windll.kernel32.GetVolumeInformationW( + ctypes.c_wchar_p(diskName), volumeNameBuffer, + ctypes.sizeof(volumeNameBuffer), None, None, None, None, 0) + return volumeNameBuffer.value + + # + # In certain circumstances, volumes are allocated to USB + # storage devices which cause a Windows popup to raise if their + # volume contains no media. Wrapping the check in SetErrorMode + # with SEM_FAILCRITICALERRORS (1) prevents this popup. + # + oldMode = ctypes.windll.kernel32.SetErrorMode(1) + try: + for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": + dirpath = "{0}:\\".format(disk) + if (os.path.exists(dirpath) and + getVolumeName(dirpath) == volumeName): + volumeDirectory = dirpath + break + finally: + ctypes.windll.kernel32.SetErrorMode(oldMode) + else: + # we are on a Linux or macOS platform + for mountCommand in ["mount", "/sbin/mount", "/usr/sbin/mount"]: + try: + mountOutput = (subprocess.check_output(mountCommand) + .splitlines()) + mountedVolumes = [x.split()[2] for x in mountOutput] + for volume in mountedVolumes: + if volume.decode("utf-8").endswith(volumeName): + volumeDirectory = volume.decode("utf-8") + break + if volumeDirectory: + break + except FileNotFoundError: + pass + + return volumeDirectory + + def getTestFileName(fn): """ Function to build the filename of a unittest file.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/chart.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + .ColorScheme-Highlight { + color:#3daee9; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4 3 L 4 9 L 4 10 L 4 15 L 4 16 L 4 17 L 3 17 L 3 18 L 4 18 L 4 19 L 5 19 L 5 18 L 18.292969 18 L 19 18 L 19 17.292969 L 19 17 L 18.707031 17 L 17.958984 16.251953 L 17.130859 15.423828 L 15.130859 13.423828 L 15.126953 13.427734 L 15.121094 13.423828 L 12.998047 15.546875 L 11.703125 14.251953 L 10.875 13.423828 L 8.875 11.423828 L 8.8730469 11.423828 L 7 11.423828 L 6 11.423828 L 5 11.423828 L 5 11 L 5 10 L 6 10 L 7 10 L 8.8730469 10 L 8.875 10 L 10.875 8 L 11.703125 7.171875 L 12.998047 5.8769531 L 15.121094 8 L 15.126953 7.9960938 L 15.130859 8 L 17.130859 6 L 17.958984 5.171875 L 19 4.1308594 L 18.292969 3.4238281 L 17.251953 4.4648438 L 16.423828 5.2929688 L 15.126953 6.5898438 L 13 4.4648438 L 12.998047 4.4667969 L 12.996094 4.4648438 L 11.46875 5.9921875 L 5 5.9921875 L 5 5 L 5 3 L 4 3 z M 5 6 L 11.460938 6 L 10.996094 6.4648438 L 10.167969 7.2929688 L 8.4609375 9 L 7 9 L 6 9 L 5 9 L 5 6 z M 5 12.423828 L 6 12.423828 L 7 12.423828 L 8.4609375 12.423828 L 10.167969 14.130859 L 10.996094 14.958984 L 11.460938 15.423828 L 5 15.423828 L 5 15 L 5 12.423828 z M 15.126953 14.833984 L 16.423828 16.130859 L 17.251953 16.958984 L 17.292969 17 L 5 17 L 5 16 L 5 15.431641 L 11.46875 15.431641 L 12.996094 16.958984 L 12.998047 16.957031 L 13 16.958984 L 15.126953 14.833984 z " + class="ColorScheme-Text" + /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/filemanager.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,241 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="22" + height="22" + viewBox="0 0 22 22" + id="svg7925" + version="1.1" + inkscape:version="0.91 r13725" + sodipodi:docname="system-file-manager.svg"> + <defs + id="defs7927"> + <linearGradient + id="linearGradient4274" + inkscape:collect="always"> + <stop + id="stop4276" + offset="0" + style="stop-color:#ffffff;stop-opacity:1;" /> + <stop + id="stop4278" + offset="1" + style="stop-color:#ffffff;stop-opacity:0.48760331" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4274" + id="linearGradient8485" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.69224711,0,0,0.69224711,-264.27155,1668.4354)" + x1="390.57144" + y1="498.298" + x2="442.57144" + y2="498.298" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4393" + id="linearGradient4399" + x1="419.4624" + y1="499.23697" + x2="432.57144" + y2="523.79797" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.34616832,0,0,0.34616832,-141.20459,864.54218)" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4393"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4395" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4397" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4172-5" + id="linearGradient4178" + y1="548.88599" + y2="495.30789" + x2="397.2283" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.34616832,0,0,0.34605565,-133.24271,862.87242)" + x1="434.16153" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4172-5"> + <stop + style="stop-color:#127bdc;stop-opacity:1" + id="stop4174-6" /> + <stop + offset="1" + style="stop-color:#64b4f4;stop-opacity:1" + id="stop4176-6" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4227" + id="linearGradient4225" + gradientUnits="userSpaceOnUse" + x1="396.57144" + y1="498.798" + x2="426.57144" + y2="511.798" + gradientTransform="matrix(0.34616832,0,0,0.34616832,-133.24271,864.50368)" /> + <linearGradient + id="linearGradient4227" + inkscape:collect="always"> + <stop + id="stop4229" + offset="0" + style="stop-color:#f5f5f5;stop-opacity:1" /> + <stop + id="stop4231" + offset="1" + style="stop-color:#f9f9f9;stop-opacity:1" /> + </linearGradient> + <linearGradient + gradientTransform="matrix(0.34616832,0,0,0.34616832,-133.24271,862.81136)" + inkscape:collect="always" + xlink:href="#linearGradient4291" + id="linearGradient4297" + x1="388.57144" + y1="487.798" + x2="416.57144" + y2="507.798" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + id="linearGradient4291"> + <stop + style="stop-color:#1d5e8c;stop-opacity:1" + offset="0" + id="stop4293" /> + <stop + style="stop-color:#2675a7;stop-opacity:1" + offset="1" + id="stop4295" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4274" + id="linearGradient8603" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.34616832,0,0,0.34616832,-133.24271,2893.0497)" + x1="390.57144" + y1="498.298" + x2="442.57144" + y2="498.298" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4274" + id="linearGradient8603-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.34616832,0,0,0.34616832,-135.15325,827.3248)" + x1="390.57144" + y1="498.298" + x2="442.57144" + y2="498.298" /> + </defs> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1" + inkscape:cx="14.052007" + inkscape:cy="13.482364" + inkscape:document-units="px" + inkscape:current-layer="layer1" + showgrid="false" + units="px" + inkscape:snap-bbox="true" + inkscape:bbox-nodes="true" + inkscape:object-nodes="true"> + <inkscape:grid + type="xygrid" + id="grid8605" /> + </sodipodi:namedview> + <metadata + id="metadata7930"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title /> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Livello 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(0,-1030.3622)"> + <path + inkscape:connector-curvature="0" + style="fill:url(#linearGradient4297);fill-opacity:1" + id="rect4180" + d="m 1,1042.3622 20,0.501 0,-8.501 -10,0 -1.588488,-2.0013 -8.411512,0 z" + sodipodi:nodetypes="ccccccc" /> + <rect + ry="1.0000174" + rx="0.9128685" + y="1036.3622" + x="2" + height="5.0000172" + width="18" + id="rect4223" + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient4225);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" /> + <path + inkscape:connector-curvature="0" + style="fill:url(#linearGradient4178)" + d="m 9,1038.3622 -2,1 -6,0 -0.0895868,1.0011 -5e-7,9.5003 c 0,0.2699 0.244354,0.4954 0.4919395,0.4954 l 19.0105828,0.01 c 0.255387,0.01 0.500065,-0.1978 0.500065,-0.5 L 21,1038.3622 Z" + id="rect4113" + sodipodi:nodetypes="cccccscccc" /> + <path + inkscape:connector-curvature="0" + style="opacity:0.3;fill:#ffffff;fill-opacity:1;fill-rule:evenodd" + id="path4224" + d="m 1,1039.3622 -0.0895868,1.0011 7.0895868,0 1,-2 -2,0.9989 z" + sodipodi:nodetypes="cccccc" /> + <path + inkscape:connector-curvature="0" + style="opacity:0.3;fill:#ffffff;fill-opacity:1;fill-rule:evenodd" + id="path4196" + d="m 9.411512,1032.3622 0.588488,3 11,0 0,-1 -10,0 z" + sodipodi:nodetypes="cccccc" /> + <path + style="opacity:0.09899998;fill:url(#linearGradient4399);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 0.9104132,1040.3633 7.0895868,-0 1,-2 12,0 0,11.6563 -10.385049,0 z" + id="path4383" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccccc" /> + <path + style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.85;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:url(#linearGradient8603);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 2.9121094,1036.3622 c -0.5057292,0 -0.9121094,0.446 -0.9121094,1 l 0,1 c 0,-0.554 0.4063802,-1 0.9121094,-1 l 16.0878906,0 c 0.505729,0 0.912109,0.446 0.912109,1 l 0,-1 c 0,-0.554 -0.40638,-1 -0.912109,-1 z" + id="rect8607" + inkscape:connector-curvature="0" + sodipodi:nodetypes="sscsscsss" /> + <path + style="fill:#000000;color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.3;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;filter-blend-mode:normal;filter-gaussianBlur-deviation:0;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="M 20.992188 8 L 20.912109 18.505859 C 20.912109 18.808059 20.667496 19.015859 20.412109 19.005859 L 1.4023438 18.996094 C 1.1547582 18.996094 0.91015625 18.771853 0.91015625 18.501953 L 0.91015625 19.501953 C 0.91015625 19.771853 1.1547582 19.996094 1.4023438 19.996094 L 20.412109 20.005859 C 20.667496 20.015859 20.912109 19.808059 20.912109 19.505859 L 21 8 L 20.992188 8 z " + transform="translate(0,1030.3622)" + id="path4164" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/linkConnect.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 18.292969 3 L 14.792969 6.5 L 13.292969 5 L 11.292969 7 L 8 10.292969 L 6 12.292969 L 7.5 13.792969 L 3 18.292969 L 3.7070312 19 L 8.2070312 14.5 L 9.7070312 16 L 11.707031 14 L 15 10.707031 L 17 8.7070312 L 15.5 7.2070312 L 19 3.7070312 L 18.292969 3 z " + class="ColorScheme-Text" + /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/linkDisconnect.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,22 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + .ColorScheme-NegativeText { + color:#da4453; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 18.292969 3 L 14.792969 6.5 L 13.292969 5 L 11.292969 7 L 9.5 8.7929688 L 11 10.292969 L 11.707031 11 L 13.207031 12.5 L 15 10.707031 L 17 8.7070312 L 15.5 7.2070312 L 19 3.7070312 L 18.292969 3 z M 8.7929688 9.5 L 8 10.292969 L 6 12.292969 L 7.5 13.792969 L 3 18.292969 L 3.7070312 19 L 8.2070312 14.5 L 9.7070312 16 L 11.707031 14 L 12.5 13.207031 L 11 11.707031 L 10.292969 11 L 8.7929688 9.5 z " + class="ColorScheme-Text" + /> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 14.833984 14 L 14 14.833984 L 15.666016 16.5 L 14 18.166016 L 14.833984 19 L 16.5 17.333984 L 18.166016 19 L 19 18.166016 L 17.333984 16.5 L 19 14.833984 L 18.166016 14 L 16.5 15.666016 L 14.833984 14 z " + class="ColorScheme-NegativeText" + /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/question.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 10.96875 3 C 9.21782 3 7.56187 3.4216019 6 4.2636719 L 6.7773438 6.0507812 C 7.5582637 5.6672613 8.2600456 5.3933025 8.8847656 5.2265625 C 9.5177356 5.0598325 10.162692 4.9765625 10.820312 4.9765625 C 11.806753 4.9765625 12.563734 5.1968119 13.089844 5.6386719 C 13.615944 6.0805419 13.878906 6.7136725 13.878906 7.5390625 C 13.878906 7.9809325 13.822111 8.369625 13.707031 8.703125 C 13.591951 9.036605 13.394724 9.369635 13.115234 9.703125 C 12.835754 10.036615 12.247563 10.586476 11.351562 11.353516 C 10.251483 12.356846 9.10046 14.139 9 16 L 11 16 L 10.994141 15.96875 C 10.994141 15.21007 11.125612 14.601138 11.388672 14.142578 C 11.659952 13.675688 12.201805 13.087576 13.015625 12.378906 C 14.010295 11.536836 14.673153 10.903726 15.001953 10.478516 C 15.338993 10.053316 15.589506 9.6041363 15.753906 9.1289062 C 15.918406 8.6536762 16 8.1071944 16 7.4902344 C 16 6.0728944 15.55227 4.97119 14.65625 4.1875 C 13.76024 3.39546 12.53061 3 10.96875 3 z M 9 17 L 9 19 L 11 19 L 11 17 L 9 17 z " + class="ColorScheme-Text" + /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/start.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m 4 4 0 14 L 18 11 Z" + class="ColorScheme-Text" + /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/icons/default/terminal.svg Tue Aug 20 17:07:06 2019 +0200 @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg id="svg2987" width="16px" height="16px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs id="defs2989"> + <linearGradient id="linearGradient2880" x1="16.143" x2="16.143" y1="4" y2="44" gradientTransform="matrix(.23078 0 0 .15386 2.4612 4.8074)" gradientUnits="userSpaceOnUse"> + <stop id="stop2225-6-4-7-2" style="stop-color:#fff" offset="0"/> + <stop id="stop2229-2-5-5-8" style="stop-color:#fff;stop-opacity:0" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2883" x1="16.626" x2="20.055" y1="15.298" y2="24.628" gradientTransform="matrix(.30476 0 0 .32156 .68572 1.0807)" gradientUnits="userSpaceOnUse"> + <stop id="stop2687-1-9-0-2" style="stop-color:#fff" offset="0"/> + <stop id="stop2689-5-4-3-1" style="stop-color:#fff;stop-opacity:0" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2886" x1="16" x2="16" y1="27.045" y2="16" gradientTransform="matrix(.34286 0 0 .36364 -.42808 -.81818)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient3680-6"/> + <linearGradient id="linearGradient3680-6"> + <stop id="stop3682-4" style="stop-color:#dcdcdc" offset="0"/> + <stop id="stop3684-8" style="stop-color:#fff" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2890" x1="20" x2="20" y1="28" y2="26" gradientTransform="matrix(.375 0 0 .375 -.74978 -.5)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient3680-6"/> + <linearGradient id="linearGradient2894" x1="12.579" x2="12.213" y1="2.9165" y2="47.279" gradientTransform="matrix(.28855 0 0 .25608 1.0743 2.6116)" gradientUnits="userSpaceOnUse"> + <stop id="stop2240-1-6-7-0" style="stop-color:#fff" offset="0"/> + <stop id="stop2242-7-3-7-2" style="stop-color:#fff;stop-opacity:0" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2897" x1="23.071" x2="23.071" y1="36.047" y2="33.296" gradientTransform="matrix(.33032 0 0 .32624 .074248 1.9649)" gradientUnits="userSpaceOnUse"> + <stop id="stop2225-6-4-7" style="stop-color:#fff" offset="0"/> + <stop id="stop2229-2-5-5" style="stop-color:#fff;stop-opacity:0" offset="1"/> + </linearGradient> + <radialGradient id="radialGradient2900" cx="7.4957" cy="8.4498" r="20" gradientTransform="matrix(0 .47178 -.86826 -1.9907e-8 15.337 1.0829)" gradientUnits="userSpaceOnUse"> + <stop id="stop3790-0-0" style="stop-color:#505050" offset="0"/> + <stop id="stop3792-0-2" style="stop-color:#141414" offset="1"/> + </radialGradient> + <linearGradient id="linearGradient2902" x1="16.143" x2="16.143" y1="4" y2="44" gradientTransform="matrix(.28207 0 0 .20514 1.2304 3.5766)" gradientUnits="userSpaceOnUse"> + <stop id="stop3796-3-0" style="stop-color:#323232" offset="0"/> + <stop id="stop3798-8-6" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2983" x1="24" x2="24" y1="7.96" y2="43.865" gradientTransform="matrix(.3333 0 0 .32429 .00075427 .06858)" gradientUnits="userSpaceOnUse"> + <stop id="stop4324-9-7" style="stop-color:#f0f0f0" offset="0"/> + <stop id="stop2860-4-4" style="stop-color:#d7d7d8" offset=".085525"/> + <stop id="stop2862-5-9" style="stop-color:#b2b2b3" offset=".92166"/> + <stop id="stop4326-1-1" style="stop-color:#979798" offset="1"/> + </linearGradient> + <linearGradient id="linearGradient2985" x1="10.014" x2="10.014" y1="44.96" y2="2.8765" gradientTransform="matrix(.31912 0 0 .29298 .34112 1.4648)" gradientUnits="userSpaceOnUse"> + <stop id="stop4334-7-6" style="stop-color:#595959" offset="0"/> + <stop id="stop4336-8-0" style="stop-color:#b3b3b3" offset="1"/> + </linearGradient> + </defs> + <metadata id="metadata2992"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <dc:title/> + </cc:Work> + </rdf:RDF> + </metadata> + <g id="layer1"> + <rect id="rect2551-5-8" x=".50107" y="2.5011" width="14.998" height="11.998" rx="1" ry="1" style="fill-rule:evenodd;fill:url(#linearGradient2983);stroke-linecap:round;stroke-linejoin:round;stroke-width:1.0021;stroke:url(#linearGradient2985)"/> + <rect id="rect1314-3-3" x="2.5" y="4.5" width="11" height="8" rx="0" ry="0" style="color:#000000;fill:url(#radialGradient2900);stroke-linecap:round;stroke-linejoin:round;stroke:url(#linearGradient2902)"/> + <rect id="rect2221-3-8" x="1.501" y="3.5011" width="12.998" height="9.9979" rx="0" ry="0" style="fill:none;opacity:.4;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.0021;stroke:url(#linearGradient2897)"/> + <rect id="rect2556-8-5" x="1.5019" y="3.5019" width="12.996" height="9.9963" style="fill:none;opacity:.8;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.0037;stroke:url(#linearGradient2894)"/> + <path id="path3651-2" d="m9.0002 10v-0.75h-3v0.75h3z" style="fill:url(#linearGradient2890)"/> + <path id="path3653-7" d="m3.6862 9-0.68571-0.72727 1.5429-1.2727-1.5429-1.2727 0.68571-0.72727 2.3143 2-2.3143 2z" style="fill:url(#linearGradient2886)"/> + <path id="path3333-3-0" d="m1.6667 3c-0.36824 0-0.66667 0.31488-0.66667 0.70342v5.145c9.545e-4 0.043283 0.018837 0.084214 0.049602 0.11286 0.030769 0.028643 0.071495 0.04238 0.1123 0.037874l13.714-2.2911c0.070547-0.011738 0.12282-0.075385 0.12381-0.15073v-2.8539c0-0.38854-0.29843-0.70342-0.66667-0.70342h-12.667z" style="fill-rule:evenodd;fill:url(#linearGradient2883);opacity:.2"/> + <rect id="rect1314-3-3-9" x="3.5" y="5.5" width="9" height="6" rx="0" ry="0" style="color:#000000;fill:none;opacity:.1;stroke-linecap:round;stroke-linejoin:round;stroke:url(#linearGradient2880)"/> + </g> +</svg>