MicroPython: continued implementing the file manager widget. micropython

Wed, 24 Jul 2019 20:12:19 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 24 Jul 2019 20:12:19 +0200
branch
micropython
changeset 7082
ec199ef0cfc6
parent 7081
ed510767c096
child 7083
217862c28319

MicroPython: continued implementing the file manager widget.

eric6/MicroPython/CircuitPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonDevices.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileManagerWidget.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileManagerWidget.ui file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonFileSystem.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonReplWidget.ui file | annotate | diff | comparison | revisions
eric6/MicroPython/MicroPythonSerialPort.py file | annotate | diff | comparison | revisions
eric6/MicroPython/MicrobitDevices.py file | annotate | diff | comparison | revisions
--- a/eric6/MicroPython/CircuitPythonDevices.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/CircuitPythonDevices.py	Wed Jul 24 20:12:19 2019 +0200
@@ -135,20 +135,24 @@
                     path = '{0}:\\'.format(disk)
                     if (os.path.exists(path) and
                             getVolumeName(path) == 'CIRCUITPY'):
-                        return path
+                        deviceDirectory =  path
+                        break
             finally:
                 ctypes.windll.kernel32.SetErrorMode(oldMode)
         else:
             # we are on a Linux or macOS platform
-            for mountCommand in ['mount', '/sbin/mount']:
+            for mountCommand in ['mount', '/sbin/mount', '/usr/sbin/mount']:
                 try:
                     mountOutput = check_output(mountCommand).splitlines()
                     mountedVolumes = [x.split()[2] for x in mountOutput]
                     for volume in mountedVolumes:
                         if volume.endswith(b'CIRCUITPY'):
                             deviceDirectory = volume.decode('utf-8')
+                            break
+                    if deviceDirectory:
+                        break
                 except FileNotFoundError:
-                    next
+                    pass
         
         if deviceDirectory:
             return deviceDirectory
--- a/eric6/MicroPython/MicroPythonDevices.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonDevices.py	Wed Jul 24 20:12:19 2019 +0200
@@ -299,3 +299,12 @@
         Public slot handling a data floof from the device.
         """
         pass
+    
+    def addActions(self, menu):
+        """
+        Public method to add device specific entries to the given menu.
+        
+        @param menu reference to the context menu
+        @type QMenu
+        """
+        pass
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManagerWidget.py	Wed Jul 24 20:12:19 2019 +0200
@@ -11,10 +11,13 @@
 
 import os
 
-from PyQt5.QtCore import pyqtSlot, Qt
-from PyQt5.QtWidgets import QWidget, QTreeWidgetItem, QHeaderView
+from PyQt5.QtCore import pyqtSlot, Qt, QPoint
+from PyQt5.QtWidgets import (
+    QWidget, QTreeWidgetItem, QHeaderView, QMenu, QInputDialog, QLineEdit
+)
 
-from E5Gui import E5MessageBox
+from E5Gui import E5MessageBox, E5PathPickerDialog
+from E5Gui.E5PathPicker import E5PathPickerModes
 from E5Gui.E5Application import e5App
 
 from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget
@@ -64,34 +67,49 @@
         self.__fileManager.longListFiles.connect(self.__handleLongListFiles)
         self.__fileManager.currentDir.connect(self.__handleCurrentDir)
         self.__fileManager.currentDirChanged.connect(self.__handleCurrentDir)
-        self.__fileManager.putFileDone.connect(self.__handlePutDone)
+        self.__fileManager.putFileDone.connect(self.__newDeviceList)
         self.__fileManager.getFileDone.connect(self.__handleGetDone)
         self.__fileManager.rsyncDone.connect(self.__handleRsyncDone)
         self.__fileManager.rsyncMessages.connect(self.__handleRsyncMessages)
+        self.__fileManager.removeDirectoryDone.connect(self.__newDeviceList)
+        self.__fileManager.createDirectoryDone.connect(self.__newDeviceList)
+        self.__fileManager.deleteFileDone.connect(self.__newDeviceList)
+        self.__fileManager.synchTimeDone.connect(self.__timeSynchronized)
+        self.__fileManager.showTimeDone.connect(self.__deviceTimeReceived)
+        self.__fileManager.showVersionDone.connect(
+            self.__deviceVersionReceived)
         
-        self.__fileManager.longListFilesFailed.connect(self.__handleError)
-        self.__fileManager.currentDirFailed.connect(self.__handleError)
-        self.__fileManager.currentDirChangeFailed.connect(self.__handleError)
-        self.__fileManager.putFileFailed.connect(self.__handleError)
-        self.__fileManager.getFileFailed.connect(self.__handleError)
-        self.__fileManager.rsyncFailed.connect(self.__handleError)
+        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)
         
-        # TODO: add context menus for panes (separate menus)
-        # local pane:
-        #  Change Directory
-        #
-        # device pane:
-        #  Change Directory
-        #  Create Directory
-        #  Delete Directory
-        #  Delete Directory Tree (= recursive delete)
-        #  ----------------------------
-        #  Delete File
-        #  ----------------------------
-        #  Synchronize Time
-        #  Show Time
-        #  ----------------------------
-        #  Show Version
+        self.__deviceMenu = QMenu(self)
+        self.__deviceMenu.addAction(
+            self.tr("Change Directory"), self.__changeDeviceDirectory)
+        self.__deviceMenu.addAction(
+            self.tr("Create Directory"), self.__createDeviceDirectory)
+        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()
+        self.__deviceMenu.addAction(
+            self.tr("Synchronize Time"), self.__synchronizeTime)
+        self.__deviceMenu.addAction(
+            self.tr("Show Time"), self.__showDeviceTime)
+        self.__deviceMenu.addSeparator()
+        self.__deviceMenu.addAction(
+            self.tr("Show Version"), self.__showDeviceVersion)
     
     def start(self):
         """
