Extended the project support for eric-ide server projects. server

Fri, 08 Mar 2024 15:30:53 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 08 Mar 2024 15:30:53 +0100
branch
server
changeset 10631
00f5aae565a3
parent 10630
552a790fd9bc
child 10632
1109854f15f9

Extended the project support for eric-ide server projects.

src/eric7/Graphics/PixmapDiagram.py file | annotate | diff | comparison | revisions
src/eric7/Graphics/SvgDiagram.py file | annotate | diff | comparison | revisions
src/eric7/Graphics/UMLDialog.py file | annotate | diff | comparison | revisions
src/eric7/HexEdit/HexEditMainWindow.py file | annotate | diff | comparison | revisions
src/eric7/IconEditor/IconEditorWindow.py file | annotate | diff | comparison | revisions
src/eric7/PdfViewer/PdfViewerWindow.py file | annotate | diff | comparison | revisions
src/eric7/Project/DebuggerPropertiesDialog.py file | annotate | diff | comparison | revisions
src/eric7/Project/DebuggerPropertiesDialog.ui file | annotate | diff | comparison | revisions
src/eric7/Project/Project.py file | annotate | diff | comparison | revisions
src/eric7/Project/ProjectBaseBrowser.py file | annotate | diff | comparison | revisions
src/eric7/Project/ProjectBrowserModel.py file | annotate | diff | comparison | revisions
src/eric7/Project/ProjectOthersBrowser.py file | annotate | diff | comparison | revisions
src/eric7/Project/ProjectSourcesBrowser.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py file | annotate | diff | comparison | revisions
--- a/src/eric7/Graphics/PixmapDiagram.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Graphics/PixmapDiagram.py	Fri Mar 08 15:30:53 2024 +0100
@@ -12,7 +12,6 @@
     QAction,
     QColor,
     QFont,
-    QImage,
     QPageLayout,
     QPainter,
     QPalette,
@@ -24,8 +23,10 @@
 from eric7 import Preferences
 from eric7.EricGui import EricPixmapCache
 from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
 from eric7.EricWidgets.EricZoomWidget import EricZoomWidget
+from eric7.SystemUtilities import FileSystemUtilities
 
 
 class PixmapDiagram(EricMainWindow):
@@ -186,8 +187,30 @@
         @return flag indicating success
         @rtype bool
         """
-        image = QImage(filename)
-        if image.isNull():
+        pixmap = QPixmap()
+        if FileSystemUtilities.isRemoteFileName(filename):
+            try:
+                data = (
+                    ericApp()
+                    .getObject("EricServer")
+                    .getServiceInterface("FileSystem")
+                    .readFile(filename)
+                )
+                pixmap.loadFromData(data)
+            except OSError as err:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("Pixmap-Viewer"),
+                    self.tr(
+                        """<p>The file <b>{0}</b> cannot be loaded.</p>"""
+                        """<p>Reason: {1}</p>"""
+                    ).format(filename, str(err)),
+                )
+                return False
+        else:
+            pixmap.load(filename)
+
+        if pixmap.isNull():
             EricMessageBox.warning(
                 self,
                 self.tr("Pixmap-Viewer"),
@@ -198,7 +221,7 @@
             )
             return False
 
-        self.pixmapLabel.setPixmap(QPixmap.fromImage(image))
+        self.pixmapLabel.setPixmap(pixmap)
         self.pixmapLabel.adjustSize()
         return True
 
--- a/src/eric7/Graphics/SvgDiagram.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Graphics/SvgDiagram.py	Fri Mar 08 15:30:53 2024 +0100
@@ -15,8 +15,11 @@
 
 from eric7 import Preferences
 from eric7.EricGui import EricPixmapCache
+from eric7.EricWidgets import EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
 from eric7.EricWidgets.EricZoomWidget import EricZoomWidget
+from eric7.SystemUtilities import FileSystemUtilities
 
 
 class SvgDiagram(EricMainWindow):
@@ -102,8 +105,7 @@
         self.resize(QSize(800, 600).expandedTo(self.minimumSizeHint()))
 
         self.zoom = 1.0
-        self.svgFile = svgFile
-        self.svgWidget.load(self.svgFile)
+        self.__loadSvgFile(svgFile)
         self.svgWidget.resize(self.svgWidget.renderer().defaultSize())
 
         self.__initActions()
@@ -112,6 +114,36 @@
 
         self.grabGesture(Qt.GestureType.PinchGesture)
 
+    def __loadSvgFile(self, svgFile):
+        """
+        Private method to load a given SVG file.
+
+        @param svgFile file path of the SVG file
+        @type str
+        """
+        self.svgFile = svgFile
+        if FileSystemUtilities.isRemoteFileName(svgFile):
+            try:
+                data = (
+                    ericApp()
+                    .getObject("EricServer")
+                    .getServiceInterface("FileSystem")
+                    .readFile(svgFile)
+                )
+                self.svgWidget.load(data)
+            except OSError as err:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("SVG-Viewer"),
+                    self.tr(
+                        "<p>The SVG file <b>{0}</b> could not be loaded.</p>"
+                        "<p>Reason: {1}</p>"
+                    ).format(svgFile, str(err)),
+                )
+                
+        else:
+            self.svgWidget.load(self.svgFile)
+
     def __initActions(self):
         """
         Private method to initialize the view actions.
--- a/src/eric7/Graphics/UMLDialog.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Graphics/UMLDialog.py	Fri Mar 08 15:30:53 2024 +0100
@@ -17,7 +17,10 @@
 
 from eric7.EricGui import EricPixmapCache
 from eric7.EricWidgets import EricFileDialog, EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
+from eric7.RemoteServerInterface import EricServerFileDialog
+from eric7.SystemUtilities import FileSystemUtilities
 
 from .ApplicationDiagramBuilder import ApplicationDiagramBuilder
 from .ImportsDiagramBuilder import ImportsDiagramBuilder
@@ -86,6 +89,9 @@
 
         self.__project = project
         self.__diagramType = diagramType
+        self.__remotefsInterface = (
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+        )
 
         self.scene = QGraphicsScene(0.0, 0.0, 800.0, 600.0)
         self.umlView = UMLGraphicsView(self.scene, parent=self)
@@ -243,34 +249,56 @@
         @type str
         """
         if not filename:
-            fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
-                self,
-                self.tr("Save Diagram"),
-                "",
-                self.tr("Eric Graphics File (*.egj);;All Files (*)"),
-                "",
-                EricFileDialog.DontConfirmOverwrite,
-            )
-            if not fname:
-                return
+            if FileSystemUtilities.isRemoteFileName(self.__project.getProjectPath()):
+                fname, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter(
+                    self,
+                    self.tr("Save Diagram"),
+                    self.__project.getProjectPath(),
+                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
+                    "",
+                )
+                if not fname:
+                    return
 
-            fpath = pathlib.Path(fname)
-            if not fpath.suffix:
-                ex = selectedFilter.split("(*")[1].split(")")[0]
-                if ex:
-                    fpath = fpath.with_suffix(ex)
-            if fpath.exists():
+                ext = self.__remotefsInterface.splitext(fname)[1]
+                if not ext:
+                    ex = selectedFilter.split("(*")[1].split(")")[0]
+                    if ex:
+                        fname += ex
+                filename = fname
+                fileExists = self.__remotefsInterface.exists(filename)
+            else:
+                fname, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
+                    self,
+                    self.tr("Save Diagram"),
+                    self.__project.getProjectPath(),
+                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
+                    "",
+                    EricFileDialog.DontConfirmOverwrite,
+                )
+                if not fname:
+                    return
+
+                fpath = pathlib.Path(fname)
+                if not fpath.suffix:
+                    ex = selectedFilter.split("(*")[1].split(")")[0]
+                    if ex:
+                        fpath = fpath.with_suffix(ex)
+                fileExists = fpath.exists()
+                filename = str(fpath)
+
+            if fileExists:
                 res = EricMessageBox.yesNo(
                     self,
                     self.tr("Save Diagram"),
                     self.tr(
-                        "<p>The file <b>{0}</b> already exists. Overwrite it?</p>"
-                    ).format(fpath),
+                        "<p>The file <b>{0}</b> exists already. Overwrite it?</p>"
+                    ).format(filename),
                     icon=EricMessageBox.Warning,
                 )
                 if not res:
                     return
-            filename = str(fpath)
+            ##filename = str(fpath)
 
         res = self.__writeJsonGraphicsFile(filename)
 
@@ -289,12 +317,20 @@
         @rtype bool
         """
         if not filename:
-            filename = EricFileDialog.getOpenFileName(
-                self,
-                self.tr("Load Diagram"),
-                "",
-                self.tr("Eric Graphics File (*.egj);;All Files (*)"),
-            )
+            if FileSystemUtilities.isRemoteFileName(self.__project.getProjectPath()):
+                filename = EricServerFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Load Diagram"),
+                    self.__project.getProjectPath(),
+                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
+                )
+            else:
+                filename = EricFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Load Diagram"),
+                    self.__project.getProjectPath(),
+                    self.tr("Eric Graphics File (*.egj);;All Files (*)"),
+                )
             if not filename:
                 # Canceled by user
                 return False
@@ -343,8 +379,11 @@
 
         try:
             jsonString = json.dumps(data, indent=2)
