eric6/MicroPython/MicroPythonFileManagerWidget.py

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
permissions
-rw-r--r--

MicroPython: continued implementing the file manager widget.

# -*- 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

from PyQt5.QtCore import pyqtSlot, Qt, QPoint
from PyQt5.QtWidgets import (
    QWidget, QTreeWidgetItem, QHeaderView, QMenu, QInputDialog, QLineEdit
)

from E5Gui import E5MessageBox, E5PathPickerDialog
from E5Gui.E5PathPicker import E5PathPickerModes
from E5Gui.E5Application import e5App

from .Ui_MicroPythonFileManagerWidget import Ui_MicroPythonFileManagerWidget

from .MicroPythonFileSystem import MicroPythonFileManager
from .MicroPythonFileSystemUtilities import (
    mtime2string, mode2string, decoratedName, listdirStat
)

import UI.PixmapCache
import Preferences
import Utilities


class MicroPythonFileManagerWidget(QWidget, Ui_MicroPythonFileManagerWidget):
    """
    Class implementing a file manager for MicroPython devices.
    """
    def __init__(self, port, parent=None):
        """
        Constructor
        
        @param port port name of the device
        @type str
        @param parent reference to the parent widget
        @type QWidget
        """
        super(MicroPythonFileManagerWidget, self).__init__(parent)
        self.setupUi(self)
        
        self.syncButton.setIcon(UI.PixmapCache.getIcon("2rightarrow"))
        self.putButton.setIcon(UI.PixmapCache.getIcon("1rightarrow"))
        self.getButton.setIcon(UI.PixmapCache.getIcon("1leftarrow"))
        self.localUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
        self.deviceUpButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
        
        self.putButton.setEnabled(False)
        self.getButton.setEnabled(False)
        
        self.localFileTreeWidget.header().setSortIndicator(
            0, Qt.AscendingOrder)
        self.deviceFileTreeWidget.header().setSortIndicator(
            0, Qt.AscendingOrder)
        
        self.__fileManager = MicroPythonFileManager(port, 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.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.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.__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):
        """
        Public method to start the widget.
        """
        self.__fileManager.connect()
        
        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)
        
        self.__fileManager.pwd()
    
    def stop(self):
        """
        Public method to stop the widget.
        """
        self.__fileManager.disconnect()
    
    @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.__fileManager.lls(dirname)
    
    @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, time in filesList:
            itm = QTreeWidgetItem(self.deviceFileTreeWidget,
                                  [name, mode, size, time])
            itm.setTextAlignment(1, Qt.AlignHCenter)
            itm.setTextAlignment(2, Qt.AlignRight)
        self.deviceFileTreeWidget.header().resizeSections(
            QHeaderView.ResizeToContents)
    
    def __listLocalFiles(self, dirname=""):
        """
        Private method to populate the local files list.
        
        @param dirname name of the local directory to be listed
        @type str
        """     # __IGNORE_WARNING_D234__
        
        if not dirname:
            dirname = os.getcwd()
        if dirname.endswith(os.sep):
            dirname = dirname[:-1]
        self.localCwd.setText(dirname)
        
        filesStatList = listdirStat(dirname)
        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]
        self.localFileTreeWidget.clear()
        for item in filesList:
            itm = QTreeWidgetItem(self.localFileTreeWidget, item)
            itm.setTextAlignment(1, Qt.AlignHCenter)
            itm.setTextAlignment(2, Qt.AlignRight)
        self.localFileTreeWidget.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)
    
    @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(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 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)
    
    @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)
        self.__fileManager.cd(dirname)
    
    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):
        """
        Private slot to copy the selected file to the connected device.
        """
        selectedItems = self.localFileTreeWidget.selectedItems()
        if selectedItems:
            filename = selectedItems[0].text(0).strip()
            if not filename.endswith("/"):
                # it is really a file
                if self.__isFileInList(filename, self.deviceFileTreeWidget):
                    # ask for overwrite permission
                    ok = E5MessageBox.yesNo(
                        self,
                        self.tr("Copy File to Device"),
                        self.tr("<p>The file <b>{0}</b> exists on the"
                                " connected device. Overwrite it?</p>")
                        .format(filename)
                    )
                    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(), deviceFilename)
                )
    
    @pyqtSlot()
    def on_getButton_clicked(self):
        """
        Private slot to copy the selected file from the connected device.
        """
        selectedItems = self.deviceFileTreeWidget.selectedItems()
        if selectedItems:
            filename = selectedItems[0].text(0).strip()
            if not filename.endswith("/"):
                # it is really a file
                if self.__isFileInList(filename, self.localFileTreeWidget):
                    # ask for overwrite permission
                    ok = E5MessageBox.yesNo(
                        self,
                        self.tr("Copy File from Device"),
                        self.tr("<p>The file <b>{0}</b> exists locally."
                                " Overwrite it?</p>")
                        .format(filename)
                    )
                    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(), localFilename)
                )
    
    @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
        )
    
    @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.__fileManager.lls(self.deviceCwd.text())
    
    @pyqtSlot(list)
    def __handleRsyncMessages(self, messages):
        """
        Private slot to handle messages from the rsync operation.
        
        @param messages list of message generated by the rsync operation
        @type list
        """
        E5MessageBox.information(
            self,
            self.tr("rsync Messages"),
            self.tr("""<p>rsync gave the following messages</p>"""
                    """<ul><li>{0}</li></ul>""").format(
                "</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)

eric ide

mercurial