src/eric7/MicroPython/MicroPythonFileManagerWidget.py

branch
eric7
changeset 10523
e4069ddd7dc7
parent 10518
1682f3203ae5
child 10690
fab36645aa7d
diff -r c04e878aa308 -r e4069ddd7dc7 src/eric7/MicroPython/MicroPythonFileManagerWidget.py
--- a/src/eric7/MicroPython/MicroPythonFileManagerWidget.py	Tue Jan 23 12:21:15 2024 +0100
+++ b/src/eric7/MicroPython/MicroPythonFileManagerWidget.py	Wed Jan 24 18:52:50 2024 +0100
@@ -7,6 +7,7 @@
 Module implementing a file manager for MicroPython devices.
 """
 
+import contextlib
 import os
 import shutil
 
@@ -86,6 +87,7 @@
 
         self.openButton.setEnabled(False)
         self.saveButton.setEnabled(False)
+        self.saveAsButton.setEnabled(False)
 
         self.localFileTreeWidget.header().setSortIndicator(
             0, Qt.SortOrder.AscendingOrder
@@ -144,6 +146,10 @@
         act.setCheckable(True)
         act.setChecked(Preferences.getMicroPython("ShowHiddenLocal"))
         act.triggered[bool].connect(self.__localHiddenChanged)
+        self.__localMenu.addSeparator()
+        self.__localClearSelectionAct = self.__localMenu.addAction(
+            self.tr("Clear Selection"), self.__clearLocalSelection
+        )
 
         self.__deviceMenu = QMenu(self)
         if not isMicrobitDeviceWithMPy:
@@ -177,14 +183,20 @@
             self.__deviceMenu.addAction(
                 self.tr("Show Filesystem Info"), self.__showFileSystemInfo
             )
+        self.__deviceMenu.addSeparator()
+        self.__deviceClearSelectionAct = self.__deviceMenu.addAction(
+            self.tr("Clear Selection"), self.__clearDeviceSelection
+        )
 
     def start(self):
         """
         Public method to start the widget.
         """
+        self.__viewmanager = ericApp().getObject("ViewManager")
+        self.__viewmanager.editorCountChanged.connect(self.__updateSaveButtonStates)
+
         dirname = ""
-        vm = ericApp().getObject("ViewManager")
-        aw = vm.activeWindow()
+        aw = self.__viewmanager.activeWindow()
         if aw and FileSystemUtilities.isPlainFileName(aw.getFileName()):
             dirname = os.path.dirname(aw.getFileName())
         if not dirname:
@@ -193,22 +205,38 @@
                 or Preferences.getMultiProject("Workspace")
                 or os.path.expanduser("~")
             )
-        self.__listLocalFiles(dirname)
+        self.__listLocalFiles(dirname=dirname)
 
         if self.__repl.deviceSupportsLocalFileAccess():
             dirname = self.__repl.getDeviceWorkspace()
             if dirname:
-                self.__listLocalFiles(dirname, True)
+                self.__listLocalFiles(dirname=dirname, localDevice=True)
                 return
 
         # list files via device script
+        self.__expandedDeviceEntries = []
         self.__fileManager.pwd()
 
     def stop(self):
         """
         Public method to stop the widget.
         """
-        pass
+        self.__viewmanager.editorCountChanged.disconnect(self.__updateSaveButtonStates)
+
+    @pyqtSlot()
+    def __updateSaveButtonStates(self):
+        """
+        Private slot to update the enabled state of the save buttons.
+        """
+        enable = bool(len(self.deviceFileTreeWidget.selectedItems()))
+        if enable:
+            enable &= not (
+                self.deviceFileTreeWidget.selectedItems()[0].text(0).endswith("/")
+            )
+        editorsCount = self.__viewmanager.getOpenEditorsCount()
+
+        self.saveButton.setEnabled(enable and bool(editorsCount))
+        self.saveAsButton.setEnabled(bool(editorsCount))
 
     @pyqtSlot(str, str)
     def __handleError(self, method, error):
@@ -240,6 +268,25 @@
         self.deviceCwd.setText(dirname)
         self.__newDeviceList()
 
+    def __findDirectoryItem(self, dirPath, fileTreeWidget):
+        """
+        Private method to find a file tree item for the given path.
+
+        @param dirPath path to be searched for
+        @type str
+        @param fileTreeWidget reference to the file list to be searched
+        @type QTreeWidget
+        @return reference to the item for the path
+        @rtype QTreeWidgetItem
+        """
+        itm = fileTreeWidget.topLevelItem(0)
+        while itm is not None:
+            if itm.data(0, Qt.ItemDataRole.UserRole) == dirPath:
+                return itm
+            itm = fileTreeWidget.itemBelow(itm)
+
+        return None
+
     @pyqtSlot(tuple)
     def __handleLongListFiles(self, filesList):
         """
@@ -249,36 +296,71 @@
             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]