@@ -117,11 +135,13 @@
         """
         self.__fileManager.disconnect()
     
-    @pyqtSlot(str)
-    def __handleError(self, error):
+    @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
         """
@@ -129,7 +149,8 @@
             self,
             self.tr("Error handling device"),
             self.tr("<p>There was an error communicating with the connected"
-                    " device.</p><p>Message: {0}</p>").format(error))
+                    " device.</p><p>Method: {0}</p><p>Message: {1}</p>")
+            .format(method, error))
     
     @pyqtSlot(str)
     def __handleCurrentDir(self, dirname):
@@ -305,13 +326,23 @@
                                 " connected device. Overwrite it?</p>")
                         .format(filename)
                     )
-                    if not ok:
-                        return
-                    # TODO: allow to rename the new file
+                    if ok:
+                        deviceFilename = filename
+                    else:
+                        deviceFilename, ok = QInputDialog.getText(
+                            self,
+                            self.tr("Copy File to Device"),
+                            self.tr("Enter a new name:"),
+                            QLineEdit.Normal,
+                            filename)
+                        if not ok or not bool(deviceFilename):
+                            return
+                else:
+                    deviceFilename = filename
                 
                 self.__fileManager.put(
                     os.path.join(self.localCwd.text(), filename),
-                    os.path.join(self.deviceCwd.text(), filename)
+                    os.path.join(self.deviceCwd.text(), deviceFilename)
                 )
     
     @pyqtSlot()
@@ -333,28 +364,26 @@
                                 " Overwrite it?</p>")
                         .format(filename)
                     )
