Wed, 24 Jul 2019 20:12:19 +0200
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)