+        if filesList:
+            dirPath = os.path.dirname(filesList[0][-1])
+            dirItem = (
+                self.__findDirectoryItem(dirPath, self.deviceFileTreeWidget)
+                if dirPath != self.deviceCwd.text()
+                else None
             )
-            itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter)
-            itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight)
+
+            if dirItem:
+                dirItem.takeChildren()
+            else:
+                self.deviceFileTreeWidget.clear()
+
+            for name, mode, size, dateTime, filePath in filesList:
+                itm = QTreeWidgetItem(
+                    self.deviceFileTreeWidget if dirItem is None else dirItem,
+                    [name, mode, size, dateTime],
+                )
+                itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter)
+                itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight)
+                itm.setData(0, Qt.ItemDataRole.UserRole, filePath)
+                if name.endswith("/"):
+                    itm.setChildIndicatorPolicy(
+                        QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator
+                    )
         self.deviceFileTreeWidget.header().resizeSections(
             QHeaderView.ResizeMode.ResizeToContents
         )
 
-    def __listLocalFiles(self, dirname="", localDevice=False):
+        if self.__expandedDeviceEntries:
+            dirPath = self.__expandedDeviceEntries.pop(0)
+            dirItem = self.__findDirectoryItem(dirPath, self.deviceFileTreeWidget)
+            if dirItem:
+                dirItem.setExpanded(True)
+
+    def __listLocalFiles(self, dirname="", localDevice=False, parentItem=None):
         """
         Private method to populate the local files list.
 
-        @param dirname name of the local directory to be listed
-        @type str
+        @param dirname name of the local directory to be listed (defaults to "")
+        @type str (optional)
         @param localDevice flag indicating device access via local file system
-        @type bool
+            (defaults to False)
+        @type bool (optional)
+        @param parentItem reference to the parent item (defaults to None)
+        @type QTreeWidgetItem (optional)
         """
-        if not dirname:
-            dirname = os.getcwd()
-        if dirname != os.sep and dirname.endswith(os.sep):
-            dirname = dirname[:-1]
-        if localDevice:
-            self.deviceCwd.setText(dirname)
-            showHidden = Preferences.getMicroPython("ShowHiddenDevice")
+        if parentItem:
+            dirname = parentItem.data(0, Qt.ItemDataRole.UserRole)
+            showHidden = (
+                Preferences.getMicroPython("ShowHiddenDevice")
+                if localDevice
+                else Preferences.getMicroPython("ShowHiddenLocal")
+            )
         else:
-            self.localCwd.setText(dirname)
-            showHidden = Preferences.getMicroPython("ShowHiddenLocal")
+            if not dirname:
+                dirname = os.getcwd()
+            if dirname != os.sep and 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 = [
@@ -287,19 +369,61 @@
                 mode2string(s[0]),
                 str(s[6]),
                 mtime2string(s[8]),
+                os.path.join(dirname, f),
             )
             for f, s in filesStatList
         ]
         fileTreeWidget = (
             self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget
         )
-        fileTreeWidget.clear()
+        if parentItem:
+            parentItem.takeChildren()
+        else:
+            fileTreeWidget.clear()
+            parentItem = fileTreeWidget
         for item in filesList:
-            itm = QTreeWidgetItem(fileTreeWidget, item)
+            itm = QTreeWidgetItem(parentItem, item[:4])
             itm.setTextAlignment(1, Qt.AlignmentFlag.AlignHCenter)
             itm.setTextAlignment(2, Qt.AlignmentFlag.AlignRight)
+            itm.setData(0, Qt.ItemDataRole.UserRole, item[4])
+            if os.path.isdir(item[4]):
+                itm.setChildIndicatorPolicy(
+                    QTreeWidgetItem.ChildIndicatorPolicy.ShowIndicator
+                )
         fileTreeWidget.header().resizeSections(QHeaderView.ResizeMode.ResizeToContents)
 
+    def __repopulateLocalFilesList(self, dirname="", localDevice=False):
+        """
+        Private method to re-populate the local files tree.
+
+        @param dirname name of the local directory to be listed (defaults to "")
+        @type str (optional)
+        @param localDevice flag indicating device access via local file system
+            (defaults to False)
+        @type bool (optional)
+        """
+        fileTreeWidget = (
+            self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget
+        )
+
+        # Step 1: record all expanded directories
+        expanded = []
+        itm = fileTreeWidget.topLevelItem(0)
+        while itm:
+            if itm.isExpanded():
+                expanded.append(itm.data(0, Qt.ItemDataRole.UserRole))
+            itm = fileTreeWidget.itemBelow(itm)
+
+        # Step 2: re-populate the top level directory
+        self.__listLocalFiles(dirname=dirname, localDevice=localDevice)
+
+        # Step 3: re-populate expanded directories
+        itm = fileTreeWidget.topLevelItem(0)
+        while itm:
+            if itm.data(0, Qt.ItemDataRole.UserRole) in expanded:
+                itm.setExpanded(True)
+            itm = fileTreeWidget.itemBelow(itm)
+
     @pyqtSlot(QTreeWidgetItem, int)
     def on_localFileTreeWidget_itemActivated(self, item, column):
         """
@@ -313,19 +437,21 @@
         @param column column of the activation
         @type int
         """