-                    if not ok:
-                        return
-                    # TODO: allow to rename the new file
+                    if ok:
+                        localFilename = filename
+                    else:
+                        localFilename, ok = QInputDialog.getText(
+                            self,
+                            self.tr("Copy File from Device"),
+                            self.tr("Enter a new name:"),
+                            QLineEdit.Normal,
+                            filename)
+                        if not ok or not bool(localFilename):
+                            return
+                else:
+                    localFilename = filename
                 
                 self.__fileManager.get(
                     os.path.join(self.deviceCwd.text(), filename),
-                    os.path.join(self.localCwd.text(), filename)
+                    os.path.join(self.localCwd.text(), localFilename)
                 )
     
     @pyqtSlot(str, str)
-    def __handlePutDone(self, localFile, deviceFile):
-        """
-        Private slot handling a successful copy of a file to the device.
-        
-        @param localFile name of the local file
-        @type str
-        @param deviceFile name of the file on the device
-        @type str
-        """
-        self.__fileManager.lls(self.deviceCwd.text())
-    
-    @pyqtSlot(str, str)
     def __handleGetDone(self, deviceFile, localFile):
         """
         Private slot handling a successful copy of a file from the device.
@@ -406,3 +435,210 @@
                 "</li><li>".join(messages)
             )
         )
+    
+    @pyqtSlot()
+    def __newDeviceList(self):
+        """
+        Private slot to initiate a new long list of the device directory.
+        """
+        self.__fileManager.lls(self.deviceCwd.text())
+    
+    ##################################################################
+    ## 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
+        """
+        self.__localMenu.exec_(self.localFileTreeWidget.mapToGlobal(pos))
+    
+    @pyqtSlot()
+    def __changeLocalDirectory(self):
+        """
+        Private slot to change the local directory.
+        """
+        path, ok = E5PathPickerDialog.getPath(
+            self,
+            self.tr("Change Directory"),
+            self.tr("Select Directory"),
+            E5PathPickerModes.DirectoryShowFilesMode,
+            defaultDirectory=self.localCwd.text(),
+        )
+        if ok and path:
+            self.localCwd.setText(path)
+            self.__listLocalFiles(path)
+    
+    ##################################################################
+    ## 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
+        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.
+        """
+        dirPath, ok = QInputDialog.getText(
+            self,
+            self.tr("Change Directory"),
+            self.tr("Enter the full directory path on the device:"),
+            QLineEdit.Normal,
+            self.deviceCwd.text())
+        if ok and dirPath:
+            self.__fileManager.cd(dirPath)
+    
+    @pyqtSlot()
+    def __createDeviceDirectory(self):
+        """
+        Private slot to create a directory on the device.
+        """
+        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 bool(len(self.deviceFileTreeWidget.selectedItems())):
+            name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
+            dirname = self.deviceCwd.text() + "/" + name[:-1]
+            self.__fileManager.rmdir(dirname)
+    
+    @pyqtSlot()
+    def __deleteDeviceDirectoryTree(self):
+        """
+        Private slot to delete a directory and all its subdirectories
+        recursively.
+        """
+        if bool(len(self.deviceFileTreeWidget.selectedItems())):
+            name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
+            dirname = self.deviceCwd.text() + "/" + name[:-1]
+            self.__fileManager.rmdir(dirname, recursive=True)
+    
+    @pyqtSlot()
+    def __deleteDeviceFile(self):
+        """
+        Private slot to delete a file
+        """
+        if bool(len(self.deviceFileTreeWidget.selectedItems())):
+            name = self.deviceFileTreeWidget.selectedItems()[0].text(0)
+            filename = self.deviceCwd.text() + "/" + name
+            self.__fileManager.delete(filename)
+    
+    @pyqtSlot()
+    def __synchronizeTime(self):
+        """
+        Private slot to synchronize the local time to the device.
+        """
+        self.__fileManager.synchronizeTime()
+    
+    @pyqtSlot()
+    def __timeSynchronized(self):
+        """
+        Private slot handling the successful syncronization of the time.
+        """
+        E5MessageBox.information(
+            self,
+            self.tr("Synchronize Time"),
+            self.tr("The time of the connected device was synchronized with"
+                    " the local time."))
+    
+    @pyqtSlot()
+    def __showDeviceTime(self):
+        """
+        Private slot to show the date and time of the connected device.
+        """
+        self.__fileManager.showTime()
+    
+    @pyqtSlot(str)
+    def __deviceTimeReceived(self, dateTimeString):
+        """
+        Private slot handling the receipt of the device date and time.
+        
+        @param dateTimeString string containg the date and time of the device
+        @type str
+        """
+        try:
+            date, time = dateTimeString.strip().split(None, 1)
+            msg = 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:
+            msg = self.tr(
+                "<h3>Device Date and Time</h3>"
+                "<p>{0}</p>"
+            ).format(dateTimeString.strip())
+        E5MessageBox.information(
+            self,
+            self.tr("Device Date and Time"),
+            msg)
+    
+    @pyqtSlot()
+    def __showDeviceVersion(self):
+        """
+        Private slot to show some version info about MicroPython of the device.
+        """
+        self.__fileManager.showVersion()
+    
+    @pyqtSlot(dict)
+    def __deviceVersionReceived(self, versionInfo):
+        """
+        Private slot handling the receipt of the version info.
+        
+        @param versionInfo dictionary containing the version information
+        @type dict
+        """
+        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, value)
+            msg += "</table>"
+        else:
+            msg = self.tr("No version information available.")
+        E5MessageBox.information(
+            self,
+            self.tr("Device Version Information"),
+            msg)
--- a/eric6/MicroPython/MicroPythonFileManagerWidget.ui	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileManagerWidget.ui	Wed Jul 24 20:12:19 2019 +0200
@@ -39,6 +39,9 @@
    </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>
@@ -129,6 +132,9 @@
    </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>
--- a/eric6/MicroPython/MicroPythonFileSystem.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonFileSystem.py	Wed Jul 24 20:12:19 2019 +0200
@@ -74,10 +74,13 @@
         """
         Private method to switch the connected device to 'raw' mode.
         