-            with open(filename, "w") as f:
-                f.write(jsonString)
+            if FileSystemUtilities.isRemoteFileName(filename):
+                self.__remotefsInterface.writeFile(filename, jsonString.encode("utf-8"))
+            else:
+                with open(filename, "w") as f:
+                    f.write(jsonString)
             return True
         except (OSError, TypeError) as err:
             EricMessageBox.critical(
@@ -368,8 +407,12 @@
         @rtype bool
         """
         try:
-            with open(filename, "r") as f:
-                jsonString = f.read()
+            if FileSystemUtilities.isRemoteFileName(filename):
+                bdata = self.__remotefsInterface.readFile(filename)
+                jsonString = bdata.decode("utf-8")
+            else:
+                with open(filename, "r") as f:
+                    jsonString = f.read()
             data = json.loads(jsonString)
         except (OSError, json.JSONDecodeError) as err:
             EricMessageBox.critical(
--- a/src/eric7/HexEdit/HexEditMainWindow.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/HexEdit/HexEditMainWindow.py	Fri Mar 08 15:30:53 2024 +0100
@@ -27,9 +27,11 @@
 from eric7.EricGui import EricPixmapCache
 from eric7.EricGui.EricAction import EricAction
 from eric7.EricWidgets import EricFileDialog, EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricClickableLabel import EricClickableLabel
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
 from eric7.Globals import recentNameHexFiles, strGroup
+from eric7.RemoteServerInterface import EricServerFileDialog
 from eric7.SystemUtilities import FileSystemUtilities
 
 from .HexEditGotoWidget import HexEditGotoWidget
@@ -82,6 +84,12 @@
         if not self.__fromEric:
             self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet"))
 
+        self.__remotefsInterface =(
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+            if self.__fromEric
+            else None
+        )
+
         self.__editor = HexEditWidget()
         self.__searchWidget = HexEditSearchReplaceWidget(self.__editor, self, False)
         self.__replaceWidget = HexEditSearchReplaceWidget(self.__editor, self, True)
@@ -1056,11 +1064,20 @@
         ):
             self.__lastOpenPath = self.__project.getProjectPath()
 
-        fileName = EricFileDialog.getOpenFileName(
-            self,
-            self.tr("Open binary file in new window"),
-            self.__lastOpenPath,
-            self.tr("All Files (*)"),
+        fileName = (
+            EricServerFileDialog.getOpenFileName(
+                self,
+                self.tr("Open binary file"),
+                self.__lastOpenPath,
+                self.tr("All Files (*)"),
+            )
+            if FileSystemUtilities.isRemoteFileName(self.__lastOpenPath)
+            else EricFileDialog.getOpenFileName(
+                self,
+                self.tr("Open binary file in new window"),
+                self.__lastOpenPath,
+                self.tr("All Files (*)"),
+            )
         )
         if fileName:
             he = HexEditMainWindow(
@@ -1097,17 +1114,12 @@
         @param fileName name of the binary file to load
         @type str
         """
-        if not os.path.exists(fileName):
-            EricMessageBox.warning(
-                self,
-                self.tr("eric Hex Editor"),
-                self.tr("The file '{0}' does not exist.").format(fileName),
-            )
-            return
-
         try:
-            with open(fileName, "rb") as f:
-                data = f.read()
+            if FileSystemUtilities.isRemoteFileName(fileName):
+                data = self.__remotefsInterface.readFile(fileName)
+            else:
+                with open(fileName, "rb") as f:
+                    data = f.read()
         except OSError as err:
             EricMessageBox.warning(
                 self,
@@ -1118,7 +1130,11 @@
             )
             return
 
-        self.__lastOpenPath = os.path.dirname(fileName)
+        self.__lastOpenPath = (
+            self.__remotefsInterface.dirname(fileName)
+            if FileSystemUtilities.isRemoteFileName(fileName)
+            else os.path.dirname(fileName)
+        )
         self.__editor.setData(data)
         self.__setCurrentFile(fileName)
 
@@ -1138,11 +1154,20 @@
             ):
                 self.__lastOpenPath = self.__project.getProjectPath()
 
-            fileName = EricFileDialog.getOpenFileName(
-                self,
-                self.tr("Open binary file"),
-                self.__lastOpenPath,
-                self.tr("All Files (*)"),
+            fileName = (
+                EricServerFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Open binary file"),
+                    self.__lastOpenPath,
+                    self.tr("All Files (*)"),
+                )
+                if FileSystemUtilities.isRemoteFileName(self.__lastOpenPath)
+                else EricFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Open binary file"),
+                    self.__lastOpenPath,
+                    self.tr("All Files (*)"),
+                )
             )
             if fileName:
                 self.__loadHexFile(fileName)
@@ -1190,17 +1215,35 @@
         if not self.__lastSavePath and self.__lastOpenPath:
             self.__lastSavePath = self.__lastOpenPath
 