-        name = os.path.join(self.localCwd.text(), item.text(0))
-        if name.endswith("/"):
+        name = item.data(0, Qt.ItemDataRole.UserRole)
+        if item.text(0).endswith("/"):
             # directory names end with a '/'
-            self.__listLocalFiles(name[:-1])
+            self.__listLocalFiles(dirname=name)
         elif MimeTypes.isTextFile(name):
-            ericApp().getObject("ViewManager").getEditor(name)
+            self.__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()))
+        enable = bool(self.localFileTreeWidget.selectedItems())
+        self.__localClearSelectionAct.setEnabled(enable)
+
         if enable:
             enable &= not (
                 self.localFileTreeWidget.selectedItems()[0].text(0).endswith("/")
@@ -333,6 +459,18 @@
         self.putButton.setEnabled(enable)
         self.putAsButton.setEnabled(enable)
 
+    @pyqtSlot(QTreeWidgetItem)
+    def on_localFileTreeWidget_itemExpanded(self, item):
+        """
+        Private slot handling the expansion of a local directory item.
+
+        @param item reference to the directory item
+        @type QTreeWidgetItem
+        """
+        if item.childCount() == 0:
+            # it was not populated yet
+            self.__listLocalFiles(parentItem=item)
+
     @pyqtSlot(str)
     def on_localCwd_textChanged(self, cwd):
         """
@@ -350,7 +488,7 @@
         """
         cwd = self.localCwd.text()
         dirname = os.path.dirname(cwd)
-        self.__listLocalFiles(dirname)
+        self.__listLocalFiles(dirname=dirname)
 
     @pyqtSlot()
     def on_localHomeButton_clicked(self):
@@ -362,7 +500,7 @@
             or Preferences.getMultiProject("Workspace")
             or os.path.expanduser("~")
         )
-        self.__listLocalFiles(dirname)
+        self.__listLocalFiles(dirname=dirname)
 
     @pyqtSlot()
     def on_localReloadButton_clicked(self):
@@ -370,7 +508,7 @@
         Private slot to reload the local list.
         """
         dirname = self.localCwd.text()
-        self.__listLocalFiles(dirname)
+        self.__repopulateLocalFilesList(dirname=dirname)
 
     @pyqtSlot(QTreeWidgetItem, int)
     def on_deviceFileTreeWidget_itemActivated(self, item, column):
@@ -385,11 +523,11 @@
         @param column column of the activation
         @type int
         """
+        name = item.data(0, Qt.ItemDataRole.UserRole)
         if self.__repl.deviceSupportsLocalFileAccess():
-            name = os.path.join(self.deviceCwd.text(), item.text(0))
-            if name.endswith("/"):
+            if item.text(0).endswith("/"):
                 # directory names end with a '/'
-                self.__listLocalFiles(name[:-1], True)
+                self.__listLocalFiles(dirname=name)
             else:
                 if not os.path.exists(name):
                     EricMessageBox.warning(
@@ -401,25 +539,16 @@
                     )
                     return
                 if MimeTypes.isTextFile(name):
-                    ericApp().getObject("ViewManager").getEditor(name)
+                    self.__viewmanager.getEditor(name)
         else:
-            cwd = self.deviceCwd.text()
-            if cwd:
-                name = (
-                    cwd + item.text(0)
-                    if cwd.endswith("/")
-                    else "{0}/{1}".format(cwd, item.text(0))
-                )
-            else:
-                name = item.text(0)
-            if name.endswith("/"):
+            if item.text(0).endswith("/"):
                 # directory names end with a '/'
-                self.__fileManager.cd(name[:-1])
+                self.__fileManager.cd(name)
             else:
                 data = self.__fileManager.getData(name)
                 try:
                     text = data.decode(encoding="utf-8")