-        Note: switching to raw mode is done with synchroneous writes.
+        Note: switching to raw mode is done with synchronous writes.
+        
+        @return flag indicating success
+        @@rtype bool
         """
         if not self.__serial:
-            return
+            return False
         
         rawReplMessage = b"raw REPL; CTRL-B to exit\r\n"
         softRebootMessage = b"soft reboot\r\n"
@@ -92,16 +95,25 @@
         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():
+            return False
         self.__serial.write(b"\x04")        # send CTRL-D to soft reset
         self.__serial.readUntil(softRebootMessage)
+        if self.__serial.hasTimedOut():
+            return False
         
         # some MicroPython devices seem to need to be convinced in some
         # special way
         data = self.__serial.readUntil(rawReplMessage)
+        if self.__serial.hasTimedOut():
+            return False
         if not data.endswith(rawReplMessage):
             self.__serial.write(b"\r\x01")  # send CTRL-A again
             self.__serial.readUntil(rawReplMessage)
+            if self.__serial.hasTimedOut():
+                return False
         self.__serial.readAll()             # read all data and discard it
+        return True
     
     def __rawOff(self):
         """
@@ -128,7 +140,13 @@
         result = bytearray()
         err = b""
         
-        self.__rawOn()
+        ok = self.__rawOn()
+        if not ok:
+            return (
+                b"",
+                b"Could not switch to raw mode. Is the device switched on?"
+            )
+        
         QThread.msleep(10)
         for command in commands:
             if command:
@@ -136,9 +154,14 @@
                 self.__serial.write(commandBytes + b"\x04")
                 # read until prompt
                 response = self.__serial.readUntil(b"\x04>")
-                # split stdout, stderr
-                out, err = response[2:-2].split(b"\x04")
-                result += out
+                if self.__serial.hasTimedOut():
+                    return b"", b"Timeout while processing commands."
+                if b"\x04" in response[2:-2]:
+                    # split stdout, stderr
+                    out, err = response[2:-2].split(b"\x04")
+                    result += out
+                else:
+                    err = b"invalid response received: " + response
                 if err:
                     return b"", err
         QThread.msleep(10)