-        fileName = EricFileDialog.getSaveFileName(
-            self,
-            self.tr("Save binary file"),
-            self.__lastSavePath,
-            self.tr("All Files (*)"),
-            options=EricFileDialog.DontConfirmOverwrite,
+        fileName = (
+            EricServerFileDialog.getSaveFileName(
+                self,
+                self.tr("Save binary file"),
+                self.__lastSavePath,
+                self.tr("All Files (*)"),
+            )
+            if FileSystemUtilities.isRemoteFileName(self.__lastSavePath)
+            else EricFileDialog.getSaveFileName(
+                self,
+                self.tr("Save binary file"),
+                self.__lastSavePath,
+                self.tr("All Files (*)"),
+                options=EricFileDialog.DontConfirmOverwrite,
+            )
         )
         if not fileName:
             return False
 
-        if pathlib.Path(fileName).exists():
+        if (
+            (
+                FileSystemUtilities.isRemoteFileName(fileName)
+                and self.__remotefsInterface.exists(fileName)
+            )
+            or (
+                FileSystemUtilities.isPlainFileName(fileName)
+                and pathlib.Path(fileName).exists()
+            )
+        ):
             res = EricMessageBox.yesNo(
                 self,
                 self.tr("Save binary file"),
@@ -1226,8 +1269,12 @@
         @rtype bool
         """
         try:
-            with open(fileName, "wb") as f:
-                f.write(self.__editor.data())
+            data = self.__editor.data()
+            if FileSystemUtilities.isRemoteFileName(fileName):
+                self.__remotefsInterface.writeFile(fileName, data)
+            else:
+                with open(fileName, "wb") as f:
+                    f.write(data)
         except OSError as err:
             EricMessageBox.warning(
                 self,
@@ -1261,29 +1308,50 @@
         if not savePath and self.__lastOpenPath:
             savePath = self.__lastOpenPath
 
-        fileName, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
-            self,
-            self.tr("Save to readable file"),
-            savePath,
-            self.tr("Text Files (*.txt);;All Files (*)"),
-            self.tr("Text Files (*.txt)"),
-            EricFileDialog.DontConfirmOverwrite,
-        )
-        if not fileName:
-            return
+        if FileSystemUtilities.isRemoteFileName(savePath):
+            fileName, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter(
+                self,
+                self.tr("Save to readable file"),
+                savePath,
+                self.tr("Text Files (*.txt);;All Files (*)"),
+                self.tr("Text Files (*.txt)"),
+            )
+            if not fileName:
+                return
 
-        fpath = pathlib.Path(fileName)
-        if not fpath.suffix:
-            ex = selectedFilter.split("(*")[1].split(")")[0]
-            if ex:
-                fpath = fpath.with_suffix(ex)
-        if fpath.exists():
+            ext = self.__remotefsInterface.splitext(fileName)[1]
+            if not ext:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fileName += ex
+            fileExists = self.__remotefsInterface.exists(fileName)
+        else:
+            fileName, selectedFilter = EricFileDialog.getSaveFileNameAndFilter(
+                self,
+                self.tr("Save to readable file"),
+                savePath,
+                self.tr("Text Files (*.txt);;All Files (*)"),
+                self.tr("Text Files (*.txt)"),
+                EricFileDialog.DontConfirmOverwrite,
+            )
+            if not fileName:
+                return
+
+            fpath = pathlib.Path(fileName)
+            if not fpath.suffix:
+                ex = selectedFilter.split("(*")[1].split(")")[0]
+                if ex:
+                    fpath = fpath.with_suffix(ex)
+            fileExists = fpath.exists()
+            fileName = str(fpath)
+
+        if fileExists:
             res = EricMessageBox.yesNo(
                 self,
                 self.tr("Save to readable file"),
                 self.tr(
                     "<p>The file <b>{0}</b> already exists. Overwrite it?</p>"
-                ).format(fpath),
+                ).format(fileName),
                 icon=EricMessageBox.Warning,
             )
             if not res:
@@ -1295,8 +1363,13 @@
             else self.__editor.toReadableString()
         )
         try:
-            with fpath.open("w", encoding="latin1") as f:
-                f.write(readableData)
+            if FileSystemUtilities.isRemoteFileName(fileName):
+                self.__remotefsInterface.writeFile(
+                    fileName, readableData.encode("latin1")
+                )
+            else:
+                with open(fileName, "w", encoding="latin1") as f:
+                    f.write(readableData)
         except OSError as err:
             EricMessageBox.warning(
                 self,
@@ -1535,6 +1608,10 @@
 
         self.__recentMenu.clear()
 
+        connected = (
+            self.__fromEric and ericApp().getObject("EricServer").isServerConnected()
+        )
+
         for idx, rs in enumerate(self.__recent, start=1):
             formatStr = "&{0:d}. {1}" if idx < 10 else "{0:d}. {1}"
             act = self.__recentMenu.addAction(
@@ -1546,7 +1623,10 @@
                 )
             )
             act.setData(rs)
-            act.setEnabled(pathlib.Path(rs).exists())
+            act.setEnabled(
+                (FileSystemUtilities.isRemoteFileName(rs) and connected)
+                or pathlib.Path(rs).exists()
+            )
 
         self.__recentMenu.addSeparator()
         self.__recentMenu.addAction(self.tr("&Clear"), self.__clearRecent)
@@ -1581,7 +1661,7 @@
         rs = Preferences.Prefs.rsettings.value(recentNameHexFiles)
         if rs is not None:
             for f in Preferences.toList(rs):
-                if pathlib.Path(f).exists():
+                if FileSystemUtilities.isRemoteFileName(f) or pathlib.Path(f).exists():
                     self.__recent.append(f)
 
     def __saveRecent(self):
--- a/src/eric7/IconEditor/IconEditorWindow.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/IconEditor/IconEditorWindow.py	Fri Mar 08 15:30:53 2024 +0100
@@ -11,7 +11,7 @@
 import os
 import pathlib
 
-from PyQt6.QtCore import QEvent, QSignalMapper, QSize, Qt, pyqtSignal
+from PyQt6.QtCore import QBuffer, QByteArray, QEvent, QSignalMapper, QSize, Qt, pyqtSignal
 from PyQt6.QtGui import QImage, QImageReader, QImageWriter, QKeySequence, QPalette
 from PyQt6.QtWidgets import QDockWidget, QLabel, QScrollArea, QWhatsThis
 
@@ -19,8 +19,10 @@
 from eric7.EricGui import EricPixmapCache
 from eric7.EricGui.EricAction import EricAction, createActionGroup
 from eric7.EricWidgets import EricFileDialog, EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
 from eric7.EricWidgets.EricZoomWidget import EricZoomWidget
+from eric7.SystemUtilities import FileSystemUtilities
 
 from .IconEditorGrid import IconEditorGrid, IconEditorTool
 
@@ -1282,7 +1284,28 @@
         @param fileName name of the icon file to load
         @type str
         """
-        img = QImage(fileName)
+        img = QImage()
+        if self.fromEric and FileSystemUtilities.isRemoteFileName(fileName):
+            try:
+                data = (
+                    ericApp()
+                    .getObject("EricServer")
+                    .getServiceInterface("FileSystem")
+                    .readFile(fileName)
+                )
+                img.loadFromData(data)
+            except OSError as err:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("eric Icon Editor"),
+                    self.tr(
+                        """<p>The file <b>{0}</b> cannot be loaded.</p>"""
+                        """<p>Reason: {1}</p>"""
+                    ).format(fileName, str(err)),
+                )
+                return
+        else:
+            img.load(fileName)
         if img.isNull():
             EricMessageBox.warning(
                 self,
@@ -1303,7 +1326,30 @@
         @rtype bool
         """
         img = self.__editor.iconImage()
-        res = img.save(fileName)
+        if self.fromEric and FileSystemUtilities.isRemoteFileName(fileName):
+            fsInterface = (
+                ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+            )
+            imageFormat = fsInterface.splitext(fileName)[1][1:].upper()
+            data = QByteArray()
+            buffer = QBuffer(data)
+            res = img.save(buffer, imageFormat)
+            if res:
+                try:
+                    fsInterface.writeFile(fileName, data)
+                except OSError as err:
+                    EricMessageBox.warning(
+                        self,
+                        self.tr("eric Icon Editor"),
+                        self.tr(
+                            """<p>The file <b>{0}</b> cannot be written.</p>"""
+                            """<p>Reason: {1}</p>"""
+                        ).format(fileName, str(err)),
+                    )
+                    self.__checkActions()
+                    return False
+        else:
+            res = img.save(fileName)
 
         if not res:
             EricMessageBox.warning(
@@ -1311,9 +1357,7 @@
                 self.tr("eric Icon Editor"),
                 self.tr("Cannot write file '{0}'.").format(fileName),
             )
-
             self.__checkActions()
-
             return False
 
         self.__editor.setDirty(False, setCleanState=True)
--- a/src/eric7/PdfViewer/PdfViewerWindow.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/PdfViewer/PdfViewerWindow.py	Fri Mar 08 15:30:53 2024 +0100
@@ -11,7 +11,7 @@
 import os
 import pathlib
 
-from PyQt6.QtCore import QPointF, QSize, Qt, pyqtSignal, pyqtSlot
+from PyQt6.QtCore import QBuffer, QByteArray, QIODevice, QPointF, QSize, Qt, pyqtSignal, pyqtSlot
 from PyQt6.QtGui import QAction, QActionGroup, QClipboard, QGuiApplication, QKeySequence
 from PyQt6.QtPdf import QPdfDocument, QPdfLink
 from PyQt6.QtPdfWidgets import QPdfView
@@ -30,9 +30,11 @@
 from eric7.EricGui import EricPixmapCache
 from eric7.EricGui.EricAction import EricAction
 from eric7.EricWidgets import EricFileDialog, EricMessageBox
+from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricMainWindow import EricMainWindow
 from eric7.EricWidgets.EricStretchableSpacer import EricStretchableSpacer
 from eric7.Globals import recentNamePdfFiles
+from eric7.RemoteServerInterface import EricServerFileDialog
 from eric7.SystemUtilities import FileSystemUtilities, OSUtilities
 
 from .PdfInfoWidget import PdfInfoWidget
@@ -79,6 +81,12 @@
         if not self.__fromEric:
             self.setStyle(Preferences.getUI("Style"), Preferences.getUI("StyleSheet"))
 
+        self.__remotefsInterface =(
+            ericApp().getObject("EricServer").getServiceInterface("FileSystem")
+            if self.__fromEric
+            else None
+        )
+
         self.__pdfDocument = QPdfDocument(self)
 
         self.__cw = QSplitter(Qt.Orientation.Horizontal, self)
@@ -961,8 +969,31 @@
         """
         canceled = False
         err = QPdfDocument.Error.IncorrectPassword
+
+        if FileSystemUtilities.isRemoteFileName(fileName):
+            try:
+                data = QByteArray(self.__remotefsInterface.readFile(fileName))
+                buffer = QBuffer(data)
+            except OSError as err:
+                EricMessageBox.warning(
+                    self,
+                    self.tr("OpenPDF File"),
+                    self.tr(
+                        "<p>The PDF file <b>{0}</b> could not be read.</p>"
+                        "<p>Reason: {1}</p>"
+                    ).format(fileName, str(err)),
+                )
+                return
+        else:
+            buffer = None
+            
         while not canceled and err == QPdfDocument.Error.IncorrectPassword:
-            err = self.__pdfDocument.load(fileName)
+            if FileSystemUtilities.isRemoteFileName(fileName):
+                buffer.open(QIODevice.OpenModeFlag.ReadOnly)
+                self.__pdfDocument.load(buffer)
+                err = QPdfDocument.Error.None_
+            else:
+                err = self.__pdfDocument.load(fileName)
             if err == QPdfDocument.Error.IncorrectPassword:
                 password, ok = QInputDialog.getText(
                     self,
@@ -986,7 +1017,11 @@
             self.__documentInfoWidget.setFileName("")
             return
 
-        self.__lastOpenPath = os.path.dirname(fileName)
+        self.__lastOpenPath = (
+            self.__remotefsInterface.dirname(fileName)
+            if FileSystemUtilities.isRemoteFileName(fileName)
+            else os.path.dirname(fileName)
+        )
         self.__setCurrentFile(fileName)
 
         documentTitle = self.__pdfDocument.metaData(QPdfDocument.MetaDataField.Title)
@@ -1019,11 +1054,20 @@
         ):
             self.__lastOpenPath = self.__project.getProjectPath()
 
-        fileName = EricFileDialog.getOpenFileName(
-            self,
-            self.tr("Open PDF File"),
-            self.__lastOpenPath,
-            self.tr("PDF Files (*.pdf);;All Files (*)"),
+        fileName = (
+            EricServerFileDialog.getOpenFileName(
+                self,
+                self.tr("Open PDF File"),
+                self.__lastOpenPath,
+                self.tr("PDF Files (*.pdf);;All Files (*)"),
+            )
+            if FileSystemUtilities.isRemoteFileName(self.__lastOpenPath)
+            else EricFileDialog.getOpenFileName(
+                self,
+                self.tr("Open PDF File"),
+                self.__lastOpenPath,
+                self.tr("PDF Files (*.pdf);;All Files (*)"),
+            )
         )
         if fileName:
             self.__loadPdfFile(fileName)
@@ -1044,11 +1088,20 @@
             self.__lastOpenPath = self.__project.getProjectPath()
 
         if not fileName:
-            fileName = EricFileDialog.getOpenFileName(
-                self,
-                self.tr("Open PDF File"),
-                self.__lastOpenPath,
-                self.tr("PDF Files (*.pdf);;All Files (*)"),
+            fileName = (
+                EricServerFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Open PDF File"),
+                    self.__lastOpenPath,
+                    self.tr("PDF Files (*.pdf);;All Files (*)"),
+                )
+                if FileSystemUtilities.isRemoteFileName(self.__lastOpenPath)
+                else EricFileDialog.getOpenFileName(
+                    self,
+                    self.tr("Open PDF File"),
+                    self.__lastOpenPath,
+                    self.tr("PDF Files (*.pdf);;All Files (*)"),
+                )
             )
         if fileName:
             viewer = PdfViewerWindow(
--- a/src/eric7/Project/DebuggerPropertiesDialog.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/DebuggerPropertiesDialog.py	Fri Mar 08 15:30:53 2024 +0100
@@ -28,16 +28,18 @@
     settings.
     """
 
-    def __init__(self, project, parent=None, name=None):
+    def __init__(self, project, isRemote=False, parent=None, name=None):
         """
         Constructor
 
         @param project reference to the project object
         @type Project
-        @param parent parent widget of this dialog
-        @type QWidget
-        @param name name of this dialog
-        @type str
+        @param isRemote flag indicating a remote project (defaults to False)
+        @type bool (optional)
+        @param parent parent widget of this dialog (defaults to None)
+        @type QWidget (optional)
+        @param name name of this dialog (defaults to None)
+        @type str (optional)
         """
         super().__init__(parent)
         if name:
@@ -45,6 +47,7 @@
         self.setupUi(self)
 
         self.project = project
+        self.__isRemote = isRemote
 
         debugClientsHistory = Preferences.getProject("DebugClientsHistory")
         self.debugClientPicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
@@ -61,8 +64,11 @@
 
         venvManager = ericApp().getObject("VirtualEnvManager")
 
+        # Virtual Environment
         self.venvGroupBox.setVisible(
             not self.project.getProjectData(dataKey="EMBEDDED_VENV")
+            and not isRemote
+            # TODO: change once remote environments have been implemented
         )
         self.venvComboBox.addItem("")
         if self.project.getProjectData(dataKey="EMBEDDED_VENV"):
@@ -89,7 +95,12 @@
                 else:
                     venvIndex = 0
         self.venvComboBox.setCurrentIndex(venvIndex)
-        if self.project.debugProperties["DEBUGCLIENT"]:
+
+        # Debug Client
+        self.debugClientGroup.setVisible(not isRemote)
+        if isRemote:
+            self.debugClientPicker.clear()
+        elif self.project.debugProperties["DEBUGCLIENT"]:
             self.debugClientPicker.setText(
                 self.project.debugProperties["DEBUGCLIENT"], toNative=False
             )
@@ -101,12 +112,17 @@
             else:
                 debugClient = ""
             self.debugClientPicker.setText(debugClient, toNative=False)
+
+        # Debug Environment
         self.debugEnvironmentOverrideCheckBox.setChecked(
             self.project.debugProperties["ENVIRONMENTOVERRIDE"]
         )
         self.debugEnvironmentEdit.setText(
             self.project.debugProperties["ENVIRONMENTSTRING"]
         )
+
+        # Remote (ssh) Debugger
+        self.remoteDebuggerGroup.setVisible(not isRemote)
         self.remoteDebuggerGroup.setChecked(
             self.project.debugProperties["REMOTEDEBUGGER"]
         )
@@ -120,11 +136,20 @@
         )
         self.translationRemoteEdit.setText(self.project.debugProperties["REMOTEPATH"])
         self.translationLocalEdit.setText(self.project.debugProperties["LOCALPATH"])
+
+        # Console Debugger
+        self.consoleDebuggerGroup.setVisible(not isRemote)
         self.consoleDebuggerGroup.setChecked(
             self.project.debugProperties["CONSOLEDEBUGGER"]
         )
         self.consoleCommandEdit.setText(self.project.debugProperties["CONSOLECOMMAND"])
-        self.redirectCheckBox.setChecked(self.project.debugProperties["REDIRECT"])
+
+        # Redirect stdin/stdout/stderr
+        self.redirectCheckBox.setChecked(
+            self.project.debugProperties["REDIRECT"] or isRemote
+        )
+
+        # No encoding
         self.noEncodingCheckBox.setChecked(self.project.debugProperties["NOENCODING"])
 
         msh = self.minimumSizeHint()
@@ -148,17 +173,20 @@
         """
         self.project.debugProperties["VIRTUALENV"] = self.venvComboBox.currentText()
 
-        self.project.debugProperties["DEBUGCLIENT"] = self.debugClientPicker.text(
-            toNative=False
-        )
-        if not self.project.debugProperties["DEBUGCLIENT"]:
-            if self.project.getProjectData(dataKey="PROGLANGUAGE") == "Python3":
-                debugClient = os.path.join(
-                    getConfig("ericDir"), "DebugClients", "Python", "DebugClient.py"
-                )
-            else:
-                debugClient = ""
-            self.project.debugProperties["DEBUGCLIENT"] = debugClient
+        if self.__isRemote:
+            self.project.debugProperties["DEBUGCLIENT"] = ""
+        else:
+            self.project.debugProperties["DEBUGCLIENT"] = self.debugClientPicker.text(
+                toNative=False
+            )
+            if not self.project.debugProperties["DEBUGCLIENT"]:
+                if self.project.getProjectData(dataKey="PROGLANGUAGE") == "Python3":
+                    debugClient = os.path.join(
+                        getConfig("ericDir"), "DebugClients", "Python", "DebugClient.py"
+                    )
+                else:
+                    debugClient = ""
+                self.project.debugProperties["DEBUGCLIENT"] = debugClient
 
         self.project.debugProperties[
             "ENVIRONMENTOVERRIDE"
@@ -166,25 +194,43 @@
         self.project.debugProperties[
             "ENVIRONMENTSTRING"
         ] = self.debugEnvironmentEdit.text()
-        self.project.debugProperties[
-            "REMOTEDEBUGGER"
-        ] = self.remoteDebuggerGroup.isChecked()
-        self.project.debugProperties["REMOTEHOST"] = self.remoteHostEdit.text()
-        self.project.debugProperties["REMOTECOMMAND"] = self.remoteCommandEdit.text()
-        self.project.debugProperties[
-            "REMOTEDEBUGCLIENT"
-        ] = self.remoteDebugClientEdit.text()
-        self.project.debugProperties[
-            "PATHTRANSLATION"
-        ] = self.pathTranslationGroup.isChecked()
-        self.project.debugProperties["REMOTEPATH"] = self.translationRemoteEdit.text()
-        self.project.debugProperties["LOCALPATH"] = self.translationLocalEdit.text()
-        self.project.debugProperties[
-            "CONSOLEDEBUGGER"
-        ] = self.consoleDebuggerGroup.isChecked()
-        self.project.debugProperties["CONSOLECOMMAND"] = self.consoleCommandEdit.text()
-        self.project.debugProperties["REDIRECT"] = self.redirectCheckBox.isChecked()
+
+        if self.__isRemote:
+            self.project.debugProperties["REMOTEDEBUGGER"] = False
+        else:
+            self.project.debugProperties[
+                "REMOTEDEBUGGER"
+            ] = self.remoteDebuggerGroup.isChecked()
+            self.project.debugProperties["REMOTEHOST"] = self.remoteHostEdit.text()
+            self.project.debugProperties[
+                "REMOTECOMMAND"
+            ] = self.remoteCommandEdit.text()
+            self.project.debugProperties[
+                "REMOTEDEBUGCLIENT"
+            ] = self.remoteDebugClientEdit.text()
+            self.project.debugProperties[
+                "PATHTRANSLATION"
+            ] = self.pathTranslationGroup.isChecked()
+            self.project.debugProperties[
+                "REMOTEPATH"
+            ] = self.translationRemoteEdit.text()
+            self.project.debugProperties["LOCALPATH"] = self.translationLocalEdit.text()
+
+        if self.__isRemote:
+            self.project.debugProperties["CONSOLEDEBUGGER"] = False
+        else:
+            self.project.debugProperties[
+                "CONSOLEDEBUGGER"
+            ] = self.consoleDebuggerGroup.isChecked()
+            self.project.debugProperties[
+                "CONSOLECOMMAND"
+            ] = self.consoleCommandEdit.text()
+
+        self.project.debugProperties["REDIRECT"] = (
+            self.redirectCheckBox.isChecked() or self.__isRemote
+        )
         self.project.debugProperties["NOENCODING"] = self.noEncodingCheckBox.isChecked()
+
         self.project.debugPropertiesLoaded = True
         self.project.debugPropertiesChanged = True
 
--- a/src/eric7/Project/DebuggerPropertiesDialog.ui	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/DebuggerPropertiesDialog.ui	Fri Mar 08 15:30:53 2024 +0100
@@ -18,7 +18,7 @@
   </property>
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
-    <widget class="QGroupBox" name="groupBox">
+    <widget class="QGroupBox" name="debugClientGroup">
      <property name="title">
       <string>Debug Client</string>
      </property>
--- a/src/eric7/Project/Project.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/Project.py	Fri Mar 08 15:30:53 2024 +0100
@@ -1145,6 +1145,8 @@
                 self.name = self.__remotefsInterface.splitext(
                     self.__remotefsInterface.basename(fn)
                 )[0]
+                # TODO: read in a file system cache of ppath
+                self.__remotefsInterface.populateFsCache(self.ppath)
             else:
                 self.pfile = os.path.abspath(fn)
                 self.ppath = os.path.abspath(os.path.dirname(fn))
@@ -1185,7 +1187,7 @@
                 for fn in self.__pdata[fileCategory]:
                     dn = (
                         self.__remotefsInterface.dirname(fn)
-                        if FileSystemUtilities.isRemoteFileName(fn)
+                        if FileSystemUtilities.isRemoteFileName(self.ppath)
                         else os.path.dirname(fn)
                     )
                     if dn and dn not in self.subdirs:
@@ -1651,7 +1653,9 @@
         """
         from .DebuggerPropertiesDialog import DebuggerPropertiesDialog
 
-        dlg = DebuggerPropertiesDialog(self)
+        dlg = DebuggerPropertiesDialog(
+            self, isRemote=FileSystemUtilities.isRemoteFileName(self.ppath)
+        )
         if dlg.exec() == QDialog.DialogCode.Accepted:
             dlg.storeData()
 
@@ -1664,7 +1668,10 @@
         @return value of the property
         @rtype Any
         """
-        if key == "INTERPRETER":
+        if (
+            key == "INTERPRETER"
+            and not FileSystemUtilities.isRemoteFileName(self.ppath)
+        ):
             return (
                 ericApp()
                 .getObject("VirtualEnvManager")
@@ -2368,16 +2375,26 @@
         isSourceFile = fn in self.__pdata["SOURCES"]
 
         if newfn is None:
-            newfn = EricFileDialog.getSaveFileName(
-                None,
-                self.tr("Rename file"),
-                oldfn,
-                "",
-                EricFileDialog.DontConfirmOverwrite,
-            )
+            if isRemote:
+                newfn = EricServerFileDialog.getSaveFileName(
+                    None,
+                    self.tr("Rename File"),
+                    oldfn,
+                    "",
+                )
+            else:
+                newfn = EricFileDialog.getSaveFileName(
+                    None,
+                    self.tr("Rename File"),
+                    oldfn,
+                    "",
+                    options=EricFileDialog.DontConfirmOverwrite,
+                )
+                if newfn:
+                    newfn = FileSystemUtilities.toNativeSeparators(newfn)
+
             if not newfn:
                 return False
-            newfn = FileSystemUtilities.toNativeSeparators(newfn)
 
         if (not isRemote and os.path.exists(newfn)) or (
             isRemote and self.__remotefsInterface.exists(newfn)
@@ -2396,7 +2413,7 @@
 
         try:
             if isRemote:
-                self.__remotefsInterface.rename(oldfn, newfn)
+                self.__remotefsInterface.replace(oldfn, newfn)
             else:
                 os.rename(oldfn, newfn)
         except OSError as msg:
@@ -4011,6 +4028,10 @@
         self.ui.taskViewer.clearProjectTasks()
         self.ui.taskViewer.setProjectOpen(False)
 
+        # TODO: clear the file system cache for ppath if remote project
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            self.__remotefsInterface.removeFromFsCache(self.ppath)
+
         # now shutdown the vcs interface
         if not FileSystemUtilities.isRemoteFileName(self.ppath) and self.vcs:
             self.vcs.vcsShutdown()
@@ -6439,6 +6460,9 @@
         forProject = True
         override = False
 
+        if FileSystemUtilities.isRemoteFileName(self.ppath):
+            return None
+
         if vcsSystem is None:
             if self.__pdata["VCS"] and self.__pdata["VCS"] != "None":
                 vcsSystem = self.__pdata["VCS"]
@@ -7902,6 +7926,8 @@
             and self.opened
             and FileSystemUtilities.isRemoteFileName(self.pfile)
         )
+        if not connected and FileSystemUtilities.isRemoteFileName(self.ppath):
+            self.closeProject(noSave=True)
 
     @pyqtSlot()
     def __openRemoteProject(self):
@@ -7930,7 +7956,6 @@
             defaultPath,
             self.tr("Project Files (*.epj)"),
             defaultFilter,
-            EricFileDialog.DontConfirmOverwrite,
         )
 
         if fn:
--- a/src/eric7/Project/ProjectBaseBrowser.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/ProjectBaseBrowser.py	Fri Mar 08 15:30:53 2024 +0100
@@ -269,7 +269,10 @@
         if self.backMenu is not None:
             self.backMenu.setEnabled(True)
 
-        if self.project.vcs is not None:
+        if (
+            self.project.vcs is not None
+            and not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        ):
             self.vcsHelper = self.project.vcs.vcsGetProjectBrowserHelper(
                 self, self.project, self.isTranslationsBrowser
             )
@@ -280,6 +283,8 @@
                 self.dirMenu,
                 self.dirMultiMenu,
             )
+        else:
+            self.vcsHelper = None
 
     def _newProject(self):
         """
@@ -502,7 +507,7 @@
         if self.project.vcs is None:
             for act in self.menuActions:
                 act.setEnabled(True)
-        else:
+        elif self.vcsHelper is not None:
             self.vcsHelper.showContextMenu(menu, self.menuActions)
 
     def _showContextMenuMulti(self, menu):
@@ -519,7 +524,7 @@
         if self.project.vcs is None:
             for act in self.multiMenuActions:
                 act.setEnabled(True)
-        else:
+        elif self.vcsHelper is not None:
             self.vcsHelper.showContextMenuMulti(menu, self.multiMenuActions)
 
     def _showContextMenuDir(self, menu):
@@ -535,7 +540,7 @@
         if self.project.vcs is None:
             for act in self.dirMenuActions:
                 act.setEnabled(True)
-        else:
+        elif self.vcsHelper is not None:
             self.vcsHelper.showContextMenuDir(menu, self.dirMenuActions)
 
     def _showContextMenuDirMulti(self, menu):
@@ -551,7 +556,7 @@
         if self.project.vcs is None:
             for act in self.dirMultiMenuActions:
                 act.setEnabled(True)
-        else:
+        elif self.vcsHelper is not None:
             self.vcsHelper.showContextMenuDirMulti(menu, self.dirMultiMenuActions)
 
     def _showContextMenuBack(self, menu):  # noqa: U100
--- a/src/eric7/Project/ProjectBrowserModel.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/ProjectBrowserModel.py	Fri Mar 08 15:30:53 2024 +0100
@@ -342,63 +342,106 @@
         """
         self._addWatchedItem(parentItem)
 
-        qdir = QDir(parentItem.dirName())
+        dirName = parentItem.dirName()
+        if FileSystemUtilities.isPlainFileName(dirName):
+            qdir = QDir(parentItem.dirName())
 
-        fileFilter = (
-            (QDir.Filter.AllEntries | QDir.Filter.Hidden | QDir.Filter.NoDotAndDotDot)
-            if Preferences.getProject("BrowsersListHiddenFiles")
-            else QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot
-        )
-        entryInfoList = qdir.entryInfoList(fileFilter)
+            fileFilter = (
+                (
+                    QDir.Filter.AllEntries
+                    | QDir.Filter.Hidden
+                    | QDir.Filter.NoDotAndDotDot
+                )
+                if Preferences.getProject("BrowsersListHiddenFiles")
+                else QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot
+            )
+            entryInfoList = qdir.entryInfoList(fileFilter)
 
-        if len(entryInfoList) > 0:
-            if repopulate:
-                self.beginInsertRows(
-                    self.createIndex(parentItem.row(), 0, parentItem),
-                    0,
-                    len(entryInfoList) - 1,
-                )
-            states = {}
-            if self.project.vcs is not None:
-                for f in entryInfoList:
-                    fname = f.absoluteFilePath()
-                    states[os.path.normcase(fname)] = 0
-                dname = parentItem.dirName()
-                self.project.vcs.clearStatusCache()
-                states = self.project.vcs.vcsAllRegisteredStates(states, dname)
+            if len(entryInfoList) > 0:
+                if repopulate:
+                    self.beginInsertRows(
+                        self.createIndex(parentItem.row(), 0, parentItem),
+                        0,
+                        len(entryInfoList) - 1,
+                    )
+                states = {}
+                if self.project.vcs is not None:
+                    for f in entryInfoList:
+                        fname = f.absoluteFilePath()
+                        states[os.path.normcase(fname)] = 0
+                    dname = parentItem.dirName()
+                    self.project.vcs.clearStatusCache()
+                    states = self.project.vcs.vcsAllRegisteredStates(states, dname)
 
-            for f in entryInfoList:
-                node = (
-                    ProjectBrowserDirectoryItem(
-                        parentItem,
-                        FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()),
-                        parentItem.getProjectTypes()[0],
-                        False,
-                        fsInterface=self.__remotefsInterface,
-                    )
-                    if f.isDir()
-                    else ProjectBrowserFileItem(
-                        parentItem,
-                        FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()),
-                        parentItem.getProjectTypes()[0],
-                        fsInterface=self.__remotefsInterface,
+                for f in entryInfoList:
+                    node = (
+                        ProjectBrowserDirectoryItem(
+                            parentItem,
+                            FileSystemUtilities.toNativeSeparators(
+                                f.absoluteFilePath()
+                            ),
+                            parentItem.getProjectTypes()[0],
+                            False,
+                            fsInterface=self.__remotefsInterface,
+                        )
+                        if f.isDir()
+                        else ProjectBrowserFileItem(
+                            parentItem,
+                            FileSystemUtilities.toNativeSeparators(
+                                f.absoluteFilePath()
+                            ),
+                            parentItem.getProjectTypes()[0],
+                            fsInterface=self.__remotefsInterface,
+                        )
                     )
-                )
-                if self.project.vcs is not None:
-                    fname = f.absoluteFilePath()
-                    if (
-                        states[os.path.normcase(fname)]
-                        == VersionControlState.Controlled
-                    ):
-                        node.addVcsStatus(self.project.vcs.vcsName())
-                        self.project.clearStatusMonitorCachedState(f.absoluteFilePath())
+                    if self.project.vcs is not None:
+                        fname = f.absoluteFilePath()
+                        if (
+                            states[os.path.normcase(fname)]
+                            == VersionControlState.Controlled
+                        ):
+                            node.addVcsStatus(self.project.vcs.vcsName())
+                            self.project.clearStatusMonitorCachedState(
+                                f.absoluteFilePath()
+                            )
+                        else:
+                            node.addVcsStatus(self.tr("local"))
                     else:
-                        node.addVcsStatus(self.tr("local"))
-                else:
+                        node.addVcsStatus("")
+                    self._addItem(node, parentItem)
+                if repopulate:
+                    self.endInsertRows()
+
+        elif FileSystemUtilities.isRemoteFileName(dirName):
+            entriesList = self.__remotefsInterface.listdir(dirName)[2]
+            if len(entriesList) > 0:
+                if repopulate:
+                    self.beginInsertRows(
+                        self.createIndex(parentItem.row(), 0, parentItem),
+                        0,
+                        len(entryInfoList) - 1,
+                    )
+                for entry in entriesList:
+                    node = (
+                        ProjectBrowserDirectoryItem(
+                            parentItem,
+                            entry["path"],
+                            parentItem.getProjectTypes()[0],
+                            False,
+                            fsInterface=self.__remotefsInterface,
+                        )
+                        if entry["is_dir"]
+                        else ProjectBrowserFileItem(
+                            parentItem,
+                            entry["path"],
+                            parentItem.getProjectTypes()[0],
+                            fsInterface=self.__remotefsInterface,
+                        )
+                    )
                     node.addVcsStatus("")
-                self._addItem(node, parentItem)
-            if repopulate:
-                self.endInsertRows()
+                    self._addItem(node, parentItem)
+                if repopulate:
+                    self.endInsertRows()
 
     def projectClosed(self):
         """
@@ -427,7 +470,9 @@
         states = {}
         fileCategories = self.project.getFileCategories()
 
-        if self.project.vcs is not None:
+        if self.project.vcs is not None and not FileSystemUtilities.isRemoteFileName(
+            self.project.ppath
+        ):
             for fileCategory in fileCategories:
                 for fn in self.project.getProjectData(dataKey=fileCategory):
                     states[os.path.normcase(os.path.join(self.project.ppath, fn))] = 0
@@ -479,7 +524,10 @@
                     )
                 )
                 self._addItem(itm, parentItem)
-                if self.project.vcs is not None:
+                if (
+                    self.project.vcs is not None
+                    and not FileSystemUtilities.isRemoteFileName(self.project.ppath)
+                ):
                     if (
                         states[os.path.normcase(fname)]
                         == VersionControlState.Controlled
--- a/src/eric7/Project/ProjectOthersBrowser.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/ProjectOthersBrowser.py	Fri Mar 08 15:30:53 2024 +0100
@@ -17,6 +17,7 @@
 from eric7 import Preferences
 from eric7.EricGui import EricPixmapCache
 from eric7.EricWidgets import EricMessageBox
+from eric7.SystemUtilities import FileSystemUtilities
 from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog
 from eric7.Utilities import MimeTypes
 
@@ -159,7 +160,9 @@
         self.menu.addSeparator()
         self.menu.addAction(self.tr("Refresh"), self.__refreshItem)
         self.menu.addSeparator()
-        self.menu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.menuFileManagerAct = self.menu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.menu.addAction(self.tr("Copy Path to Clipboard"), self._copyToClipboard)
         self.menu.addSeparator()
         self.menu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
@@ -180,7 +183,9 @@
         self.dirMenu.addAction(self.tr("Add files..."), self.__addOthersFiles)
         self.dirMenu.addAction(self.tr("Add directory..."), self.__addOthersDirectory)
         self.dirMenu.addSeparator()
-        self.dirMenu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.dirMenuFileManagerAct = self.dirMenu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.dirMenu.addAction(self.tr("Copy Path to Clipboard"), self._copyToClipboard)
         self.dirMenu.addSeparator()
         self.dirMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
@@ -198,7 +203,7 @@
             self.tr("Add directory..."), lambda: self.project.addDirectory("OTHERS")
         )
         self.backMenu.addSeparator()
-        self.backMenu.addAction(
+        self.backMenuFileManagerAct = self.backMenu.addAction(
             self.tr("Show in File Manager"), self._showProjectInFileManager
         )
         self.backMenu.addSeparator()
@@ -241,6 +246,8 @@
         if not self.project.isOpen():
             return
 
+        isRemote = FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+
         with contextlib.suppress(Exception):  # secok
             cnt = self.getSelectedItemsCount(
                 [
@@ -272,18 +279,24 @@
                         self.openInEditorAct.setVisible(itm.isSvgFile())
                         self.openInPdfViewerAct.setVisible(itm.isPdfFile())
                         self.mimeTypeAct.setVisible(True)
+                        self.menuFileManagerAct.setVisible(not isRemote)
                         self.menu.popup(self.mapToGlobal(coord))
-                    elif isinstance(itm, ProjectBrowserDirectoryItem):
+                    elif isinstance(
+                        itm,
+                        (
+                            ProjectBrowserDirectoryItem,
+                            ProjectBrowserSimpleDirectoryItem,
+                        ),
+                    ):
                         self.removeDirAct.setVisible(True)
                         self.deleteDirAct.setVisible(True)
-                        self.dirMenu.popup(self.mapToGlobal(coord))
-                    elif isinstance(itm, ProjectBrowserSimpleDirectoryItem):
-                        self.removeDirAct.setVisible(False)
-                        self.deleteDirAct.setVisible(False)
+                        self.dirMenuFileManagerAct.setVisible(not isRemote)
                         self.dirMenu.popup(self.mapToGlobal(coord))
                     else:
+                        self.backMenuFileManagerAct.setVisible(not isRemote)
                         self.backMenu.popup(self.mapToGlobal(coord))
                 else:
+                    self.backMenuFileManagerAct.setVisible(not isRemote)
                     self.backMenu.popup(self.mapToGlobal(coord))
 
     def __showContextMenu(self):
@@ -336,7 +349,7 @@
                 itm, (ProjectBrowserSimpleDirectoryItem, ProjectBrowserDirectoryItem)
             ):
                 self.renameFileAct.setEnabled(False)
-        else:
+        elif self.vcsHelper is not None:
             self.vcsHelper.showContextMenu(menu, self.menuActions)
 
     def _editPixmap(self):
--- a/src/eric7/Project/ProjectSourcesBrowser.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/Project/ProjectSourcesBrowser.py	Fri Mar 08 15:30:53 2024 +0100
@@ -23,6 +23,7 @@
 from eric7.EricWidgets.EricApplication import ericApp
 from eric7.EricWidgets.EricPathPickerDialog import EricPathPickerModes
 from eric7.Graphics.UMLDialog import UMLDialog, UMLDialogType
+from eric7.SystemUtilities import FileSystemUtilities
 from eric7.UI.BrowserModel import (
     BrowserClassAttributeItem,
     BrowserClassItem,
@@ -317,12 +318,14 @@
         self.sourceMenu.addSeparator()
         self.sourceMenu.addMenu(self.graphicsMenu)
         self.sourceMenu.addMenu(self.checksMenu)
-        self.sourceMenu.addMenu(self.formattingMenu)
+        self.sourceMenuActions["Formatting"] = self.sourceMenu.addMenu(
+            self.formattingMenu
+        )
         self.sourceMenuActions["Show"] = self.sourceMenu.addMenu(self.menuShow)
         self.sourceMenu.addSeparator()
         self.__startAct = self.sourceMenu.addMenu(self.__startMenu)
         self.sourceMenu.addSeparator()
-        self.sourceMenu.addAction(
+        self.__sourceMenuFileManagerAct = self.sourceMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.sourceMenu.addAction(
@@ -348,7 +351,9 @@
             self.tr("Add source directory..."), self.__addSourceDirectory
         )
         self.menu.addSeparator()
-        self.menu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__menuFileManagerAct = self.menu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.menu.addSeparator()
         self.menu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
         self.menu.addAction(self.tr("Collapse all directories"), self._collapseAllDirs)
@@ -378,7 +383,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.attributeMenu.addSeparator()
-        self.attributeMenu.addAction(
+        self.__attributeMenuFileManagerAct = self.attributeMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.attributeMenu.addSeparator()
@@ -407,7 +412,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.backMenu.addSeparator()
-        self.backMenu.addAction(
+        self.__backMenuFileManagerAct = self.backMenu.addAction(
             self.tr("Show in File Manager"), self._showProjectInFileManager
         )
         self.backMenu.addSeparator()
@@ -427,7 +432,7 @@
         self.multiMenuActions.append(act)
         self.multiMenu.addSeparator()
         self.multiMenu.addMenu(self.checksMenu)
-        self.multiMenu.addMenu(self.formattingMenu)
+        self.__multiMenuFormattingAct = self.multiMenu.addMenu(self.formattingMenu)
         self.multiMenu.addSeparator()
         self.multiMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
         self.multiMenu.addAction(
@@ -453,9 +458,11 @@
         self.dirMenu.addSeparator()
         act = self.dirMenu.addMenu(self.graphicsMenu)
         self.dirMenu.addMenu(self.checksMenu)
-        self.dirMenu.addMenu(self.formattingMenu)
+        self.__dirMenuFormattingAct = self.dirMenu.addMenu(self.formattingMenu)
         self.dirMenu.addSeparator()
-        self.dirMenu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__dirMenuFileManagerAct = self.dirMenu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.dirMenu.addAction(self.tr("Copy Path to Clipboard"), self._copyToClipboard)
         self.dirMenu.addSeparator()
         self.dirMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
@@ -480,6 +487,8 @@
         self.dirMultiMenu.addAction(self.tr("Configure..."), self._configure)
 
         self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
+        self.menu.aboutToShow.connect(self.__showContextMenuGeneral)
+        self.attributeMenu.aboutToShow.connect(self.__showContextMenuAttribute)
         self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
         self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
         self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
@@ -524,7 +533,7 @@
         self.sourceMenu.addSeparator()
         act = self.sourceMenu.addMenu(self.graphicsMenu)
         self.sourceMenu.addSeparator()
-        self.sourceMenu.addAction(
+        self.__sourceMenuFileManagerAct = self.sourceMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.sourceMenu.addSeparator()
@@ -544,7 +553,9 @@
             self.tr("Add source directory..."), self.__addSourceDirectory
         )
         self.menu.addSeparator()
-        self.menu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__menuFileManagerAct = self.menu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.menu.addSeparator()
         self.menu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
         self.menu.addAction(self.tr("Collapse all directories"), self._collapseAllDirs)
@@ -569,7 +580,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.attributeMenu.addSeparator()
-        self.attributeMenu.addAction(
+        self.__attributeMenuFileManagerAct = self.attributeMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.attributeMenu.addSeparator()
@@ -595,7 +606,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.backMenu.addSeparator()
-        self.backMenu.addAction(
+        self.__backMenuFileManagerAct = self.backMenu.addAction(
             self.tr("Show in File Manager"), self._showProjectInFileManager
         )
         self.backMenu.addSeparator()
@@ -633,7 +644,9 @@
         self.dirMenu.addSeparator()
         act = self.dirMenu.addMenu(self.graphicsMenu)
         self.dirMenu.addSeparator()
-        self.dirMenu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__dirMenuFileManagerAct = self.dirMenu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.dirMenu.addSeparator()
         self.dirMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
         self.dirMenu.addAction(
@@ -657,6 +670,8 @@
         self.dirMultiMenu.addAction(self.tr("Configure..."), self._configure)
 
         self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
+        self.menu.aboutToShow.connect(self.__showContextMenuGeneral)
+        self.attributeMenu.aboutToShow.connect(self.__showContextMenuAttribute)
         self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
         self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
         self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
@@ -687,7 +702,7 @@
         self.sourceMenu.addSeparator()
         self.sourceMenu.addMenu(self.checksMenu)
         self.sourceMenu.addSeparator()
-        self.sourceMenu.addAction(
+        self.__sourceMenuFileManagerAct = self.sourceMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.sourceMenu.addAction(
@@ -710,7 +725,9 @@
             self.tr("Add source directory..."), self.__addSourceDirectory
         )
         self.menu.addSeparator()
-        self.menu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__menuFileManagerAct = self.menu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.menu.addSeparator()
         self.menu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
         self.menu.addAction(self.tr("Collapse all directories"), self._collapseAllDirs)
@@ -735,7 +752,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.attributeMenu.addSeparator()
-        self.attributeMenu.addAction(
+        self.__attrMenuFileManagerAct = self.attributeMenu.addAction(
             self.tr("Show in File Manager"), self._showInFileManager
         )
         self.attributeMenu.addSeparator()
@@ -761,7 +778,7 @@
             lambda: self.project.addDirectory("SOURCES"),
         )
         self.backMenu.addSeparator()
-        self.backMenu.addAction(
+        self.__backMenuFileManagerAct = self.backMenu.addAction(
             self.tr("Show in File Manager"), self._showProjectInFileManager
         )
         self.backMenu.addSeparator()
@@ -803,7 +820,9 @@
         self.dirMenu.addSeparator()
         self.dirMenu.addMenu(self.checksMenu)
         self.dirMenu.addSeparator()
-        self.dirMenu.addAction(self.tr("Show in File Manager"), self._showInFileManager)
+        self.__dirMenuFileManagerAct = self.dirMenu.addAction(
+            self.tr("Show in File Manager"), self._showInFileManager
+        )
         self.dirMenu.addAction(self.tr("Copy Path to Clipboard"), self._copyToClipboard)
         self.dirMenu.addSeparator()
         self.dirMenu.addAction(self.tr("Expand all directories"), self._expandAllDirs)
@@ -828,6 +847,8 @@
         self.dirMultiMenu.addAction(self.tr("Configure..."), self._configure)
 
         self.sourceMenu.aboutToShow.connect(self.__showContextMenu)
+        self.menu.aboutToShow.connect(self.__showContextMenuGeneral)
+        self.attributeMenu.aboutToShow.connect(self.__showContextMenuAttribute)
         self.multiMenu.aboutToShow.connect(self.__showContextMenuMulti)
         self.dirMenu.aboutToShow.connect(self.__showContextMenuDir)
         self.dirMultiMenu.aboutToShow.connect(self.__showContextMenuDirMulti)
@@ -955,14 +976,46 @@
         else:
             self.__startAct.setEnabled(False)
 
+        self.sourceMenuActions["Formatting"].setEnabled(
+            self.sourceMenuActions["Formatting"].isEnabled()
+            and not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+        self.__sourceMenuFileManagerAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
         self.showMenu.emit("Main", self.sourceMenu)
 
+    def __showContextMenuGeneral(self):
+        """
+        Private slot called by the menu aboutToShow signal.
+        """
+        ProjectBaseBrowser._showContextMenu(self, self.menu)
+
+        self.__menuFileManagerAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
+    def __showContextMenuAttribute(self):
+        """
+        Private slot called by the attributeMenu aboutToShow signal.
+        """
+        ProjectBaseBrowser._showContextMenu(self, self.menu)
+
+        self.__attributeMenuFileManagerAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
     def __showContextMenuMulti(self):
         """
         Private slot called by the multiMenu aboutToShow signal.
         """
         ProjectBaseBrowser._showContextMenuMulti(self, self.multiMenu)
 
+        self.__multiMenuFormattingAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
         self.showMenu.emit("MainMulti", self.multiMenu)
 
     def __showContextMenuDir(self):
@@ -971,6 +1024,13 @@
         """
         ProjectBaseBrowser._showContextMenuDir(self, self.dirMenu)
 
+        self.__dirMenuFormattingAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+        self.__dirMenuFileManagerAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
         self.showMenu.emit("MainDir", self.dirMenu)
 
     def __showContextMenuDirMulti(self):
@@ -987,6 +1047,10 @@
         """
         ProjectBaseBrowser._showContextMenuBack(self, self.backMenu)
 
+        self.__backMenuFileManagerAct.setEnabled(
+            not FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        )
+
         self.showMenu.emit("MainBack", self.backMenu)
 
     def __showContextMenuShow(self):
@@ -1062,8 +1126,14 @@
         """
         from .NewPythonPackageDialog import NewPythonPackageDialog
 
+        isRemote = FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        remotefsInterface = ericApp().getObject("EricServer").getServiceInterface(
+            "FileSystem"
+        )
+        separator = remotefsInterface.separator() if isRemote else os.sep
+
         dn = self.currentDirectory(relative=True)
-        if dn.startswith(os.sep):
+        if dn.startswith(separator):
             dn = dn[1:]
         dlg = NewPythonPackageDialog(dn, self)
         if dlg.exec() == QDialog.DialogCode.Accepted:
@@ -1072,10 +1142,22 @@
             packagePath = self.project.ppath
             packageFile = ""
             for name in nameParts:
-                packagePath = os.path.join(packagePath, name)
-                if not os.path.exists(packagePath):
+                packagePath = (
+                    remotefsInterface.join(packagePath, name)
+                    if isRemote
+                    else os.path.join(packagePath, name)
+                )
+                exists = (
+                    remotefsInterface.exists(packagePath)
+                    if isRemote
+                    else os.path.exists(packagePath)
+                )
+                if not exists:
                     try:
-                        os.mkdir(packagePath)
+                        if isRemote:
+                            remotefsInterface.mkdir(packagePath)
+                        else:
+                            os.mkdir(packagePath)
                     except OSError as err:
                         EricMessageBox.critical(
                             self,
@@ -1087,11 +1169,23 @@
                             ).format(packagePath, str(err)),
                         )
                         return
-                packageFile = os.path.join(packagePath, "__init__.py")
-                if not os.path.exists(packageFile):
+                packageFile = (
+                    remotefsInterface.join(packagePath, "__init__.py")
+                    if isRemote
+                    else os.path.join(packagePath, "__init__.py")
+                )
+                exists = (
+                    remotefsInterface.exists(packageFile)
+                    if isRemote
+                    else os.path.exists(packageFile)
+                )
+                if not exists:
                     try:
-                        with open(packageFile, "w", encoding="utf-8"):
-                            pass
+                        if isRemote:
+                            remotefsInterface.writeFile(packageFile, b"")
+                        else:
+                            with open(packageFile, "w", encoding="utf-8"):
+                                pass
                     except OSError as err:
                         EricMessageBox.critical(
                             self,
@@ -1111,6 +1205,11 @@
         """
         Private method to add a new source file to the project.
         """
+        isRemote = FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        remotefsInterface = ericApp().getObject("EricServer").getServiceInterface(
+            "FileSystem"
+        )
+
         dn = self.currentDirectory()
         filename, ok = EricPathPickerDialog.getStrPath(
             self,
@@ -1122,9 +1221,15 @@
             filters=self.project.getFileCategoryFilters(
                 categories=["SOURCES"], withAll=False
             ),
+            remote=isRemote,
         )
         if ok:
-            if os.path.exists(filename):
+            exists = (
+                remotefsInterface.exists(filename)
+                if isRemote
+                else os.path.exists(filename)
+            )
+            if exists:
                 EricMessageBox.critical(
                     self,
                     self.tr("New source file"),
@@ -1139,9 +1244,16 @@
                 newline = (
                     None if self.project.useSystemEol() else self.project.getEolString()
                 )
-                with open(filename, "w", newline=newline) as f:
-                    f.write("# -*- coding: utf-8 -*-\n")
-                    f.write("# {0}\n".format(self.project.getRelativePath(filename)))
+                header = "# -*- coding: utf-8 -*-\n# {0}\n".format(
+                    self.project.getRelativePath(filename)
+                )
+                if isRemote:
+                    remotefsInterface.writeFile(
+                        filename, header.encode("utf-8"), newline=newline
+                    )
+                else:
+                    with open(filename, "w", newline=newline) as f:
+                        f.write(header)
             except OSError as err:
                 EricMessageBox.critical(
                     self,
@@ -1342,12 +1454,22 @@
         """
         Private method to handle the imports diagram context menu action.
         """
+        isRemote = FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        remotefsInterface = ericApp().getObject("EricServer").getServiceInterface(
+            "FileSystem"
+        )
+
         itm = self.model().item(self.currentIndex())
         try:
             fn = itm.fileName()
         except AttributeError:
             fn = itm.dirName()
-        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
+        if isRemote:
+            package = (
+                fn if remotefsInterface.isdir(fn) else remotefsInterface.dirname(fn)
+            )
+        else:
+            package = fn if os.path.isdir(fn) else os.path.dirname(fn)
         res = EricMessageBox.yesNo(
             self,
             self.tr("Imports Diagram"),
@@ -1367,12 +1489,22 @@
         """
         Private method to handle the package diagram context menu action.
         """
+        isRemote = FileSystemUtilities.isRemoteFileName(self.project.getProjectPath())
+        remotefsInterface = ericApp().getObject("EricServer").getServiceInterface(
+            "FileSystem"
+        )
+
         itm = self.model().item(self.currentIndex())
         try:
             fn = itm.fileName()
         except AttributeError:
             fn = itm.dirName()
-        package = fn if os.path.isdir(fn) else os.path.dirname(fn)
+        if isRemote:
+            package = (
+                fn if remotefsInterface.isdir(fn) else remotefsInterface.dirname(fn)
+            )
+        else:
+            package = fn if os.path.isdir(fn) else os.path.dirname(fn)
         res = EricMessageBox.yesNo(
             self,
             self.tr("Package Diagram"),
--- a/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Fri Mar 08 15:30:23 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Fri Mar 08 15:30:53 2024 +0100
@@ -13,13 +13,17 @@
 import re
 import stat
 
-from PyQt6.QtCore import QEventLoop, QObject, pyqtSlot
+from PyQt6.QtCore import QByteArray, QEventLoop, QObject, pyqtSlot
 
 from eric7 import Utilities
 from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
 from eric7.SystemUtilities import FileSystemUtilities
 
 
+_RemoteFsCache = {}
+# dictionary containing cached remote file system data keyed by remote path
+
+
 class EricServerNotConnectedError(OSError):
     """
     Class defining a special OSError indicating a missing server connection.
@@ -29,7 +33,7 @@
         """
         Constructor
         """
-        super().__init("Not connected to an 'eric-ide' server.")
+        super().__init__("Not connected to an 'eric-ide' server.")
 
 
 class EricServerFileSystemInterface(QObject):
@@ -82,8 +86,6 @@
         if connected:
             if not bool(self.__serverPathSep):
                 self.__serverPathSep = self.__getPathSep()
-        else:
-            self.__serverPathSep = ""
 
     def __getPathSep(self):
         """
@@ -203,13 +205,15 @@
         else:
             return False, EricServerFileSystemInterface.NotConnectedMessage
 
-    def listdir(self, directory=""):
+    def listdir(self, directory="", recursive=False):
         """
         Public method to get a directory listing.
 
         @param directory directory to be listed. An empty directory means to list
             the eric-ide server current directory. (defaults to "")
         @type str (optional)
+        @param recursive flag indicating a recursive listing (defaults to False)
+        @type bool (optional)
         @return tuple containing the listed directory, the path separator and the
             directory listing. Each directory listing entry contains a dictionary
             with the relevant data.
@@ -252,7 +256,10 @@
             self.__serverInterface.sendJson(
                 category=EricRequestCategory.FileSystem,
                 request="Listdir",
-                params={"directory": FileSystemUtilities.plainFileName(directory)},
+                params={
+                    "directory": FileSystemUtilities.plainFileName(directory),
+                    "recursive": recursive,
+                },
                 callback=callback,
             )
 
@@ -293,7 +300,7 @@
         @param recursive flag indicating a recursive search (defaults to True)
         @type bool (optional)
         @param dirsonly flag indicating to return only directories. When True it has
-            precedence over the 'filesonly' parameter ((defaults to False)
+            precedence over the 'filesonly' parameter (defaults to False)
         @type bool
         @return list of all files and directories in the tree rooted at path.
             The names are expanded to start with the given directory name.
@@ -443,9 +450,12 @@
         @return flag indicating a directory
         @rtype bool
         """
-        with contextlib.suppress(KeyError, OSError):
-            result = self.stat(name, ["st_mode"])
-            return stat.S_ISDIR(result["st_mode"])
+        try:
+            return stat.S_ISDIR(_RemoteFsCache[name]["mode"])
+        except KeyError:
+            with contextlib.suppress(KeyError, OSError):
+                result = self.stat(name, ["st_mode"])
+                return stat.S_ISDIR(result["st_mode"])
 
         return False
 
@@ -458,9 +468,12 @@
         @return flag indicating a regular file
         @rtype bool
         """
-        with contextlib.suppress(KeyError, OSError):
-            result = self.stat(name, ["st_mode"])
-            return stat.S_ISREG(result["st_mode"])
+        try:
+            return stat.S_ISREG(_RemoteFsCache[name]["mode"])
+        except KeyError:
+            with contextlib.suppress(KeyError, OSError):
+                result = self.stat(name, ["st_mode"])
+                return stat.S_ISREG(result["st_mode"])
 
         return False
 
@@ -491,6 +504,9 @@
                 nameExists = params["exists"]
                 loop.quit()
 
+        if name in _RemoteFsCache:
+            return True
+
         if self.__serverInterface.isServerConnected():
             self.__serverInterface.sendJson(
                 category=EricRequestCategory.FileSystem,
@@ -598,6 +614,8 @@
             )
 
             loop.exec()
+            if ok:
+                self.populateFsCache(directory)
             return ok, error
 
         else:
@@ -649,6 +667,8 @@
             )
 
             loop.exec()
+            if ok:
+                self.populateFsCache(directory)
             return ok, error
 
         else:
@@ -693,6 +713,8 @@
             )
 
             loop.exec()
+            if ok:
+                self.removeFromFsCache(directory)
             return ok, error
 
         else:
@@ -742,6 +764,12 @@
             )
 
             loop.exec()
+            if ok:
+                with contextlib.suppress(KeyError):
+                    entry = _RemoteFsCache.pop(oldName)
+                    entry["path"] = newName
+                    entry["name"] = self.basename(newName)
+                    _RemoteFsCache[newName] = entry
             return ok, error
 
         else:
@@ -786,6 +814,9 @@
             )
 
             loop.exec()
+            if ok:
+                with contextlib.suppress(KeyError):
+                    del _RemoteFsCache[filename]
             return ok, error
 
         else:
@@ -859,14 +890,11 @@
         @return flag indicating an absolute path
         @rtype bool
         """
-        if self.__serverInterface.isServerConnected():
-            if self.__serverPathSep == "\\":
-                s = FileSystemUtilities.plainFileName(p)[:3].replace("/", "\\")
-                return s.startswith("\\)") or s.startswith(":\\", 1)
-            else:
-                return FileSystemUtilities.plainFileName(p).startswith("/")
+        if self.__serverPathSep == "\\":
+            s = FileSystemUtilities.plainFileName(p)[:3].replace("/", "\\")
+            return s.startswith("\\)") or s.startswith(":\\", 1)
         else:
-            return os.path.isabs(p)
+            return FileSystemUtilities.plainFileName(p).startswith("/")
 
     def abspath(self, p):
         """
@@ -877,13 +905,10 @@
         @return absolute path
         @rtype str
         """
-        if self.__serverInterface.isServerConnected():
-            p = FileSystemUtilities.plainFileName(p)
-            if not self.isabs(p):
-                p = self.join(self.getcwd(), p)
-            return FileSystemUtilities.remoteFileName(p)
-        else:
-            return os.path.abspath(p)
+        p = FileSystemUtilities.plainFileName(p)
+        if not self.isabs(p):
+            p = self.join(self.getcwd(), p)
+        return FileSystemUtilities.remoteFileName(p)
 
     def join(self, a, *p):
         """
@@ -897,19 +922,15 @@
         @return joined path name
         @rtype str
         """
-        if self.__serverInterface.isServerConnected():
-            path = a
-            for b in p:
-                if b.startswith(self.__serverPathSep):
-                    path = b
-                elif not path or path.endswith(self.__serverPathSep):
-                    path += b
-                else:
-                    path += self.__serverPathSep + b
-            return path
-
-        else:
-            return os.path.join(a, *p)
+        path = a
+        for b in p:
+            if b.startswith(self.__serverPathSep):
+                path = b
+            elif not path or path.endswith(self.__serverPathSep):
+                path += b
+            else:
+                path += self.__serverPathSep + b
+        return path
 
     def split(self, p):
         """
@@ -921,22 +942,18 @@
             path separator.
         @rtype tuple of (str, str)
         """
-        if self.__serverInterface.isServerConnected():
-            if self.__serverPathSep == "\\":
-                # remote is a Windows system
-                normp = p.replace("/", "\\")
-            else:
-                # remote is a Posix system
-                normp = p.replace("\\", "/")
+        if self.__serverPathSep == "\\":
+            # remote is a Windows system
+            normp = p.replace("/", "\\")
+        else:
+            # remote is a Posix system
+            normp = p.replace("\\", "/")
 
-            i = normp.rfind(self.__serverPathSep) + 1
-            head, tail = normp[:i], normp[i:]
-            if head and head != self.__serverPathSep * len(head):
-                head = head.rstrip(self.__serverPathSep)
-            return head, tail
-
-        else:
-            return os.path.split(p)
+        i = normp.rfind(self.__serverPathSep) + 1
+        head, tail = normp[:i], normp[i:]
+        if head and head != self.__serverPathSep * len(head):
+            head = head.rstrip(self.__serverPathSep)
+        return head, tail
 
     def splitext(self, p):
         """
@@ -958,23 +975,19 @@
         @return tuple containing the drive letter (incl. colon) and the path
         @rtype tuple of (str, str)
         """
-        if self.__serverInterface.isServerConnected():
-            plainp = FileSystemUtilities.plainFileName(p)
+        plainp = FileSystemUtilities.plainFileName(p)
 
-            if self.__serverPathSep == "\\":
-                # remote is a Windows system
-                normp = plainp.replace("/", "\\")
-                if normp[1:2] == ":":
-                    return normp[:2], normp[2:]
-                else:
-                    return "", normp
+        if self.__serverPathSep == "\\":
+            # remote is a Windows system
+            normp = plainp.replace("/", "\\")
+            if normp[1:2] == ":":
+                return normp[:2], normp[2:]
             else:
-                # remote is a Posix system
-                normp = plainp.replace("\\", "/")
                 return "", normp
-
         else:
-            return os.path.splitdrive(p)
+            # remote is a Posix system
+            normp = plainp.replace("\\", "/")
+            return "", normp
 
     def dirname(self, p):
         """
@@ -1028,7 +1041,7 @@
     ## Methods for reading and writing files
     #######################################################################
 
-    def readFile(self, filename, create=False):
+    def readFile(self, filename, create=False, newline=None):
         """
         Public method to read a file from the eric-ide server.
 
@@ -1037,6 +1050,9 @@
         @param create flag indicating to create an empty file, if it does not exist
             (defaults to False)
         @type bool (optional)
+        @param newline determines how to parse newline characters from the stream
+            (defaults to None)
+        @type str (optional)
         @return bytes data read from the eric-ide server
         @rtype bytes
         @exception EricServerNotConnectedError raised to indicate a missing server
@@ -1079,6 +1095,7 @@
                 params={
                     "filename": FileSystemUtilities.plainFileName(filename),
                     "create": create,
+                    "newline": "<<none>>" if newline is None else newline,
                 },
                 callback=callback,
             )
@@ -1089,17 +1106,20 @@
 
             return bText
 
-    def writeFile(self, filename, data, withBackup=False):
+    def writeFile(self, filename, data, withBackup=False, newline=None):
         """
         Public method to write the data to a file on the eric-ide server.
 
         @param filename name of the file to write
         @type str
         @param data data to be written
-        @type bytes
+        @type bytes or QByteArray
         @param withBackup flag indicating to create a backup file first
             (defaults to False)
         @type bool (optional)
+        @param newline determines how to parse newline characters from the stream
+            (defaults to None)
+        @type str (optional)
         @exception EricServerNotConnectedError raised to indicate a missing server
             connection
         @exception OSError raised in case the server reported an issue
@@ -1129,6 +1149,8 @@
             raise EricServerNotConnectedError
 
         else:
+            if isinstance(data, QByteArray):
+                data = bytes(data)
             self.__serverInterface.sendJson(
                 category=EricRequestCategory.FileSystem,
                 request="WriteFile",
@@ -1136,6 +1158,7 @@
                     "filename": FileSystemUtilities.plainFileName(filename),
                     "filedata": str(base64.b85encode(data), encoding="ascii"),
                     "with_backup": withBackup,
+                    "newline": "<<none>>" if newline is None else newline,
                 },
                 callback=callback,
             )
@@ -1315,6 +1338,9 @@
             )
 
             loop.exec()
+            if ok:
+                self.removeFromFsCache(pathname)
+
             if not ok:
                 raise OSError(error)
 
@@ -1364,3 +1390,41 @@
                 return cpath
             tail = tail[1:]
         return ""
+
+    #######################################################################
+    ## Remote file system cache methods.
+    #######################################################################
+
+    # TODO: change cache when file renamed/moved/deleted/...
+    def populateFsCache(self, directory):
+        """
+        Public method to populate the remote file system cache for a given directory.
+
+        @param directory remote directory to be cached
+        @type str
+        @exception ValueError raised to indicate an empty directory
+        """
+        if not directory:
+            raise ValueError("The directory to be cached must not be empty.")
+
+        try:
+            listing = self.listdir(directory=directory, recursive=True)[2]
+            for entry in listing:
+                _RemoteFsCache[
+                    FileSystemUtilities.remoteFileName(entry["path"])
+                ] = entry
+            print(f"Remote Cache Size: {len(_RemoteFsCache)} entries")
+        except OSError as err:
+            print("error in 'populateFsCache()':", str(err))
+
+    def removeFromFsCache(self, directory):
+        """
+        Public method to remove a given directory from the remote file system cache.
+
+        @param directory remote directory to be removed
+        @type str
+        """
+        for entryPath in list(_RemoteFsCache.keys()):
+            if entryPath.startswith(directory):
+                del _RemoteFsCache[entryPath]
+        print(f"Remote Cache Size: {len(_RemoteFsCache)} entries")

eric ide

mercurial