-                    ericApp().getObject("ViewManager").newEditorWithText(
+                    self.__viewmanager.newEditorWithText(
                         text, fileName=FileSystemUtilities.deviceFileName(name)
                     )
                 except UnicodeDecodeError:
@@ -437,16 +566,38 @@
         """
         Private slot handling a change of selection in the local pane.
         """
-        enable = bool(len(self.deviceFileTreeWidget.selectedItems()))
+        enable = bool(self.deviceFileTreeWidget.selectedItems())
+        self.__deviceClearSelectionAct.setEnabled(enable)
+
         if enable:
             enable &= not (
                 self.deviceFileTreeWidget.selectedItems()[0].text(0).endswith("/")
             )
+
         self.getButton.setEnabled(enable)
         self.getAsButton.setEnabled(enable)
 
         self.openButton.setEnabled(enable)
-        self.saveButton.setEnabled(enable)
+
+        self.__updateSaveButtonStates()
+
+    @pyqtSlot(QTreeWidgetItem)
+    def on_deviceFileTreeWidget_itemExpanded(self, item):
+        """
+        Private slot handling the expansion of a local directory item.
+
+        @param item reference to the directory item
+        @type QTreeWidgetItem
+        """
+        if item.childCount() == 0:
+            # it was not populated yet
+            if self.__repl.deviceSupportsLocalFileAccess():
+                self.__listLocalFiles(localDevice=True, parentItem=item)
+            else:
+                self.__fileManager.lls(
+                    item.data(0, Qt.ItemDataRole.UserRole),
+                    showHidden=Preferences.getMicroPython("ShowHiddenDevice"),
+                )
 
     @pyqtSlot(str)
     def on_deviceCwd_textChanged(self, cwd):
@@ -466,7 +617,7 @@
         cwd = self.deviceCwd.text()
         dirname = os.path.dirname(cwd)
         if self.__repl.deviceSupportsLocalFileAccess():
-            self.__listLocalFiles(dirname, True)
+            self.__listLocalFiles(dirname=dirname, localDevice=True)
         else:
             self.__fileManager.cd(dirname)
 
@@ -478,7 +629,7 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             dirname = self.__repl.getDeviceWorkspace()
             if dirname:
-                self.__listLocalFiles(dirname, True)
+                self.__listLocalFiles(dirname=dirname, localDevice=True)
                 return
 
         # list files via device script
@@ -491,28 +642,34 @@
         """
         dirname = self.deviceCwd.text()
         if self.__repl.deviceSupportsLocalFileAccess():
-            self.__listLocalFiles(dirname, True)
+            self.__repopulateLocalFilesList(dirname=dirname, localDevice=True)
         else:
             if dirname:
                 self.__newDeviceList()
             else:
                 self.__fileManager.pwd()
 
-    def __isFileInList(self, filename, treeWidget):
+    def __isFileInList(self, filename, parent):
         """
         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
-        @type QTreeWidget
+        @param parent reference to the parent to be checked against
+        @type QTreeWidget or QTreeWidgetItem
         @return flag indicating that the file name is present
         @rtype bool
         """
-        itemCount = treeWidget.topLevelItemCount()
-        return itemCount > 0 and any(
-            treeWidget.topLevelItem(row).text(0) == filename for row in range(itemCount)
-        )
+        if isinstance(parent, QTreeWidgetItem):
+            itemCount = parent.childCount()
+            return itemCount > 0 and any(
+                parent.child(row).text(0) == filename for row in range(itemCount)
+            )
+        else:
+            itemCount = parent.topLevelItemCount()
+            return itemCount > 0 and any(
+                parent.topLevelItem(row).text(0) == filename for row in range(itemCount)
+            )
 
     @pyqtSlot()
     def on_putButton_clicked(self, putAs=False):
@@ -524,8 +681,9 @@
         """
         selectedItems = self.localFileTreeWidget.selectedItems()
         if selectedItems:
-            filename = selectedItems[0].text(0).strip()
-            if not filename.endswith("/"):
+            filepath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+            filename = os.path.basename(filepath)
+            if not selectedItems[0].text(0).endswith("/"):
                 # it is really a file
                 if putAs:
                     deviceFilename, ok = QInputDialog.getText(
@@ -535,12 +693,28 @@
                         QLineEdit.EchoMode.Normal,
                         filename,
                     )
-                    if not ok or not filename:
+                    if not ok or not deviceFilename:
                         return
                 else:
                     deviceFilename = filename
 
-                if self.__isFileInList(deviceFilename, self.deviceFileTreeWidget):
+                selectedDeviceItems = self.deviceFileTreeWidget.selectedItems()
+                if selectedDeviceItems:
+                    item = selectedDeviceItems[0]
+                    if not item.text(0).endswith("/"):
+                        # it is no directory, take its parent
+                        item = item.parent()
+                    devicePath = (
+                        self.deviceCwd.text()
+                        if item is None
+                        else item.data(0, Qt.ItemDataRole.UserRole)
+                    )
+                    deviceParent = item
+                else:
+                    devicePath = self.deviceCwd.text()
+                    deviceParent = self.deviceFileTreeWidget
+
+                if self.__isFileInList(deviceFilename, deviceParent):
                     # ask for overwrite permission
                     action, resultFilename = confirmOverwrite(
                         deviceFilename,
@@ -557,21 +731,16 @@
                         deviceFilename = os.path.basename(resultFilename)
 
                 if self.__repl.deviceSupportsLocalFileAccess():
-                    shutil.copy2(
-                        os.path.join(self.localCwd.text(), filename),
-                        os.path.join(self.deviceCwd.text(), deviceFilename),
-                    )
-                    self.__listLocalFiles(self.deviceCwd.text(), localDevice=True)
+                    shutil.copy2(filepath, os.path.join(devicePath, deviceFilename))
+                    self.__listLocalFiles(dirname=devicePath, 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
-                    )
+                    if devicePath:
+                        deviceFilename = (
+                            f"{devicePath}/{deviceFilename}"
+                            if devicePath != "/"
+                            else f"/{devicePath}"
+                        )
+                    self.__fileManager.put(filepath, deviceFilename)
 
     @pyqtSlot()
     def on_putAsButton_clicked(self):
@@ -592,6 +761,7 @@
         selectedItems = self.deviceFileTreeWidget.selectedItems()
         if selectedItems:
             filename = selectedItems[0].text(0).strip()
+            deviceFilename = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
             if not filename.endswith("/"):
                 # it is really a file
                 if getAs:
@@ -607,7 +777,23 @@
                 else:
                     localFilename = filename
 
-                if self.__isFileInList(localFilename, self.localFileTreeWidget):
+                selectedLocalItems = self.localFileTreeWidget.selectedItems()
+                if selectedLocalItems:
+                    item = selectedLocalItems[0]
+                    if not item.text(0).endswith("/"):
+                        # it is no directory, take its parent
+                        item = item.parent()
+                    localPath = (
+                        self.localCwd.text()
+                        if item is None
+                        else item.data(0, Qt.ItemDataRole.UserRole)
+                    )
+                    localParent = item
+                else:
+                    localPath = self.localCwd.text()
+                    localParent = self.localFileTreeWidget
+
+                if self.__isFileInList(localFilename, localParent):
                     # ask for overwrite permission
                     action, resultFilename = confirmOverwrite(
                         localFilename,
@@ -623,16 +809,16 @@
 
                 if self.__repl.deviceSupportsLocalFileAccess():
                     shutil.copy2(
-                        os.path.join(self.deviceCwd.text(), filename),
-                        os.path.join(self.localCwd.text(), localFilename),
+                        deviceFilename,
+                        os.path.join(localPath, localFilename),
                     )
-                    self.__listLocalFiles(self.localCwd.text())
+                    if isinstance(localParent, QTreeWidgetItem):
+                        self.__listLocalFiles(parentItem=localParent)
+                    else:
+                        self.__listLocalFiles(dirname=localPath)
                 else:
-                    deviceCwd = self.deviceCwd.text()
-                    if deviceCwd:
-                        filename = deviceCwd + "/" + filename
                     self.__fileManager.get(
-                        filename, os.path.join(self.localCwd.text(), localFilename)
+                        deviceFilename, os.path.join(localPath, localFilename)
                     )
 
     @pyqtSlot()
@@ -653,16 +839,61 @@
         @param localFile name of the local file
         @type str
         """
-        self.__listLocalFiles(self.localCwd.text())
+        localPath = os.path.dirname(localFile)
+
+        # find the directory entry associated with the new file
+        localParent = self.__findDirectoryItem(localPath, self.localFileTreeWidget)
+
+        if localParent:
+            self.__listLocalFiles(parentItem=localParent)
+        else:
+            self.__listLocalFiles(dirname=self.localCwd.text())
 
     @pyqtSlot()
     def on_syncButton_clicked(self):
         """
         Private slot to synchronize the local directory to the device.
         """
+        # 1. local directory
+        selectedItems = self.localFileTreeWidget.selectedItems()
+        if selectedItems:
+            localName = selectedItems[0].text(0)
+            if localName.endswith("/"):
+                localDirPath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+            else:
+                # it is not a directory
+                localDirPath = os.path.dirname(
+                    selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+                )
+        else:
+            localName = ""
+            localDirPath = self.localCwd.text()
+
+        # 2. device directory
+        selectedItems = self.deviceFileTreeWidget.selectedItems()
+        if selectedItems:
+            if not selectedItems[0].text(0).endswith("/"):
+                # it is not a directory
+                deviceDirPath = os.path.dirname(
+                    selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+                )
+            else:
+                deviceDirPath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+        else:
+            if localDirPath == self.localCwd.text():
+                # syncronize complete local directory
+                deviceDirPath = self.deviceCwd.text()
+            else:
+                deviceCwd = self.deviceCwd.text()
+                deviceDirPath = (
+                    f"{deviceCwd}{localName[:-1]}"
+                    if deviceCwd.endswith("/")
+                    else f"{deviceCwd}/{localName[:-1]}"
+                )
+
         self.__fileManager.rsync(
-            self.localCwd.text(),
-            self.deviceCwd.text(),
+            localDirPath,
+            deviceDirPath,
             mirror=True,
             localDevice=self.__repl.deviceSupportsLocalFileAccess(),
         )
@@ -712,6 +943,15 @@
         """
         Private slot to initiate a new long list of the device directory.
         """
+        self.__expandedDeviceEntries.clear()
+        itm = self.deviceFileTreeWidget.topLevelItem(0)
+        while itm:
+            if itm.isExpanded():
+                self.__expandedDeviceEntries.append(
+                    itm.data(0, Qt.ItemDataRole.UserRole)
+                )
+            itm = self.deviceFileTreeWidget.itemBelow(itm)
+
         self.__fileManager.lls(
             self.deviceCwd.text(),
             showHidden=Preferences.getMicroPython("ShowHiddenDevice"),
@@ -724,25 +964,17 @@
         """
         selectedItems = self.deviceFileTreeWidget.selectedItems()
         if selectedItems:
-            filename = selectedItems[0].text(0).strip()
+            name = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
             if self.__repl.deviceSupportsLocalFileAccess():
-                name = os.path.join(self.deviceCwd.text(), filename)
-                if not name.endswith("/") and MimeTypes.isTextFile(name):
-                    ericApp().getObject("ViewManager").getEditor(name)
+                if not selectedItems[0].text(0).endswith("/") and MimeTypes.isTextFile(
+                    name
+                ):
+                    self.__viewmanager.getEditor(name)
             else:
-                cwd = self.deviceCwd.text()
-                if cwd:
-                    name = (
-                        cwd + filename
-                        if cwd.endswith("/")
-                        else "{0}/{1}".format(cwd, filename)
-                    )
-                else:
-                    name = filename
-                if not name.endswith("/"):
+                if not selectedItems[0].text(0).endswith("/"):
                     data = self.__fileManager.getData(name)
                     text = data.decode(encoding="utf-8")
-                    ericApp().getObject("ViewManager").newEditorWithText(
+                    self.__viewmanager.newEditorWithText(
                         text, "Python3", FileSystemUtilities.deviceFileName(name)
                     )
 
@@ -754,12 +986,14 @@
         @param saveAs flag indicating to save the file with a new name
         @type bool
         """
-        aw = ericApp().getObject("ViewManager").activeWindow()
+        aw = self.__viewmanager.activeWindow()
         if aw:
             selectedItems = self.deviceFileTreeWidget.selectedItems()
+
             if selectedItems:
-                filename = selectedItems[0].text(0).strip()
-                if filename.endswith("/"):
+                filepath = selectedItems[0].data(0, Qt.ItemDataRole.UserRole)
+                filename = os.path.basename(filepath)
+                if selectedItems[0].text(0).endswith("/"):
                     saveAs = True
             else:
                 saveAs = True
@@ -784,7 +1018,22 @@
                 if editorFileName != filename:
                     saveAs = True
 
-            if saveAs and self.__isFileInList(filename, self.deviceFileTreeWidget):
+            if selectedItems:
+                item = selectedItems[0]
+                if not item.text(0).endswith("/"):
+                    # it is no directory, take its parent
+                    item = item.parent()
+                devicePath = (
+                    self.deviceCwd.text()
+                    if item is None
+                    else item.data(0, Qt.ItemDataRole.UserRole)
+                )
+                deviceParent = item
+            else:
+                devicePath = self.deviceCwd.text()
+                deviceParent = self.deviceFileTreeWidget
+
+            if saveAs and self.__isFileInList(filename, deviceParent):
                 # ask for overwrite permission
                 action, resultFilename = confirmOverwrite(
                     filename,
@@ -800,28 +1049,26 @@
 
             text = aw.text()
             if self.__repl.deviceSupportsLocalFileAccess():
-                filename = os.path.join(self.deviceCwd.text(), filename)
+                filename = os.path.join(devicePath, filename)
                 os.makedirs(os.path.dirname(filename), exist_ok=True)
                 with open(filename, "w") as f:
                     f.write(text)
                 self.__newDeviceList()
                 aw.setFileName(filename)
             else:
-                if not filename.startswith("/"):
-                    deviceCwd = self.deviceCwd.text()
-                    if deviceCwd:
-                        filename = (
-                            deviceCwd + "/" + filename
-                            if deviceCwd != "/"
-                            else "/" + filename
-                        )
+                filename = (
+                    f"{devicePath}/{filename}"
+                    if devicePath != "/"
+                    else f"/{devicePath}"
+                )
                 dirname = filename.rsplit("/", 1)[0]
                 self.__fileManager.makedirs(dirname)
                 self.__fileManager.putData(filename, text.encode("utf-8"))
                 aw.setFileName(FileSystemUtilities.deviceFileName(filename))
 
             aw.setModified(False)
-            aw.resetOnlineChangeTraceInfo()
+            with contextlib.suppress(AttributeError):
+                aw.resetOnlineChangeTraceInfo()
 
     @pyqtSlot()
     def on_saveAsButton_clicked(self):
@@ -846,7 +1093,8 @@
         if hasSelection:
             name = self.localFileTreeWidget.selectedItems()[0].text(0)
             isDir = name.endswith("/")
-            isFile = not isDir
+            isLink = name.endswith("@")
+            isFile = not (isDir or isLink)
         else:
             isDir = False
             isFile = False
@@ -865,20 +1113,31 @@
         @type bool
         """
         cwdWidget = self.deviceCwd if localDevice else self.localCwd
+        fileTreeWidget = (
+            self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget
+        )
 
+        if fileTreeWidget.selectedItems():
+            defaultPath = fileTreeWidget.selectedItems()[0].data(
+                0, Qt.ItemDataRole.UserRole
+            )
+            if not os.path.isdir(defaultPath):
+                defaultPath = os.path.dirname(defaultPath)
+        else:
+            defaultPath = cwdWidget.text()
         dirPath, ok = EricPathPickerDialog.getStrPath(
             self,
             self.tr("Change Directory"),
             self.tr("Select Directory"),
             EricPathPickerModes.DIRECTORY_SHOW_FILES_MODE,
-            strPath=cwdWidget.text(),
-            defaultDirectory=cwdWidget.text(),
+            strPath=defaultPath,
+            defaultDirectory=defaultPath,
         )
         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)
+            self.__listLocalFiles(dirname=dirPath, localDevice=localDevice)
 
     @pyqtSlot()
     def __createLocalDirectory(self, localDevice=False):
@@ -889,6 +1148,19 @@
         @type bool
         """
         cwdWidget = self.deviceCwd if localDevice else self.localCwd
+        fileTreeWidget = (
+            self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget
+        )
+
+        if fileTreeWidget.selectedItems():
+            localItem = fileTreeWidget.selectedItems()[0]
+            defaultPath = localItem.data(0, Qt.ItemDataRole.UserRole)
+            if not os.path.isdir(defaultPath):
+                defaultPath = os.path.dirname(defaultPath)
+                localItem = localItem.parent()
+        else:
+            defaultPath = cwdWidget.text()
+            localItem = None
 
         dirPath, ok = QInputDialog.getText(
             self,
@@ -897,10 +1169,15 @@
             QLineEdit.EchoMode.Normal,
         )
         if ok and dirPath:
-            dirPath = os.path.join(cwdWidget.text(), dirPath)
+            dirPath = os.path.join(defaultPath, dirPath)
             try:
                 os.mkdir(dirPath)
-                self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice)
+                if localItem:
+                    self.__listLocalFiles(localDevice=localDevice, parentItem=localItem)
+                else:
+                    self.__listLocalFiles(
+                        dirname=cwdWidget.text(), localDevice=localDevice
+                    )
             except OSError as exc:
                 EricMessageBox.critical(
                     self,
@@ -926,9 +1203,10 @@
             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])
+        if bool(fileTreeWidget.selectedItems()):
+            localItem = fileTreeWidget.selectedItems()[0]
+            parentItem = localItem.parent()
+            dirname = localItem.data(0, Qt.ItemDataRole.UserRole)
             dlg = DeleteFilesConfirmationDialog(
                 self,
                 self.tr("Delete Directory Tree"),
@@ -938,7 +1216,14 @@
             if dlg.exec() == QDialog.DialogCode.Accepted:
                 try:
                     shutil.rmtree(dirname)
-                    self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice)
+                    if parentItem:
+                        self.__listLocalFiles(
+                            localDevice=localDevice, parentItem=parentItem
+                        )
+                    else:
+                        self.__listLocalFiles(
+                            dirname=cwdWidget.text(), localDevice=localDevice
+                        )
                 except Exception as exc:
                     EricMessageBox.critical(
                         self,
@@ -965,8 +1250,9 @@
             fileTreeWidget = self.localFileTreeWidget
 
         if bool(len(fileTreeWidget.selectedItems())):
-            name = fileTreeWidget.selectedItems()[0].text(0)
-            filename = os.path.join(cwdWidget.text(), name)
+            localItem = fileTreeWidget.selectedItems()[0]
+            parentItem = localItem.parent()
+            filename = localItem.data(0, Qt.ItemDataRole.UserRole)
             dlg = DeleteFilesConfirmationDialog(
                 self,
                 self.tr("Delete File"),
@@ -976,7 +1262,14 @@
             if dlg.exec() == QDialog.DialogCode.Accepted:
                 try:
                     os.remove(filename)
-                    self.__listLocalFiles(cwdWidget.text(), localDevice=localDevice)
+                    if parentItem:
+                        self.__listLocalFiles(
+                            localDevice=localDevice, parentItem=parentItem
+                        )
+                    else:
+                        self.__listLocalFiles(
+                            dirname=cwdWidget.text(), localDevice=localDevice
+                        )
                 except OSError as exc:
                     EricMessageBox.critical(
                         self,
@@ -996,16 +1289,13 @@
             (defaults to False)
         @type bool (optional)
         """
-        if localDevice:
-            cwdWidget = self.deviceCwd
-            fileTreeWidget = self.deviceFileTreeWidget
-        else:
-            cwdWidget = self.localCwd
-            fileTreeWidget = self.localFileTreeWidget
+        fileTreeWidget = (
+            self.deviceFileTreeWidget if localDevice else self.localFileTreeWidget
+        )
 
         if bool(len(fileTreeWidget.selectedItems())):
-            name = fileTreeWidget.selectedItems()[0].text(0)
-            filename = os.path.join(cwdWidget.text(), name)
+            localItem = fileTreeWidget.selectedItems()[0]
+            filename = localItem.data(0, Qt.ItemDataRole.UserRole)
             newname, ok = QInputDialog.getText(
                 self,
                 self.tr("Rename File"),
@@ -1043,6 +1333,14 @@
         Preferences.setMicroPython("ShowHiddenLocal", checked)
         self.on_localReloadButton_clicked()
 
+    @pyqtSlot()
+    def __clearLocalSelection(self):
+        """
+        Private slot to clear the local selection.
+        """
+        for item in self.localFileTreeWidget.selectedItems()[:]:
+            item.setSelected(False)
+
     ##################################################################
     ## Context menu methods for the device files below
     ##################################################################
@@ -1082,12 +1380,22 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             self.__changeLocalDirectory(True)
         else:
+            selectedItems = self.deviceFileTreeWidget.selectedItems()
+            if selectedItems:
+                item = selectedItems[0]
+                dirName = (
+                    item.data(0, Qt.ItemDataRole.UserRole)
+                    if item.text(0).endswith("/")
+                    else os.path.dirname(item.data(0, Qt.ItemDataRole.UserRole))
+                )
+            else:
+                dirName = self.deviceCwd.text()
             dirPath, ok = QInputDialog.getText(
                 self,
                 self.tr("Change Directory"),
                 self.tr("Enter the directory path on the device:"),
                 QLineEdit.EchoMode.Normal,
-                self.deviceCwd.text(),
+                dirName,
             )
             if ok and dirPath:
                 if not dirPath.startswith("/"):
@@ -1102,11 +1410,22 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             self.__createLocalDirectory(True)
         else:
+            selectedItems = self.deviceFileTreeWidget.selectedItems()
+            if selectedItems:
+                item = selectedItems[0]
+                defaultPath = (
+                    item.data(0, Qt.ItemDataRole.UserRole)
+                    if item.text(0).endswith("/")
+                    else os.path.dirname(item.data(0, Qt.ItemDataRole.UserRole))
+                )
+            else:
+                defaultPath = self.deviceCwd.text()
             dirPath, ok = QInputDialog.getText(
                 self,
                 self.tr("Create Directory"),
                 self.tr("Enter directory name:"),
                 QLineEdit.EchoMode.Normal,
+                defaultPath,
             )
             if ok and dirPath:
                 self.__fileManager.mkdir(dirPath)
@@ -1119,16 +1438,10 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             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]
+            if bool(self.deviceFileTreeWidget.selectedItems()):
+                dirname = self.deviceFileTreeWidget.selectedItems()[0].data(
+                    0, Qt.ItemDataRole.UserRole
+                )
                 dlg = DeleteFilesConfirmationDialog(
                     self,
                     self.tr("Delete Directory"),
@@ -1148,15 +1461,9 @@
             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]
+                dirname = self.deviceFileTreeWidget.selectedItems()[0].data(
+                    0, Qt.ItemDataRole.UserRole
+                )
                 dlg = DeleteFilesConfirmationDialog(
                     self,
                     self.tr("Delete Directory Tree"),
@@ -1174,16 +1481,10 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             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
+            if bool(self.deviceFileTreeWidget.selectedItems()):
+                filename = self.deviceFileTreeWidget.selectedItems()[0].data(
+                    0, Qt.ItemDataRole.UserRole
+                )
                 dlg = DeleteFilesConfirmationDialog(
                     self,
                     self.tr("Delete File"),
@@ -1201,16 +1502,10 @@
         if self.__repl.deviceSupportsLocalFileAccess():
             self.__renameLocalFile(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
+            if bool(self.deviceFileTreeWidget.selectedItems()):
+                filename = self.deviceFileTreeWidget.selectedItems()[0].data(
+                    0, Qt.ItemDataRole.UserRole
+                )
                 newname, ok = QInputDialog.getText(
                     self,
                     self.tr("Rename File"),
@@ -1271,3 +1566,11 @@
                 "<p>No file systems or file system information available.</p>"
             )
         EricMessageBox.information(self, self.tr("Filesystem Information"), msg)
+
+    @pyqtSlot()
+    def __clearDeviceSelection(self):
+        """
+        Private slot to clear the local selection.
+        """
+        for item in self.deviceFileTreeWidget.selectedItems()[:]:
+            item.setSelected(False)

eric ide

mercurial