@@ -511,21 +534,30 @@
                 "    try:",           # Pyboard (it doesn't have machine.RTC())
                 "        import pyb",
                 "        rtc = pyb.RTC()",
-                "        rtc.datetime(rtc_time)",
+                "        clock_time = rtc_time[:6] + (rtc_time[6] + 1, 0)",
+                "        rtc.datetime(clock_time)",
                 "    except:",
                 "        try:",
                 "            import machine",
                 "            rtc = machine.RTC()",
-                "            try:",     # ESP8266 uses rtc.datetime()
-                "                rtc.datetime(rtc_time)",
+                "            try:",     # ESP8266 may use rtc.datetime()
+                "                clock_time = rtc_time[:6] +"
+                " (rtc_time[6] + 1, 0)",
+                "                rtc.datetime(clock_time)",
                 "            except:",  # ESP32 uses rtc.init()
-                "                rtc.init(rtc_time)",
+                "                rtc.init(rtc_time[:6])",
                 "        except:",
-                "            pass",
+                "            try:",
+                "                import rtc, time",
+                "                clock=rtc.RTC()",
+                "                clock.datetime = time.struct_time(rtc_time +"
+                " (-1, -1))",
+                "            except:",
+                "                pass",
             ]),
             "set_time({0})".format((now.tm_year, now.tm_mon, now.tm_mday,
-                                    now.tm_wday + 1, now.tm_hour, now.tm_min,
-                                    now.tm_sec, 0))
+                                    now.tm_hour, now.tm_min, now.tm_sec,
+                                    now.tm_wday))
         ]
         out, err = self.__execute(commands)
         if err:
@@ -570,21 +602,17 @@
     @signal rsyncDone(localName, deviceName) emitted after the rsync operation
         has been completed
     @signal rsyncMessages(list) emitted with a list of messages
+    @signal removeDirectoryDone() emitted after a directory has been deleted
+    @signal createDirectoryDone() emitted after a directory was created
+    @signal synchTimeDone() emitted after the time was synchronizde to the
+        device
+    @signal showTimeDone(dateTime) emitted after the date and time was fetched
+        from the connected device
+    @signal showVersionDone(versionInfo) emitted after the version information
+        was fetched from the connected device
     
-    @signal longListFilesFailed(exc) emitted with a failure message to indicate
-        a failed long listing operation
-    @signal currentDirFailed(exc) emitted with a failure message to indicate
-        that the current directory is not available
-    @signal currentDirChangeFailed(exc) emitted with a failure message to
-        indicate that the current directory could not be changed
-    @signal getFileFailed(exc) emitted with a failure message to indicate that
-        the file could not be fetched
-    @signal putFileFailed(exc) emitted with a failure message to indicate that
-        the file could not be copied
-    @signal deleteFileFailed(exc) emitted with a failure message to indicate
-        that the file could not be deleted on the device
-    @signal rsyncFailed(exc) emitted with a failure message to indicate that
-        the rsync operation could not be completed
+    @signal error(exc) emitted with a failure message to indicate a failure
+        during the most recent operation
     """
     longListFiles = pyqtSignal(tuple)
     currentDir = pyqtSignal(str)
@@ -594,14 +622,13 @@
     deleteFileDone = pyqtSignal(str)
     rsyncDone = pyqtSignal(str, str)
     rsyncMessages = pyqtSignal(list)
+    removeDirectoryDone = pyqtSignal()
+    createDirectoryDone = pyqtSignal()
+    synchTimeDone = pyqtSignal()
+    showTimeDone = pyqtSignal(str)
+    showVersionDone = pyqtSignal(dict)
     
-    longListFilesFailed = pyqtSignal(str)
-    currentDirFailed = pyqtSignal(str)
-    currentDirChangeFailed = pyqtSignal(str)
-    getFileFailed = pyqtSignal(str)
-    putFileFailed = pyqtSignal(str)
-    deleteFileFailed = pyqtSignal(str)
-    rsyncFailed = pyqtSignal(str)
+    error = pyqtSignal(str, str)
     
     def __init__(self, port, parent=None):
         """
@@ -650,7 +677,7 @@
                       name, (mode, size, time) in filesList]
             self.longListFiles.emit(tuple(result))
         except Exception as exc:
-            self.longListFilesFailed.emit(str(exc))
+            self.error.emit("lls", str(exc))
     
     @pyqtSlot()
     def pwd(self):
@@ -661,7 +688,7 @@
             pwd = self.__fs.pwd()
             self.currentDir.emit(pwd)
         except Exception as exc:
-            self.currentDirFailed.emit(str(exc))
+            self.error.emit("pwd", str(exc))
     
     @pyqtSlot(str)
     def cd(self, dirname):
@@ -675,7 +702,7 @@
             self.__fs.cd(dirname)
             self.currentDirChanged.emit(dirname)
         except Exception as exc:
-            self.currentDirChangeFailed.emit(str(exc))
+            self.error.emit("cd", str(exc))
     
     @pyqtSlot(str)
     @pyqtSlot(str, str)
@@ -696,7 +723,7 @@
             self.__fs.get(deviceFileName, hostFileName)
             self.getFileDone.emit(deviceFileName, hostFileName)
         except Exception as exc:
-            self.getFileFailed.emit(str(exc))
+            self.error.emit("get", str(exc))
     
     @pyqtSlot(str)
     @pyqtSlot(str, str)
@@ -713,7 +740,7 @@
             self.__fs.put(hostFileName, deviceFileName)
             self.putFileDone.emit(hostFileName, deviceFileName)
         except Exception as exc:
-            self.putFileFailed.emit(str(exc))
+            self.error.emit("put", str(exc))
     
     @pyqtSlot(str)
     def delete(self, deviceFileName):
@@ -727,7 +754,7 @@
             self.__fs.rm(deviceFileName)
             self.deleteFileDone.emit(deviceFileName)
         except Exception as exc:
-            self.deleteFileFailed.emit(str(exc))
+            self.error.emit("delete", str(exc))
     
     def __rsync(self, hostDirectory, deviceDirectory, mirror=True):
         """
@@ -841,9 +868,11 @@
         
         return messages, errors
     
+    @pyqtSlot(str, str)
+    @pyqtSlot(str, str, bool)
     def rsync(self, hostDirectory, deviceDirectory, mirror=True):
         """
-        Public method to synchronize a local directory to the device.
+        Public slot to synchronize a local directory to the device.
         
         @param hostDirectory name of the local directory
         @type str
@@ -856,9 +885,82 @@
         messages, errors = self.__rsync(hostDirectory, deviceDirectory,
                                         mirror=mirror)
         if errors:
-            self.rsyncFailed.emit("\n".join(errors))
+            self.error.emit("rsync", "\n".join(errors))
         
         if messages:
             self.rsyncMessages.emit(messages)
         
         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.__fs.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.__fs.rmrf(dirname, recursive=True, force=True)
+            else:
+                self.__fs.rmdir(dirname)
+            self.removeDirectoryDone.emit()
+        except Exception as exc:
+            self.error.emit("rmdir", str(exc))
+    
+    ##################################################################
+    ## some non-filesystem related methods below
+    ##################################################################
+    
+    @pyqtSlot()
+    def synchronizeTime(self):
+        """
+        Public slot to set the time of the connected device to the local
+        computer's time.
+        """
+        try:
+            self.__fs.syncTime()
+            self.synchTimeDone.emit()
+        except Exception as exc:
+            self.error.emit("rmdir", str(exc))
+    
+    @pyqtSlot()
+    def showTime(self):
+        """
+        Public slot to get the current date and time of the device.
+        """
+        try:
+            dt = self.__fs.showTime()
+            self.showTimeDone.emit(dt)
+        except Exception as exc:
+            self.error.emit("showTime", str(exc))
+    
+    @pyqtSlot()
+    def showVersion(self):
+        """
+        Public slot to get the version info for the MicroPython run by the
+        connected device.
+        """
+        try:
+            versionInfo = self.__fs.version()
+            self.showVersionDone.emit(versionInfo)
+        except Exception as exc:
+            self.error.emit("showVersion", str(exc))
--- a/eric6/MicroPython/MicroPythonReplWidget.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.py	Wed Jul 24 20:12:19 2019 +0200
@@ -218,8 +218,6 @@
         
         self.__populateDeviceTypeComboBox()
         
-        self.replEdit.setContextMenuPolicy(Qt.CustomContextMenu)
-        
         self.replEdit.installEventFilter(self)
         
         self.replEdit.customContextMenuRequested.connect(
@@ -334,7 +332,9 @@
         menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys)
         menu.addAction(self.tr("Paste"), self.__paste, pasteKeys)
         menu.addSeparator()
-        # TODO: add device specific context menu entries
+        if self.__device is not None:
+            # allow device interface to add specific context menu entries
+            self.__device.addActions(menu)
         menu.exec_(self.replEdit.mapToGlobal(pos))
     
     def setConnected(self, connected):
--- a/eric6/MicroPython/MicroPythonReplWidget.ui	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonReplWidget.ui	Wed Jul 24 20:12:19 2019 +0200
@@ -146,6 +146,9 @@
    </item>
    <item>
     <widget class="QTextEdit" name="replEdit">
+     <property name="contextMenuPolicy">
+      <enum>Qt::CustomContextMenu</enum>
+     </property>
      <property name="undoRedoEnabled">
       <bool>false</bool>
      </property>
--- a/eric6/MicroPython/MicroPythonSerialPort.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicroPythonSerialPort.py	Wed Jul 24 20:12:19 2019 +0200
@@ -32,6 +32,7 @@
         
         self.__connected = False
         self.__timeout = timeout      # 10s default timeout
+        self.__timedOut = False
     
     def setTimeout(self, timeout):
         """
@@ -83,6 +84,15 @@
         """
         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
@@ -96,6 +106,7 @@
         @rtype bytes
         """
         data = bytearray()
+        self.__timedOut = False
         
         t = QTime()
         t.start()
@@ -111,6 +122,7 @@
 #            else:
 #                break
             if t.elapsed() > self.__timeout:
+                self.__timedOut = True
                 break
         
         return bytes(data)
--- a/eric6/MicroPython/MicrobitDevices.py	Tue Jul 23 19:43:14 2019 +0200
+++ b/eric6/MicroPython/MicrobitDevices.py	Wed Jul 24 20:12:19 2019 +0200
@@ -104,7 +104,6 @@
         self.__plotterActive = on
         self.microPython.setActionButtons(files=not on)
     
-    # TODO: not yet implemented
     def canStartFileManager(self):
         """
         Public method to determine, if a File Manager can be started.
@@ -113,9 +112,14 @@
             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.")
+        if self.__replActive or self.__plotterActive:
+            return False, self.tr("The file manager and the REPL/plotter use"
+                                  " the same USB serial connection. Only one"
+                                  " can be active at any time. Disconnect the"
+                                  " REPL/plotter and try again.")
+        else:
+            return True, ""
     
-    # TODO: not yet implemented
     def setFileManager(self, on):
         """
         Public method to set the File Manager status and dependent status.
@@ -123,7 +127,9 @@
         @param on flag indicating the active status
         @type bool
         """
-        pass
+        self.__fileManagerActive = on
+        self.microPython.setActionButtons(
+            run=not on, repl=not on, chart=HAS_QTCHART and not on)
     
     @pyqtSlot()
     def handleDataFlood(self):

eric ide

mercurial