Fri, 23 Feb 2024 10:46:46 +0100
Merged with branch 'eric7' in order to track these changes.
src/eric7/Preferences/__init__.py | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/dialog-cancel.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/dialog-ok.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/dirNew.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/fileSaveAsRemote.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/open-remote.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/preferences-eric-server.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/projectOpen-remote.png | file | annotate | diff | comparison | revisions | |
src/eric7/icons/oxygen/projectSaveAs-remote.png | file | annotate | diff | comparison | revisions |
--- a/eric7.epj Fri Feb 23 10:43:39 2024 +0100 +++ b/eric7.epj Fri Feb 23 10:46:46 2024 +0100 @@ -630,6 +630,7 @@ "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.ui", "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.ui", "src/eric7/Preferences/ConfigurationPages/EmailPage.ui", + "src/eric7/Preferences/ConfigurationPages/EricServerPage.ui", "src/eric7/Preferences/ConfigurationPages/GraphicsPage.ui", "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.ui", "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.ui", @@ -709,6 +710,9 @@ "src/eric7/QtHelpInterface/QtHelpDocumentationConfigurationDialog.ui", "src/eric7/QtHelpInterface/QtHelpDocumentationSelectionDialog.ui", "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.ui", + "src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui", + "src/eric7/RemoteServerInterface/EricServerFileDialog.ui", + "src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui", "src/eric7/Snapshot/SnapWidget.ui", "src/eric7/SqlBrowser/SqlBrowserWidget.ui", "src/eric7/SqlBrowser/SqlConnectionDialog.ui", @@ -1188,6 +1192,7 @@ "src/eric7/EricGraphics/EricGraphicsView.py", "src/eric7/EricGraphics/__init__.py", "src/eric7/EricGui/EricAction.py", + "src/eric7/EricGui/EricFileIconProvider.py", "src/eric7/EricGui/EricGenericDiffHighlighter.py", "src/eric7/EricGui/EricOverrideCursor.py", "src/eric7/EricGui/EricPixmapCache.py", @@ -1918,6 +1923,7 @@ "src/eric7/Preferences/ConfigurationPages/EditorSyntaxPage.py", "src/eric7/Preferences/ConfigurationPages/EditorTypingPage.py", "src/eric7/Preferences/ConfigurationPages/EmailPage.py", + "src/eric7/Preferences/ConfigurationPages/EricServerPage.py", "src/eric7/Preferences/ConfigurationPages/GraphicsPage.py", "src/eric7/Preferences/ConfigurationPages/HelpDocumentationPage.py", "src/eric7/Preferences/ConfigurationPages/HelpViewersPage.py", @@ -2108,6 +2114,20 @@ "src/eric7/QtHelpInterface/QtHelpDocumentationSettingsWidget.py", "src/eric7/QtHelpInterface/QtHelpSchemeHandler.py", "src/eric7/QtHelpInterface/__init__.py", + "src/eric7/RemoteServer/EricRequestCategory.py", + "src/eric7/RemoteServer/EricServer.py", + "src/eric7/RemoteServer/EricServerCoverageRequestHandler.py", + "src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py", + "src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py", + "src/eric7/RemoteServer/__init__.py", + "src/eric7/RemoteServerInterface/EricServerConnectionDialog.py", + "src/eric7/RemoteServerInterface/EricServerCoverageInterface.py", + "src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py", + "src/eric7/RemoteServerInterface/EricServerFileDialog.py", + "src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py", + "src/eric7/RemoteServerInterface/EricServerInterface.py", + "src/eric7/RemoteServerInterface/EricServerProfilesDialog.py", + "src/eric7/RemoteServerInterface/__init__.py", "src/eric7/Sessions/SessionFile.py", "src/eric7/Sessions/__init__.py", "src/eric7/Snapshot/SnapWidget.py", @@ -2493,6 +2513,7 @@ "src/eric7/eric7_qregularexpression.pyw", "src/eric7/eric7_re.py", "src/eric7/eric7_re.pyw", + "src/eric7/eric7_server.py", "src/eric7/eric7_shell.py", "src/eric7/eric7_shell.pyw", "src/eric7/eric7_snap.py",
--- a/pyproject.toml Fri Feb 23 10:43:39 2024 +0100 +++ b/pyproject.toml Fri Feb 23 10:46:46 2024 +0100 @@ -101,6 +101,7 @@ eric7_api = "eric7.eric7_api:main" eric7_doc = "eric7.eric7_doc:main" eric7_post_install = "eric7.eric7_post_install:main" +eric7_server = "eric7.eric7_server:main" [project.gui-scripts] eric7_browser = "eric7.eric7_browser:main"
--- a/src/eric7/DataViews/CodeMetrics.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/DataViews/CodeMetrics.py Fri Feb 23 10:46:46 2024 +0100 @@ -24,6 +24,8 @@ from dataclasses import dataclass from eric7 import Utilities +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities KEYWORD = token.NT_OFFSET + 1 COMMENT = tokenize.COMMENT @@ -226,7 +228,13 @@ @rtype SourceStat """ try: - text = Utilities.readEncodedFile(filename)[0] + if FileSystemUtilities.isRemoteFileName(filename): + remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + text = remotefsInterface.readEncodedFile(filename)[0] + else: + text = Utilities.readEncodedFile(filename)[0] except (OSError, UnicodeError): return SourceStat()
--- a/src/eric7/DataViews/CodeMetricsDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/DataViews/CodeMetricsDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -56,6 +56,10 @@ self.cancelled = False + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + self.__menu = QMenu(self) self.__menu.addAction(self.tr("Collapse All"), self.__resultCollapse) self.__menu.addAction(self.tr("Expand All"), self.__resultExpand) @@ -153,15 +157,26 @@ loc = QLocale() if isinstance(fn, list): files = fn - elif os.path.isdir(fn): + elif FileSystemUtilities.isRemoteFileName( + fn + ) and self.__remotefsInterface.isdir(fn): + files = [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.direntries(fn, True, "*.py", False) + ] + elif FileSystemUtilities.isPlainFileName(fn) and os.path.isdir(fn): files = FileSystemUtilities.direntries(fn, True, "*.py", False) else: files = [fn] files.sort() # check for missing files for f in files[:]: - if not os.path.exists(f): - files.remove(f) + if FileSystemUtilities.isRemoteFileName(f): + if not self.__remotefsInterface.exists(f): + files.remove(f) + else: + if not os.path.exists(f): + files.remove(f) self.checkProgress.setMaximum(len(files)) QApplication.processEvents()
--- a/src/eric7/DataViews/PyCoverageDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/DataViews/PyCoverageDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -25,6 +25,9 @@ from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp +from eric7.RemoteServerInterface.EricServerCoverageInterface import ( + EricServerCoverageError, +) from eric7.SystemUtilities import FileSystemUtilities from .Ui_PyCoverageDialog import Ui_PyCoverageDialog @@ -57,7 +60,6 @@ self.resultList.headerItem().setText(self.resultList.columnCount(), "") self.cancelled = False - self.path = "." self.reload = False self.excludeList = ["# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]"] @@ -78,6 +80,11 @@ self.resultList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.resultList.customContextMenuRequested.connect(self.__showContextMenu) + # eric-ide server interface + self.__serverCoverageInterface = ( + ericApp().getObject("EricServer").getServiceInterface("Coverage") + ) + def __format_lines(self, lines): """ Private method to format a list of integers into string by coalescing @@ -143,7 +150,7 @@ @param coverage percent of coverage @type int @param excluded list of excluded lines - @type str + @type list of int @param missing list of lines without coverage @type str """ @@ -154,7 +161,7 @@ str(statements), str(executed), "{0:.0f}%".format(coverage), - excluded, + excluded and self.__format_lines(excluded) or "", missing, ], ) @@ -194,18 +201,19 @@ if isinstance(fn, list): files = fn - self.path = os.path.dirname(cfn) - elif os.path.isdir(fn): + elif FileSystemUtilities.isRemoteFileName( + self.cfn + ) and self.__remotefsInterface.isdir(fn): + files = [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.direntries(fn, True, "*.py", False) + ] + elif FileSystemUtilities.isPlainFileName(self.cfn) and os.path.isdir(fn): files = FileSystemUtilities.direntries(fn, True, "*.py", False) - self.path = fn else: files = [fn] - self.path = os.path.dirname(cfn) files.sort() - cover = Coverage(data_file=self.cfn) - cover.load() - # set the exclude pattern self.excludeCombo.clear() self.excludeCombo.addItems(self.excludeList) @@ -217,7 +225,23 @@ total_executed = 0 total_exceptions = 0 - cover.exclude(self.excludeList[0]) + if FileSystemUtilities.isRemoteFileName(self.cfn): + ok, error = self.__serverCoverageInterface.loadCoverageData( + self.cfn, self.excludeList[0] + ) + if not ok: + EricMessageBox.critical( + self, + self.tr("Load Coverage Data"), + self.tr( + "<p>The coverage data could not be loaded from file" + " <b>{0}</b>.</p><p>Reason: {1}</p>" + ).format(self.cfn, error), + ) + else: + cover = Coverage(data_file=self.cfn) + cover.load() + cover.exclude(self.excludeList[0]) try: # disable updates of the list for speed @@ -231,18 +255,28 @@ return try: - statements, excluded, missing, readable = cover.analysis2(file)[1:] - readableEx = excluded and self.__format_lines(excluded) or "" + if FileSystemUtilities.isRemoteFileName(self.cfn): + ( + file, + statements, + excluded, + missing, + readable, + ) = self.__serverCoverageInterface.analyzeFile(file) + else: + statements, excluded, missing, readable = cover.analysis2(file)[ + 1: + ] n = len(statements) m = n - len(missing) pc = 100.0 * m / n if n > 0 else 100.0 self.__createResultItem( - file, str(n), str(m), pc, readableEx, readable + file, str(n), str(m), pc, excluded, readable ) total_statements += n total_executed += m - except CoverageException: + except (CoverageException, EricServerCoverageError): total_exceptions += 1 self.checkProgress.setValue(progress)
--- a/src/eric7/DataViews/PyProfileDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/DataViews/PyProfileDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -22,7 +22,8 @@ ) from eric7.EricWidgets import EricMessageBox -from eric7.SystemUtilities import PythonUtilities +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities, PythonUtilities from .Ui_PyProfileDialog import Ui_PyProfileDialog @@ -104,6 +105,11 @@ self.summaryList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.summaryList.customContextMenuRequested.connect(self.__showContextMenu) + # eric-ide server interface + self.__serverFsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def __createResultItem( self, calls, @@ -265,7 +271,10 @@ self.basename = os.path.splitext(pfn)[0] fname = "{0}.profile".format(self.basename) - if not os.path.exists(fname): + if ( + FileSystemUtilities.isRemoteFileName(fname) + and not self.__serverFsInterface.exists(fname) + ) or (FileSystemUtilities.isPlainFileName(fname) and not os.path.exists(fname)): EricMessageBox.warning( self, self.tr("Profile Results"), @@ -277,8 +286,12 @@ self.close() return try: - with open(fname, "rb") as f: - self.stats = pickle.load(f) # secok + if FileSystemUtilities.isRemoteFileName(fname): + data = self.__serverFsInterface.readFile(fname) + self.stats = pickle.loads(data) + else: + with open(fname, "rb") as f: + self.stats = pickle.load(f) # secok except (EOFError, OSError, pickle.PickleError): EricMessageBox.critical( self, @@ -291,7 +304,10 @@ self.close() return - self.file = fn + if FileSystemUtilities.isRemoteFileName(fname): + self.file = FileSystemUtilities.plainFileName(fn) + else: + self.file = fn self.__populateLists() self.__finish()
--- a/src/eric7/DebugClients/Python/DebugClientBase.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/DebugClients/Python/DebugClientBase.py Fri Feb 23 10:46:46 2024 +0100 @@ -1106,6 +1106,7 @@ self.eventExit = False self.pollingDisabled = disablePolling selectErrors = 0 + self.rawLine = "" while not self.eventExit: wrdy = []
--- a/src/eric7/Debugger/DebugServer.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Debugger/DebugServer.py Fri Feb 23 10:46:46 2024 +0100 @@ -18,6 +18,7 @@ from eric7 import Preferences from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities from . import DebugClientCapabilities from .BreakPointModel import BreakPointModel @@ -241,6 +242,8 @@ self.__autoClearShell = False self.__forProject = False + self.__ericServerDebugging = False + self.clientClearBreak.connect(self.__clientClearBreakPoint) self.clientClearWatch.connect(self.__clientClearWatchPoint) self.newConnection.connect(self.__newConnection) @@ -492,28 +495,32 @@ forProject=False, runInConsole=False, venvName="", - workingDir=None, + workingDir="", configOverride=None, + startRemote=None, ): """ Public method to start a debug client. - @param unplanned flag indicating that the client has died - @type bool - @param clType type of client to be started - @type str - @param forProject flag indicating a project related action - @type bool + @param unplanned flag indicating that the client has died (defaults to True) + @type bool (optional) + @param clType type of client to be started (defaults to None) + @type str (optional) + @param forProject flag indicating a project related action (defaults to False) + @type bool (optional) @param runInConsole flag indicating to start the debugger in a - console window - @type bool - @param venvName name of the virtual environment to be used - @type str - @param workingDir directory to start the debugger client in - @type str - @param configOverride dictionary containing the global config override - data - @type dict + console window (defaults to False) + @type bool (optional) + @param venvName name of the virtual environment to be used (defaults to "") + @type str (optional) + @param workingDir directory to start the debugger client in (defaults to "") + @type str (optional) + @param configOverride dictionary containing the global config override data + (defaults to None) + @type dict (optional) + @param startRemote flag indicating to start the client via an eric-ide server + (defaults to None) + @type bool (optional) """ self.running = False @@ -560,6 +567,7 @@ self.__originalPathString, workingDir=workingDir, configOverride=configOverride, + startRemote=startRemote, ) else: ( @@ -573,6 +581,7 @@ self.__originalPathString, workingDir=workingDir, configOverride=configOverride, + startRemote=startRemote, ) else: ( @@ -586,6 +595,7 @@ self.__originalPathString, workingDir=workingDir, configOverride=configOverride, + startRemote=startRemote, ) if self.clientProcess: @@ -603,6 +613,14 @@ elif self.__autoClearShell: self.__autoClearShell = False self.remoteBanner() + elif startRemote: + self.__ericServerDebugging = True + if self.lastClientType != self.clientType: + self.lastClientType = self.clientType + self.remoteBanner() + elif self.__autoClearShell: + self.__autoClearShell = False + self.remoteBanner() else: if clType and self.lastClientType: self.__setClientType(self.lastClientType) @@ -726,11 +744,20 @@ if (fn, lineno) in self.__reportedBreakpointIssues: self.__reportedBreakpointIssues.remove((fn, lineno)) - self.remoteBreakpoint(debuggerId, fn, lineno, True, cond, temp) - if not enabled: - self.__remoteBreakpointEnable(debuggerId, fn, lineno, False) - if ignorecount: - self.__remoteBreakpointIgnore(debuggerId, fn, lineno, ignorecount) + if ( + self.__ericServerDebugging + and FileSystemUtilities.isRemoteFileName(fn) + ) or ( + not self.__ericServerDebugging + and FileSystemUtilities.isPlainFileName(fn) + ): + self.remoteBreakpoint(debuggerId, fn, lineno, True, cond, temp) + if not enabled: + self.__remoteBreakpointEnable(debuggerId, fn, lineno, False) + if ignorecount: + self.__remoteBreakpointIgnore( + debuggerId, fn, lineno, ignorecount + ) def __makeWatchCondition(self, cond, special): """ @@ -901,12 +928,13 @@ def isClientProcessUp(self): """ - Public method to check, if the debug client process is up. + Public method to check, if the debug client process is up or we are + doing debugging via the eric-ide server. @return flag indicating a running debug client process @rtype bool """ - return self.clientProcess is not None + return self.clientProcess is not None or self.__ericServerDebugging def __newConnection(self): """ @@ -1088,6 +1116,7 @@ runInConsole=runInConsole, venvName=venvName, configOverride=configOverride, + startRemote=FileSystemUtilities.isRemoteFileName(fn), ) self.setCallTraceEnabled("", enableCallTrace) @@ -1178,6 +1207,7 @@ runInConsole=runInConsole, venvName=venvName, configOverride=configOverride, + startRemote=FileSystemUtilities.isRemoteFileName(fn), ) self.remoteEnvironment(env) @@ -1260,6 +1290,7 @@ runInConsole=runInConsole, venvName=venvName, configOverride=configOverride, + startRemote=FileSystemUtilities.isRemoteFileName(fn), ) self.remoteEnvironment(env) @@ -1342,6 +1373,7 @@ runInConsole=runInConsole, venvName=venvName, configOverride=configOverride, + startRemote=FileSystemUtilities.isRemoteFileName(fn), ) self.remoteEnvironment(env) @@ -1910,6 +1942,7 @@ self.signalClientOutput(self.tr("\nNot connected\n")) self.signalClientStatement(False, "") self.running = False + self.__ericServerDebugging = False def signalClientClearBreak(self, filename, lineno, debuggerId): """ @@ -2223,3 +2256,14 @@ except KeyError: # The project object is not present return "" + + def getEricServerEnvironmentString(self): + """ + Public method to get the string for an eric-ide server environment. + + @return string for the eric-ide server environment + @rtype str + """ + # TODO: make this more elaborate once server environments definitions + # are supported + return "eric-ide Server"
--- a/src/eric7/Debugger/DebugUI.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Debugger/DebugUI.py Fri Feb 23 10:46:46 2024 +0100 @@ -2048,6 +2048,13 @@ self.clientType = editor.determineFileType() self.lastStartAction = 5 + if ( + FileSystemUtilities.isRemoteFileName(fn) + and not self.ui.isEricServerConnected() + ): + self.__showNotConnectedWarning(title=cap) + return + # save the filename for use by the restart method self.lastDebuggedFile = fn self.restartAct.setEnabled(True) @@ -2243,6 +2250,13 @@ self.clientType = editor.determineFileType() self.lastStartAction = 7 + if ( + FileSystemUtilities.isRemoteFileName(fn) + and not self.ui.isEricServerConnected() + ): + self.__showNotConnectedWarning(title=cap) + return + # save the filename for use by the restart method self.lastDebuggedFile = fn self.restartAct.setEnabled(True) @@ -2433,6 +2447,13 @@ self.clientType = editor.determineFileType() self.lastStartAction = 3 + if ( + FileSystemUtilities.isRemoteFileName(fn) + and not self.ui.isEricServerConnected() + ): + self.__showNotConnectedWarning(title=cap) + return + # save the filename for use by the restart method self.lastDebuggedFile = fn self.restartAct.setEnabled(True) @@ -2515,8 +2536,8 @@ @param debugProject flag indicating debugging the current project (True) or script (False) @type bool - @param script name of a script (optional) - @type str + @param script name of a script (defaults to "") + @type str (optional) """ from .StartDialog import StartDialog, StartDialogMode @@ -2636,6 +2657,13 @@ self.clientType = editor.determineFileType() self.lastStartAction = 1 + if ( + FileSystemUtilities.isRemoteFileName(fn) + and not self.ui.isEricServerConnected() + ): + self.__showNotConnectedWarning(title=cap) + return + # save the filename for use by the restart method self.lastDebuggedFile = fn self.restartAct.setEnabled(True) @@ -2828,6 +2856,13 @@ self.viewmanager.unhighlight() if not doNotStart: + if ( + FileSystemUtilities.isRemoteFileName(fn) + and not self.ui.isEricServerConnected() + ): + self.__showNotConnectedWarning(title=self.tr("Restart")) + return + if forProject and self.project.getProjectType() in ["E7Plugin"]: argv = '--plugin="{0}" {1}'.format(fn, argv) fn = "" # script name of the eric IDE is set in debug client @@ -3169,3 +3204,19 @@ @rtype str """ return self.debugServer.getProjectEnvironmentString() + + def __showNotConnectedWarning(self, title): + """ + Private method to show a warning about a not connected eric-ide server. + + @param title title for the dialog + @type str + """ + EricMessageBox.warning( + None, + title, + self.tr( + "<p>The selected file is located on an eric-ide server but no such" + " server is connected. Aborting...</p>" + ), + )
--- a/src/eric7/Debugger/DebuggerInterfacePython.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Debugger/DebuggerInterfacePython.py Fri Feb 23 10:46:46 2024 +0100 @@ -15,7 +15,7 @@ import struct import zlib -from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer +from PyQt6.QtCore import QObject, QProcess, QProcessEnvironment, QTimer, pyqtSlot from eric7 import Preferences, Utilities from eric7.EricWidgets import EricMessageBox @@ -50,6 +50,23 @@ self.__autoContinued = [] self.__isStepCommand = False + self.__ericServerDebugging = False # are we debugging via the eric-ide server? + try: + self.__ericServerDebuggerInterface = ( + ericApp().getObject("EricServer").getServiceInterface("Debugger") + ) + self.__ericServerDebuggerInterface.debugClientResponse.connect( + lambda jsonStr: self.handleJsonCommand(jsonStr, None) + ) + self.__ericServerDebuggerInterface.debugClientDisconnected.connect( + self.__handleServerDebugClientDisconnected + ) + self.__ericServerDebuggerInterface.lastClientExited.connect( + self.__handleServerLastClientExited + ) + except KeyError: + self.__ericServerDebuggerInterface = None + self.debugServer = debugServer self.passive = passive self.process = None @@ -88,8 +105,8 @@ @param fn filename to be translated @type str @param remote2local flag indicating the direction of translation - (False = local to remote, True = remote to local [default]) - @type bool + (False = local to remote, True = remote to local) (defaults to True) + @type bool (optional) @return translated filename @rtype str """ @@ -102,8 +119,8 @@ @param fn filename to be translated @type str @param remote2local flag indicating the direction of translation - (False = local to remote, True = remote to local [default]) - @type bool + (False = local to remote, True = remote to local) (defaults to True) + @type bool (optional) @return translated filename @rtype str """ @@ -118,7 +135,24 @@ return path - def __startProcess(self, program, arguments, environment=None, workingDir=None): + def __ericServerTranslation(self, fn, remote2local=True): + """ + Private method to perform the eric-ide server path translation. + + @param fn filename to be translated + @type str + @param remote2local flag indicating the direction of translation + (False = local to remote, True = remote to local) (defaults to True) + @type bool (optional) + @return translated filename + @rtype str + """ + if remote2local: + return FileSystemUtilities.remoteFileName(fn) + else: + return FileSystemUtilities.plainFileName(fn) + + def __startProcess(self, program, arguments, environment=None, workingDir=""): """ Private method to start the debugger client process. @@ -154,8 +188,9 @@ runInConsole, venvName, originalPathString, - workingDir=None, + workingDir="", configOverride=None, + startRemote=None, ): """ Public method to start a remote Python interpreter. @@ -169,41 +204,61 @@ @type str @param originalPathString original PATH environment variable @type str - @param workingDir directory to start the debugger client in - @type str - @param configOverride dictionary containing the global config override - data - @type dict + @param workingDir directory to start the debugger client in (defaults to "") + @type str (optional) + @param configOverride dictionary containing the global config override data + (defaults to None) + @type dict (optional) + @param startRemote flag indicating to start the client via an eric-ide server + (defaults to None) + @type bool (optional) @return client process object, a flag to indicate a network connection and the name of the interpreter in case of a local execution @rtype tuple of (QProcess, bool, str) """ global origPathEnv - if not venvName: - venvName = Preferences.getDebugger("Python3VirtualEnv") - if venvName == self.debugServer.getProjectEnvironmentString(): - project = ericApp().getObject("Project") - venvName = project.getProjectVenv() - execPath = project.getProjectExecPath() - interpreter = project.getProjectInterpreter() + if ( + startRemote is True + or ( + startRemote is None + and ( + venvName == self.debugServer.getEricServerEnvironmentString() + or self.__ericServerDebugging + ) + ) + ) and ericApp().getObject("EricServer").isServerConnected(): + # TODO change this once server environment definitions are supported + startRemote = True + venvName = self.debugServer.getEricServerEnvironmentString() + interpreter = "" else: - venvManager = ericApp().getObject("VirtualEnvManager") - interpreter = venvManager.getVirtualenvInterpreter(venvName) - execPath = venvManager.getVirtualenvExecPath(venvName) - if interpreter == "": - # use the interpreter used to run eric for identical variants - interpreter = PythonUtilities.getPythonExecutable() - if interpreter == "": - EricMessageBox.critical( - None, - self.tr("Start Debugger"), - self.tr("""<p>No suitable Python3 environment configured.</p>"""), - ) - return None, False, "" + if not venvName: + venvName = Preferences.getDebugger("Python3VirtualEnv") + if venvName == self.debugServer.getProjectEnvironmentString(): + project = ericApp().getObject("Project") + venvName = project.getProjectVenv() + execPath = project.getProjectExecPath() + interpreter = project.getProjectInterpreter() + else: + venvManager = ericApp().getObject("VirtualEnvManager") + interpreter = venvManager.getVirtualenvInterpreter(venvName) + execPath = venvManager.getVirtualenvExecPath(venvName) + if interpreter == "": + # use the interpreter used to run eric for identical variants + interpreter = PythonUtilities.getPythonExecutable() + if interpreter == "": + EricMessageBox.critical( + None, + self.tr("Start Debugger"), + self.tr("""<p>No suitable Python3 environment configured.</p>"""), + ) + return None, False, "" self.__inShutdown = False + self.__ericServerDebugging = False + redirect = ( str(configOverride["redirect"]) if configOverride and configOverride["enable"] @@ -290,6 +345,28 @@ ) return None, False, "" + elif startRemote and self.__ericServerDebuggerInterface is not None: + # debugging via an eric-ide server + self.translate = self.__ericServerTranslation + self.__ericServerDebugging = True + + args = [] + if noencoding: + args.append(noencoding) + if multiprocessEnabled: + args.append(multiprocessEnabled) + if callTraceOptimization: + args.append(callTraceOptimization) + self.__ericServerDebuggerInterface.startClient( + interpreter, + originalPathString, + args, + workingDir=workingDir, + ) + self.__startedVenv = venvName + + return None, self.__isNetworked, "" + else: # local debugging code below debugClient = self.__determineDebugClient() @@ -398,6 +475,7 @@ originalPathString, workingDir=None, configOverride=None, + startRemote=False, ): """ Public method to start a remote Python interpreter for a project. @@ -416,10 +494,15 @@ @param configOverride dictionary containing the global config override data @type dict + @param startRemote flag indicating to start the client via an eric-ide server + (defaults to False) + @type bool (optional) @return client process object, a flag to indicate a network connection and the name of the interpreter in case of a local execution @rtype tuple of (QProcess, bool, str) """ + # TODO: 'startRemoteForProject()' - implement the 'startRemote' logic like + # for 'startRemote'. global origPathEnv project = ericApp().getObject("Project") @@ -460,6 +543,8 @@ self.__inShutdown = False + self.__ericServerDebugging = False + if project.getDebugProperty("REMOTEDEBUGGER"): # remote debugging code ipaddr = self.debugServer.getHostAddress(False) @@ -517,6 +602,8 @@ # remote shell command is missing return None, self.__isNetworked, "" + # TODO: add server debugging for projects + else: # local debugging code below debugClient = project.getDebugProperty("DEBUGCLIENT") @@ -619,7 +706,7 @@ """ self.__pendingConnections.append(sock) - sock.readyRead.connect(lambda: self.__parseClientLine(sock)) + sock.readyRead.connect(lambda: self.__receiveJson(sock)) sock.disconnected.connect(lambda: self.__socketDisconnected(sock)) return True @@ -634,30 +721,30 @@ @param debuggerId id of the connected debug client @type str """ - if sock in self.__pendingConnections: + if sock and sock in self.__pendingConnections: self.__connections[debuggerId] = sock self.__pendingConnections.remove(sock) - if self.__mainDebugger is None: - self.__mainDebugger = debuggerId - # Get the remote clients capabilities - self.remoteCapabilities(debuggerId) + if self.__mainDebugger is None: + self.__mainDebugger = debuggerId + # Get the remote clients capabilities + self.remoteCapabilities(debuggerId) - self.debugServer.signalClientDebuggerId(debuggerId) + self.debugServer.signalClientDebuggerId(debuggerId) - if debuggerId == self.__mainDebugger: - self.__flush() - self.debugServer.mainClientConnected() + if debuggerId == self.__mainDebugger: + self.__flush() + self.debugServer.mainClientConnected() - self.debugServer.initializeClient(debuggerId) + self.debugServer.initializeClient(debuggerId) - # perform auto-continue except for main - if ( - debuggerId != self.__mainDebugger - and self.__autoContinue - and not self.__isStepCommand - ): - QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId)) + # perform auto-continue except for main + if ( + debuggerId != self.__mainDebugger + and self.__autoContinue + and not self.__isStepCommand + ): + QTimer.singleShot(0, lambda: self.remoteContinue(debuggerId)) def __socketDisconnected(self, sock): """ @@ -669,14 +756,7 @@ for debuggerId in list(self.__connections): if self.__connections[debuggerId] is sock: del self.__connections[debuggerId] - if debuggerId == self.__mainDebugger: - self.__mainDebugger = None - if debuggerId in self.__autoContinued: - self.__autoContinued.remove(debuggerId) - if not self.__inShutdown: - with contextlib.suppress(RuntimeError): - # can be ignored during a shutdown - self.debugServer.signalClientDisconnected(debuggerId) + self.__handleServerDebugClientDisconnected(debuggerId) break else: if sock in self.__pendingConnections: @@ -684,13 +764,37 @@ if not self.__connections: # no active connections anymore + self.__handleServerLastClientExited() + + @pyqtSlot(str) + def __handleServerDebugClientDisconnected(self, debuggerId): + """ + Private slot handling the disconnect of a debug client. + + @param debuggerId ID of the disconnected debugger + @type str + """ + if debuggerId == self.__mainDebugger: + self.__mainDebugger = None + if debuggerId in self.__autoContinued: + self.__autoContinued.remove(debuggerId) + if not self.__inShutdown: with contextlib.suppress(RuntimeError): - # debug server object might have been deleted already - # ignore this + # can be ignored during a shutdown + self.debugServer.signalClientDisconnected(debuggerId) + + @pyqtSlot() + def __handleServerLastClientExited(self): + """ + Private slot to handle the exit of the last debug client connected. + """ + with contextlib.suppress(RuntimeError): + # debug server object might have been deleted already + # ignore this + self.__autoContinued.clear() + if not self.__inShutdown: self.debugServer.signalLastClientExited() - self.__autoContinued.clear() - if not self.__inShutdown: - self.debugServer.startClient() + self.debugServer.startClient() def getDebuggerIds(self): """ @@ -707,9 +811,15 @@ """ if self.__mainDebugger: # Send commands that were waiting for the connection. - conn = self.__connections[self.__mainDebugger] - for jsonStr in self.__commandQueue: - self.__writeJsonCommandToSocket(jsonStr, conn) + if self.__ericServerDebugging: + for jsonStr in self.__commandQueue: + self.__ericServerDebuggerInterface.sendClientCommand( + self.__mainDebugger, jsonStr + ) + else: + conn = self.__connections[self.__mainDebugger] + for jsonStr in self.__commandQueue: + self.__writeJsonCommandToSocket(jsonStr, conn) self.__commandQueue.clear() @@ -733,6 +843,8 @@ sock = self.__pendingConnections.pop() self.__shutdownSocket(sock) + self.__ericServerDebuggerInterface.stopClient() + # reinitialize self.__commandQueue.clear() @@ -765,7 +877,7 @@ @return flag indicating the connection status @rtype bool """ - return bool(self.__connections) + return bool(self.__connections) or self.__ericServerDebugging def remoteEnvironment(self, env): """ @@ -810,12 +922,15 @@ instead of unhandled exceptions only @type bool """ + if FileSystemUtilities.isPlainFileName(fn): + fn = os.path.abspath(fn) + self.__autoContinue = autoContinue - self.__scriptName = os.path.abspath(fn) + self.__scriptName = fn self.__isStepCommand = False wd = self.translate(wd, False) - fn = self.translate(os.path.abspath(fn), False) + fn = self.translate(fn, False) self.__sendJsonCommand( "RequestLoad", { @@ -840,10 +955,13 @@ @param wd working directory for the program @type str """ - self.__scriptName = os.path.abspath(fn) + if FileSystemUtilities.isPlainFileName(fn): + fn = os.path.abspath(fn) + + self.__scriptName = fn wd = self.translate(wd, False) - fn = self.translate(os.path.abspath(fn), False) + fn = self.translate(fn, False) self.__sendJsonCommand( "RequestRun", { @@ -868,10 +986,13 @@ cleared first @type bool """ - self.__scriptName = os.path.abspath(fn) + if FileSystemUtilities.isPlainFileName(fn): + fn = os.path.abspath(fn) + + self.__scriptName = fn wd = self.translate(wd, False) - fn = self.translate(os.path.abspath(fn), False) + fn = self.translate(fn, False) self.__sendJsonCommand( "RequestCoverage", { @@ -897,10 +1018,13 @@ first @type bool """ - self.__scriptName = os.path.abspath(fn) + if FileSystemUtilities.isPlainFileName(fn): + fn = os.path.abspath(fn) + + self.__scriptName = fn wd = self.translate(wd, False) - fn = self.translate(os.path.abspath(fn), False) + fn = self.translate(fn, False) self.__sendJsonCommand( "RequestProfile", { @@ -1042,7 +1166,12 @@ @param temp flag indicating a temporary breakpoint @type bool """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: self.__sendJsonCommand( "RequestBreakpoint", @@ -1069,7 +1198,12 @@ @param enable flag indicating enabling or disabling a breakpoint @type bool """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: self.__sendJsonCommand( "RequestBreakpointEnable", @@ -1094,7 +1228,12 @@ @param count number of occurrences to ignore @type int """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: self.__sendJsonCommand( "RequestBreakpointIgnore", @@ -1119,7 +1258,12 @@ @param temp flag indicating a temporary watch expression @type bool """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: # cond is combination of cond and special (s. watch expression # viewer) @@ -1144,7 +1288,12 @@ @param enable flag indicating enabling or disabling a watch expression @type bool """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: # cond is combination of cond and special (s. watch expression # viewer) @@ -1169,7 +1318,12 @@ @param count number of occurrences to ignore @type int """ - debuggerList = [debuggerId] if debuggerId else list(self.__connections) + if debuggerId: + debuggerList = [debuggerId] + elif self.__ericServerDebugging: + debuggerList = ["<<all>>"] + else: + debuggerList = list(self.__connections) for debuggerId in debuggerList: # cond is combination of cond and special (s. watch expression # viewer) @@ -1396,7 +1550,7 @@ debuggerId, ) - def __parseClientLine(self, sock): + def __receiveJson(self, sock): """ Private method to handle data from the client. @@ -1423,11 +1577,11 @@ logging.debug("<Debug-Server> %s", jsonStr) ##print("Server: ", jsonStr) ## debug # __IGNORE_WARNING_M891__ - self.__handleJsonCommand(jsonStr, sock) + self.handleJsonCommand(jsonStr, sock) - def __handleJsonCommand(self, jsonStr, sock): + def handleJsonCommand(self, jsonStr, sock): """ - Private method to handle a command or response serialized as a + Public method to handle a command or response serialized as a JSON string. @param jsonStr string containing the command or response received @@ -1553,6 +1707,7 @@ self.debugServer.signalClientRawInput( params["prompt"], params["echo"], params["debuggerId"] ) + pass elif method == "ResponseBPConditionError": fn = self.translate(params["filename"], True) @@ -1622,7 +1777,7 @@ elif method == "ResponseExit": self.__scriptName = "" self.debugServer.signalClientExit( - params["program"], + self.translate(params["program"], True), params["status"], params["message"], params["debuggerId"], @@ -1663,14 +1818,26 @@ } jsonStr = json.dumps(commandDict) - if debuggerId and debuggerId in self.__connections: - sock = self.__connections[debuggerId] - elif sock is None and self.__mainDebugger is not None: - sock = self.__connections[self.__mainDebugger] - if sock is not None: - self.__writeJsonCommandToSocket(jsonStr, sock) + if self.__ericServerDebugging: + # Debugging via the eric-ide server -> pass the command on to it + if self.__mainDebugger is None: + # debugger has not connected yet -> queue the command + self.__commandQueue.append(jsonStr) + else: + self.__ericServerDebuggerInterface.sendClientCommand( + debuggerId, jsonStr + ) else: - self.__commandQueue.append(jsonStr) + # Local debugging -> send the command to the client + if debuggerId and debuggerId in self.__connections: + sock = self.__connections[debuggerId] + elif sock is None and self.__mainDebugger is not None: + with contextlib.suppress(KeyError): + sock = self.__connections[self.__mainDebugger] + if sock is not None: + self.__writeJsonCommandToSocket(jsonStr, sock) + else: + self.__commandQueue.append(jsonStr) def __writeJsonCommandToSocket(self, jsonCommand, sock): """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/EricGui/EricFileIconProvider.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a file icon provider determining the icon based on file name. +""" + +import fnmatch + +from PyQt6.QtGui import QImageReader + +from eric7.EricGui import EricPixmapCache + + +class EricFileIconProvider: + """ + Class implementing a file icon provider determining the icon based on file name. + """ + + def __init__(self): + """ + Constructor + """ + # pixmap icon names first because some are overwritten later + self.__iconMappings = { + "*.{0}".format(bytes(f).decode()): "filePixmap" + for f in QImageReader.supportedImageFormats() + } + + # specific one next + self.__iconMappings.update( + { + "*.sh": "lexerBash", + "*.bash": "lexerBash", + "*.bat": "lexerBatch", + "*.cmd": "lexerBatch", + "*.cpp": "lexerCPP", + "*.cxx": "lexerCPP", + "*.cc": "lexerCPP", + "*.c": "lexerCPP", + "*.hpp": "lexerCPP", + "*.hh": "lexerCPP", + "*.h": "lexerCPP", + "*.cs": "lexerCsharp", + "CMakeLists.txt": "lexerCMake", + "*.cmake": "lexerCMake", + "*.cmake.in": "lexerCMake", + "*.ctest": "lexerCMake", + "*.ctest.in": "lexerCMake", + "*.css": "lexerCSS", + "*.qss": "lexerCSS", + "*.d": "lexerD", + "*.di": "lexerD", + "*.diff": "lexerDiff", + "*.patch": "lexerDiff", + "*.html": "lexerHTML", + "*.htm": "lexerHTML", + "*.asp": "lexerHTML", + "*.shtml": "lexerHTML", + "*.php": "lexerHTML", + "*.php3": "lexerHTML", + "*.php4": "lexerHTML", + "*.php5": "lexerHTML", + "*.phtml": "lexerHTML", + "*.docbook": "lexerHTML", + "*.ui": "fileDesigner", + "*.ts": "fileLinguist", + "*.qm": "fileLinguist2", + "*.qrc": "fileResource", + "*.kid": "lexerHTML", + "*.java": "lexerJava", + "*.js": "lexerJavaScript", + "*.lua": "lexerLua", + "*makefile": "lexerMakefile", + "Makefile*": "lexerMakefile", + "*.mak": "lexerMakefile", + "*.pl": "lexerPerl", + "*.pm": "lexerPerl", + "*.ph": "lexerPerl", + "*.pov": "lexerPovray", + "*.properties": "lexerProperties", + "*.ini": "lexerProperties", + "*.inf": "lexerProperties", + "*.reg": "lexerProperties", + "*.cfg": "lexerProperties", + "*.cnf": "lexerProperties", + "*.rc": "lexerProperties", + "*.py": "lexerPython3", + "*.pyw": "lexerPython3", + "*.py3": "lexerPython3", + "*.pyw3": "lexerPython3", + "*.pyx": "lexerCython", + "*.pxd": "lexerCython", + "*.pxi": "lexerCython", + "*.ptl": "lexerPython3", + "*.rb": "lexerRuby", + "*.rbw": "lexerRuby", + "*.sql": "lexerSQL", + "*.tex": "lexerTeX", + "*.sty": "lexerTeX", + "*.aux": "lexerTeX", + "*.toc": "lexerTeX", + "*.idx": "lexerTeX", + "*.vhd": "lexerVHDL", + "*.vhdl": "lexerVHDL", + "*.tcl": "lexerTCL", + "*.tk": "lexerTCL", + "*.f": "lexerFortran", + "*.for": "lexerFortran", + "*.f90": "lexerFortran", + "*.f95": "lexerFortran", + "*.f2k": "lexerFortran", + "*.dpr": "lexerPascal", + "*.dpk": "lexerPascal", + "*.pas": "lexerPascal", + "*.dfm": "lexerPascal", + "*.inc": "lexerPascal", + "*.pp": "lexerPascal", + "*.ps": "lexerPostScript", + "*.xml": "lexerXML", + "*.xsl": "lexerXML", + "*.svg": "fileSvg", + "*.xsd": "lexerXML", + "*.xslt": "lexerXML", + "*.dtd": "lexerXML", + "*.rdf": "lexerXML", + "*.xul": "lexerXML", + "*.yaml": "lexerYAML", + "*.yml": "lexerYAML", + "*.m": "lexerMatlab", + "*.m.matlab": "lexerMatlab", + "*.m.octave": "lexerOctave", + "*.e4c": "lexerXML", + "*.e4d": "lexerXML", + "*.e4k": "fileShortcuts", + "*.e4m": "fileMultiProject", + "*.e4p": "fileProject", + "*.e4q": "lexerXML", + "*.e4s": "lexerXML", + "*.e4t": "lexerXML", + "*.e5d": "lexerXML", + "*.e5g": "fileUML", + "*.e5k": "fileShortcuts", + "*.e5m": "fileMultiProject", + "*.e5p": "fileProject", + "*.e5q": "lexerXML", + "*.e5s": "lexerXML", + "*.e5t": "lexerXML", + "*.e6d": "lexerXML", + "*.e6k": "fileShortcuts", + "*.e6m": "fileMultiProject", + "*.e6p": "fileProject", + "*.e6q": "lexerXML", + "*.e6s": "lexerXML", + "*.e6t": "lexerXML", + "*.ecj": "lexerJSON", + "*.edj": "lexerJSON", + "*.egj": "fileUML", + "*.ehj": "lexerJSON", + "*.ekj": "fileShortcuts", + "*.emj": "fileMultiProject", + "*.epj": "fileProject", + "*.eqj": "lexerJSON", + "*.esj": "lexerJSON", + "*.etj": "lexerJSON", + "*.ethj": "lexerJSON", + "*.po": "lexerGettext", + "*.coffee": "lexerCoffeeScript", + "*.json": "lexerJSON", + "*.md": "lexerMarkdown", + "*.toml": "lexerProperties", + "Pipfile": "lexerProperties", + "poetry.lock": "lexerProperties", + "*.pdf": "pdfviewer", + } + ) + + def fileIcon(self, name): + """ + Public method to get an icon for the given file name. + + @param name file name + @type str + @return icon + @rtype QIcon + """ + for pat in self.__iconMappings: + if fnmatch.fnmatch(name, pat): + return EricPixmapCache.getIcon(self.__iconMappings[pat]) + else: + return EricPixmapCache.getIcon("fileMisc") + + def fileIconName(self, name): + """ + Public method to get an icon name for the given file name. + + @param name file name + @type str + @return icon name + @rtype str + """ + for pat in self.__iconMappings: + if fnmatch.fnmatch(name, pat): + return self.__iconMappings[pat] + else: + return "fileMisc"
--- a/src/eric7/Globals/__init__.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Globals/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -197,7 +197,7 @@ """ if loc is None: if size < 1024: - return QCoreApplication.translate("Globals", "{0:4.2f} Bytes").format(size) + return QCoreApplication.translate("Globals", "{0:4d} Bytes").format(size) elif size < 1024 * 1024: size /= 1024 return QCoreApplication.translate("Globals", "{0:4.2f} KiB").format(size) @@ -211,9 +211,12 @@ size /= 1024 * 1024 * 1024 * 1024 return QCoreApplication.translate("Globals", "{0:4.2f} TiB").format(size) else: + if not isinstance(size, float): + size = float(size) + if size < 1024: return QCoreApplication.translate("Globals", "{0} Bytes").format( - loc.toString(size, "f", 2) + loc.toString(size) ) elif size < 1024 * 1024: size /= 1024
--- a/src/eric7/Graphics/ApplicationDiagramBuilder.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Graphics/ApplicationDiagramBuilder.py Fri Feb 23 10:46:46 2024 +0100 @@ -15,6 +15,7 @@ from eric7 import Globals, Preferences from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp from eric7.EricWidgets.EricProgressDialog import EricProgressDialog from eric7.SystemUtilities import FileSystemUtilities @@ -49,6 +50,10 @@ self.tr("Application Diagram {0}").format(self.project.getProjectName()) ) + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def __buildModulesDict(self): """ Private method to build a dictionary of modules contained in the @@ -64,9 +69,16 @@ mods = self.project.getProjectData(dataKey="SOURCES") modules = [] for module in mods: - modules.append( - FileSystemUtilities.normabsjoinpath(self.project.ppath, module) - ) + if FileSystemUtilities.isRemoteFileName(self.project.getProjectPath()): + modules.append( + self.__remotefsInterface.join(self.project.getProjectPath(), module) + ) + else: + modules.append( + FileSystemUtilities.normabsjoinpath( + self.project.getProjectPath(), module + ) + ) tot = len(modules) progress = EricProgressDialog( self.tr("Parsing modules..."), @@ -111,26 +123,48 @@ @rtype str """ candidates = [] - path = self.project.getProjectPath() - init = os.path.join(path, "__init__.py") - if os.path.exists(init): - # project is a package - return path + ppath = self.project.getProjectPath() + + if FileSystemUtilities.isRemoteFileName(ppath): + init = self.__remotefsInterface.join(ppath, "__init__.py") + if self.__remotefsInterface.exists(init): + # remote project is a package + return ppath + else: + # check, if any of the top directories is a package + for entry in self.__remotefsInterface.listdir(ppath)[2]: + if ( + not entry["name"].startswith(".") + and entry["is_dir"] + and self.__remotefsInterface.exists( + self.__remotefsInterface.join(entry["path"], "__init__.py") + ) + ): + candidates.append(entry["path"]) + + # check, if project uses the 'src' layout + srcPath = self.__remotefsInterface.join(ppath, "src") + if self.__remotefsInterface.exists(srcPath): + for entry in self.__remotefsInterface.listdir(srcPath)[2]: + if ( + not entry["name"].startswith(".") + and entry["is_dir"] + and self.__remotefsInterface.exists( + self.__remotefsInterface.join( + entry["path"], "__init__.py" + ) + ) + ): + candidates.append(entry["path"]) + else: - # check, if any of the top directories is a package - with os.scandir(path) as dirEntriesIterator: - for entry in [ - e for e in dirEntriesIterator if not e.name.startswith(".") - ]: - if entry.is_dir() and os.path.exists( - os.path.join(entry.path, "__init__.py") - ): - candidates.append(entry.path) - - # check, if project uses the 'src' layout - srcPath = os.path.join(path, "src") - if os.path.exists(srcPath): - with os.scandir(srcPath) as dirEntriesIterator: + init = os.path.join(ppath, "__init__.py") + if os.path.exists(init): + # project is a package + return ppath + else: + # check, if any of the top directories is a package + with os.scandir(ppath) as dirEntriesIterator: for entry in [ e for e in dirEntriesIterator if not e.name.startswith(".") ]: @@ -139,6 +173,18 @@ ): candidates.append(entry.path) + # check, if project uses the 'src' layout + srcPath = os.path.join(ppath, "src") + if os.path.exists(srcPath): + with os.scandir(srcPath) as dirEntriesIterator: + for entry in [ + e for e in dirEntriesIterator if not e.name.startswith(".") + ]: + if entry.is_dir() and os.path.exists( + os.path.join(entry.path, "__init__.py") + ): + candidates.append(entry.path) + if len(candidates) == 1: return candidates[0] elif len(candidates) > 1: @@ -172,7 +218,13 @@ # no root path detected return - root = os.path.splitdrive(rpath)[1].replace(os.sep, ".")[1:] + root = ( + self.__remotefsInterface.splitdrive(rpath)[1][1:].replace( + self.__remotefsInterface.separator(), "." + ) + if FileSystemUtilities.isRemoteFileName(rpath) + else os.path.splitdrive(rpath)[1][1:].replace(os.sep, ".") + ) packages = {} self.__shapes = {}
--- a/src/eric7/Graphics/ImportsDiagramBuilder.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Graphics/ImportsDiagramBuilder.py Fri Feb 23 10:46:46 2024 +0100 @@ -14,6 +14,7 @@ from PyQt6.QtWidgets import QApplication, QGraphicsTextItem from eric7 import Globals, Preferences +from eric7.EricWidgets.EricApplication import ericApp from eric7.EricWidgets.EricProgressDialog import EricProgressDialog from eric7.SystemUtilities import FileSystemUtilities @@ -49,7 +50,10 @@ self.setObjectName("ImportsDiagram") self.showExternalImports = showExternalImports - self.packagePath = os.path.abspath(package) + if FileSystemUtilities.isRemoteFileName(package): + self.packagePath = package + else: + self.packagePath = os.path.abspath(package) self.__relPackagePath = ( self.project.getRelativePath(self.packagePath) @@ -57,16 +61,33 @@ else "" ) + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def initialize(self): """ Public method to initialize the object. """ - self.package = os.path.splitdrive(self.packagePath)[1].replace(os.sep, ".")[1:] hasInit = True ppath = self.packagePath - while hasInit: - ppath = os.path.dirname(ppath) - hasInit = len(glob.glob(os.path.join(ppath, "__init__.*"))) > 0 + + if FileSystemUtilities.isRemoteFileName(self.packagePath): + self.package = self.__remotefsInterface.splitdrive(self.packagePath)[1][ + 1: + ].replace(self.__remotefsInterface.separator(), ".") + while hasInit: + ppath = self.__remotefsInterface.dirname(ppath) + globPattern = self.__remotefsInterface.join(ppath, "__init__.*") + hasInit = len(self.__remotefsInterface.glob(globPattern)) > 0 + else: + self.package = os.path.splitdrive(self.packagePath)[1][1:].replace( + os.sep, "." + ) + while hasInit: + ppath = os.path.dirname(ppath) + hasInit = len(glob.glob(os.path.join(ppath, "__init__.*"))) > 0 + self.shortPackage = self.packagePath.replace(ppath, "").replace(os.sep, ".")[1:] pname = self.project.getProjectName() @@ -93,13 +114,21 @@ moduleDict = {} modules = [] for ext in Preferences.getPython("Python3Extensions"): - modules.extend( - glob.glob( - FileSystemUtilities.normjoinpath( - self.packagePath, "*{0}".format(ext) + if FileSystemUtilities.isRemoteFileName(self.packagePath): + modules.extend( + [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.glob( + self.__remotefsInterface.join(self.packagePath, f"*{ext}") + ) + ] + ) + else: + modules.extend( + glob.glob( + FileSystemUtilities.normjoinpath(self.packagePath, f"*{ext}") ) ) - ) tot = len(modules) progress = EricProgressDialog( @@ -141,7 +170,11 @@ """ Public method to build the modules shapes of the diagram. """ - initlist = glob.glob(os.path.join(self.packagePath, "__init__.*")) + if FileSystemUtilities.isRemoteFileName(self.packagePath): + globPattern = self.__remotefsInterface.join(self.packagePath, "__init__.*") + initlist = self.__remotefsInterface.glob(globPattern) + else: + initlist = glob.glob(os.path.join(self.packagePath, "__init__.*")) if len(initlist) == 0: ct = QGraphicsTextItem(None) ct.setHtml(
--- a/src/eric7/Graphics/PackageDiagramBuilder.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Graphics/PackageDiagramBuilder.py Fri Feb 23 10:46:46 2024 +0100 @@ -16,6 +16,7 @@ from PyQt6.QtWidgets import QApplication, QGraphicsTextItem from eric7 import Globals, Preferences +from eric7.EricWidgets.EricApplication import ericApp from eric7.EricWidgets.EricProgressDialog import EricProgressDialog from eric7.SystemUtilities import FileSystemUtilities @@ -45,7 +46,10 @@ super().__init__(dialog, view, project) self.setObjectName("PackageDiagram") - self.package = os.path.abspath(package) + if FileSystemUtilities.isRemoteFileName(package): + self.package = package + else: + self.package = os.path.abspath(package) self.noAttrs = noAttrs self.__relPackage = ( @@ -54,6 +58,10 @@ else "" ) + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def initialize(self): """ Public method to initialize the object. @@ -97,9 +105,19 @@ moduleDict = {} modules = [] for ext in supportedExt: - modules.extend( - glob.glob(FileSystemUtilities.normjoinpath(self.package, ext)) - ) + if FileSystemUtilities.isRemoteFileName(self.package): + modules.extend( + [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.glob( + self.__remotefsInterface.join(self.package, ext) + ) + ] + ) + else: + modules.extend( + glob.glob(FileSystemUtilities.normjoinpath(self.package, ext)) + ) tot = len(modules) progress = EricProgressDialog( self.tr("Parsing modules..."), @@ -154,19 +172,47 @@ subpackagesDict = {} subpackagesList = [] - with os.scandir(self.package) as dirEntriesIterator: - for subpackage in dirEntriesIterator: + if FileSystemUtilities.isRemoteFileName(self.package): + for subpackage in self.__remotefsInterface.listdir(self.package)[2]: if ( - subpackage.is_dir() - and subpackage.name != "__pycache__" - and len(glob.glob(os.path.join(subpackage.path, "__init__.*"))) != 0 + subpackage["is_dir"] + and subpackage["name"] != "__pycache__" + and len( + self.__remotefsInterface.glob( + self.__remotefsInterface.join( + subpackage["path"], "__init__.*" + ) + ) + ) + != 0 ): - subpackagesList.append(subpackage.path) + subpackagesList.append( + FileSystemUtilities.remoteFileName(subpackage["path"]) + ) + else: + with os.scandir(self.package) as dirEntriesIterator: + for subpackage in dirEntriesIterator: + if ( + subpackage.is_dir() + and subpackage.name != "__pycache__" + and len(glob.glob(os.path.join(subpackage.path, "__init__.*"))) + != 0 + ): + subpackagesList.append(subpackage.path) tot = 0 for ext in supportedExt: for subpackage in subpackagesList: - tot += len(glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext))) + if FileSystemUtilities.isRemoteFileName(subpackage): + tot += len( + self.__remotefsInterface.glob( + self.__remotefsInterface.join(subpackage, ext) + ) + ) + else: + tot += len( + glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext)) + ) progress = EricProgressDialog( self.tr("Parsing modules..."), None, @@ -187,9 +233,19 @@ subpackagesDict[packageName] = [] modules = [] for ext in supportedExt: - modules.extend( - glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext)) - ) + if FileSystemUtilities.isRemoteFileName(subpackage): + modules.extend( + [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.glob( + self.__remotefsInterface.join(subpackage, ext) + ) + ] + ) + else: + modules.extend( + glob.glob(FileSystemUtilities.normjoinpath(subpackage, ext)) + ) for prog, module in enumerate(modules, start=start): progress.setValue(prog) if time.monotonic() - now > 0.01: @@ -225,7 +281,12 @@ """ self.allClasses = {} - initlist = glob.glob(os.path.join(self.package, "__init__.*")) + globPattern = os.path.join(self.package, "__init__.*") + initlist = ( + self.__remotefsInterface.glob(globPattern) + if FileSystemUtilities.isRemoteFileName(self.package) + else glob.glob(globPattern) + ) if len(initlist) == 0: ct = QGraphicsTextItem(None) self.scene.addItem(ct)
--- a/src/eric7/JediInterface/AssistantJedi.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/JediInterface/AssistantJedi.py Fri Feb 23 10:46:46 2024 +0100 @@ -13,6 +13,7 @@ from PyQt6.QtWidgets import QMenu from eric7 import Preferences +from eric7.SystemUtilities import FileSystemUtilities from .JediServer import JediServer @@ -254,4 +255,7 @@ @type Editor """ if menuName == "Main": - self.__menu.setEnabled(editor.hasSelectedText()) + self.__menu.setEnabled( + not FileSystemUtilities.isRemoteFileName(editor.getFileName()) + and editor.hasSelectedText() + )
--- a/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/CodeStyleCheckerDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -202,6 +202,10 @@ self.mainWidget.setCurrentWidget(self.configureTab) self.optionsTabWidget.setCurrentWidget(self.globalOptionsTab) + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def __defaultStatistics(self): """ Private method to return the default statistics entry. @@ -840,13 +844,21 @@ if isinstance(fn, list): self.files = fn[:] - elif os.path.isdir(fn): - self.files = [] + elif FileSystemUtilities.isRemoteFileName( + fn + ) and self.__remotefsInterface.isdir(fn): extensions = set(Preferences.getPython("Python3Extensions")) - for ext in extensions: - self.files.extend( - FileSystemUtilities.direntries(fn, True, "*{0}".format(ext), 0) + self.files = [ + FileSystemUtilities.remoteFileName(f) + for f in self.__remotefsInterface.direntries( + fn, True, [f"*{ext}" for ext in extensions], False ) + ] + elif FileSystemUtilities.isPlainFileName(fn) and os.path.isdir(fn): + extensions = set(Preferences.getPython("Python3Extensions")) + self.files = FileSystemUtilities.direntries( + fn, True, [f"*{ext}" for ext in extensions], False + ) else: self.files = [fn] @@ -1085,7 +1097,12 @@ encoding = Utilities.get_coding(source) else: try: - source, encoding = Utilities.readEncodedFile(self.filename) + if FileSystemUtilities.isRemoteFileName(self.filename): + source, encoding = self.__remotefsInterface.readEncodedFile( + self.filename + ) + else: + source, encoding = Utilities.readEncodedFile(self.filename) source = source.splitlines(True) except (OSError, UnicodeError) as msg: self.results = CodeStyleCheckerDialog.hasResults @@ -1131,7 +1148,12 @@ self.__timenow = time.monotonic() try: - source, encoding = Utilities.readEncodedFile(filename) + if FileSystemUtilities.isRemoteFileName(filename): + source, encoding = self.__remotefsInterface.readEncodedFile( + filename + ) + else: + source, encoding = Utilities.readEncodedFile(filename) source = source.splitlines(True) except (OSError, UnicodeError) as msg: self.results = CodeStyleCheckerDialog.hasResults
--- a/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -87,6 +87,10 @@ self.syntaxCheckService = None self.filename = None + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + def __resort(self): """ Private method to resort the tree. @@ -421,7 +425,12 @@ self.source = Utilities.normalizeCode(codestring) else: try: - self.source = Utilities.readEncodedFile(self.filename)[0] + if FileSystemUtilities.isRemoteFileName(self.filename): + self.source = self.__remotefsInterface.readEncodedFile( + self.filename + )[0] + else: + self.source = Utilities.readEncodedFile(self.filename)[0] self.source = Utilities.normalizeCode(self.source) except (OSError, UnicodeError) as msg: self.noResults = False @@ -458,7 +467,11 @@ self.__timenow = time.monotonic() try: - source = Utilities.readEncodedFile(filename)[0] + source = ( + self.__remotefsInterface.readEncodedFile(self.filename)[0] + if FileSystemUtilities.isRemoteFileName(self.filename) + else Utilities.readEncodedFile(filename)[0] + ) source = Utilities.normalizeCode(source) except (OSError, UnicodeError) as msg: self.noResults = False
--- a/src/eric7/Plugins/PluginCodeStyleChecker.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Plugins/PluginCodeStyleChecker.py Fri Feb 23 10:46:46 2024 +0100 @@ -476,6 +476,7 @@ if menuName == "Checks": if self.__editorAct not in menu.actions(): menu.addAction(self.__editorAct) + # TODO: disable the action for eric-ide server files self.__editorAct.setEnabled(editor.isPyFile()) def __editorCodeStyleCheck(self):
--- a/src/eric7/Preferences/ConfigurationDialog.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Preferences/ConfigurationDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -210,6 +210,13 @@ None, None, ], + "ericServerPage": [ + self.tr("eric-ide Server"), + "preferences-eric-server", + "EricServerPage", + None, + None, + ], "graphicsPage": [ self.tr("Graphics"), "preferences-graphics",
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the eric-ide server related settings. +""" + +from eric7 import Preferences + +from .ConfigurationPageBase import ConfigurationPageBase +from .Ui_EricServerPage import Ui_EricServerPage + + +class EricServerPage(ConfigurationPageBase, Ui_EricServerPage): + """ + Class implementing the eric-ide server related settings. + """ + + def __init__(self): + """ + Constructor + """ + super().__init__() + self.setupUi(self) + self.setObjectName("EricServerPage") + + # set initial values + self.timeoutSpinBox.setValue(Preferences.getEricServer("ConnectionTimeout")) + self.startShellCheckBox.setChecked(Preferences.getEricServer("AutostartShell")) + + def save(self): + """ + Public slot to save the Cooperation configuration. + """ + Preferences.setEricServer("ConnectionTimeout", self.timeoutSpinBox.value()) + Preferences.setEricServer("AutostartShell", self.startShellCheckBox.isChecked()) + + +def create(dlg): # noqa: U100 + """ + Module function to create the configuration page. + + @param dlg reference to the configuration dialog + @type ConfigurationDialog + @return reference to the instantiated page + @rtype ConfigurationPageBase + """ + page = EricServerPage() + return page
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/Preferences/ConfigurationPages/EricServerPage.ui Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EricServerPage</class> + <widget class="QWidget" name="EricServerPage"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>510</width> + <height>452</height> + </rect> + </property> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <item> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string><b>Configure eric-ide Server Settings</b></string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line11"> + <property name="frameShape"> + <enum>QFrame::HLine</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Sunken</enum> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string><b>Note:</b> The eric-ide server is configured via command line parameters. The parameters of this page configure the interface to the eric-ide server.</string> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox"> + <property name="title"> + <string>Server Connection</string> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Default Timeout:</string> + </property> + </widget> + </item> + <item> + <widget class="QSpinBox" name="timeoutSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="minimum"> + <number>5</number> + </property> + <property name="maximum"> + <number>60</number> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>294</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Shell</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QCheckBox" name="startShellCheckBox"> + <property name="toolTip"> + <string>Select this to start an eric-ide Server Shell when a connection to an eric-ide Server is established.</string> + </property> + <property name="text"> + <string>Start server Shell when server is conncted</string> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>87</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> + <tabstops> + <tabstop>timeoutSpinBox</tabstop> + <tabstop>startShellCheckBox</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- a/src/eric7/Preferences/__init__.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Preferences/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -1709,7 +1709,7 @@ else: jediDefaults["MouseClickGotoModifiers"] = Qt.KeyboardModifier.ControlModifier - # defaults for Hex Editor + # defaults for PDF viewer pdfViewerDefaults = { "PdfViewerState": QByteArray(), "PdfViewerSplitterState": QByteArray(), @@ -1723,6 +1723,13 @@ "PdfSearchHighlightAll": True, } + # defaults for the eric-ide server interface + ericServerDefaults = { + "ConnectionProfiles": "{}", # JSON encoded dictionary + "ConnectionTimeout": 10, # timeout in seconds + "AutostartShell": True, + } + def readToolGroups(): """ @@ -4141,7 +4148,7 @@ def getPdfViewer(key): """ - Module function to retrieve the Pdf Viewer related settings. + Module function to retrieve the PDF Viewer related settings. @param key the key of the value to get @type str @@ -4174,7 +4181,7 @@ def setPdfViewer(key, value): """ - Module function to store the Pdf Viewer related settings. + Module function to store the PDF Viewer related settings. @param key the key of the setting to be set @type str @@ -4186,6 +4193,52 @@ Prefs.settings.setValue("PdfViewer/" + key, value) +def getEricServer(key): + """ + Module function to retrieve the eric-ide server interface related settings. + + @param key the key of the value to get + @type str + @return the requested user setting + @rtype Any + """ + prefix = "EricServer/" + + if key in ("ConnectionTimeout",): + return int( + Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key]) + ) + elif key in ("AutostartShell",): + return toBool( + Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key]) + ) + elif key in ("ConnectionProfiles",): + jsonStr = Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key]) + if jsonStr: + return json.loads(jsonStr) + else: + return None + else: + return Prefs.settings.value(f"{prefix}{key}", Prefs.ericServerDefaults[key]) + + +def setEricServer(key, value): + """ + Module function to store the eric-ide server interface related settings. + + @param key the key of the setting to be set + @type str + @param value the value to be set + @type Any + """ + prefix = "EricServer/" + + if key in ("ConnectionProfiles",): + Prefs.settings.setValue(f"{prefix}{key}", json.dumps(value)) + else: + Prefs.settings.setValue(f"{prefix}{key}", value) + + def getGeometry(key): """ Module function to retrieve the display geometry.
--- a/src/eric7/Project/DebuggerPropertiesFile.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Project/DebuggerPropertiesFile.py Fri Feb 23 10:46:46 2024 +0100 @@ -17,6 +17,8 @@ from eric7 import Preferences from eric7.EricGui.EricOverrideCursor import EricOverridenCursor from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities Project = typing.TypeVar("Project") @@ -48,6 +50,10 @@ @return flag indicating a successful write @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + debuggerPropertiesDict = { "header": { "comment": "eric debugger properties file for project {0}".format( @@ -66,13 +72,18 @@ try: jsonString = json.dumps(debuggerPropertiesDict, indent=2) + "\n" - with open(filename, "w") as f: - f.write(jsonString) + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Save Remote Debugger Properties") + fsInterface.writeFile(filename, jsonString.encode("utf-8")) + else: + title = self.tr("Save Debugger Properties") + with open(filename, "w") as f: + f.write(jsonString) except (OSError, TypeError) as err: with EricOverridenCursor(): EricMessageBox.critical( None, - self.tr("Save Debugger Properties"), + title, self.tr( "<p>The project debugger properties file" " <b>{0}</b> could not be written.</p>" @@ -93,14 +104,23 @@ @return flag indicating a successful read @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + try: - with open(filename, "r") as f: - jsonString = f.read() + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Read Remote Debugger Properties") + jsonString = fsInterface.readFile(filename).decode("utf-8") + else: + title = self.tr("Read Debugger Properties") + with open(filename, "r") as f: + jsonString = f.read() debuggerPropertiesDict = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.critical( None, - self.tr("Read Debugger Properties"), + title, self.tr( "<p>The project debugger properties file <b>{0}</b>" " could not be read.</p><p>Reason: {1}</p>"
--- a/src/eric7/Project/Project.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Project/Project.py Fri Feb 23 10:46:46 2024 +0100 @@ -44,6 +44,7 @@ from eric7.EricWidgets.EricListSelectionDialog import EricListSelectionDialog from eric7.EricWidgets.EricProgressDialog import EricProgressDialog from eric7.Globals import recentNameProject +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.Sessions.SessionFile import SessionFile from eric7.SystemUtilities import ( FileSystemUtilities, @@ -158,18 +159,22 @@ DefaultMake = "make" DefaultMakefile = "makefile" - def __init__(self, parent=None, filename=None): + def __init__(self, parent=None, filename=None, remoteServer=None): """ Constructor @param parent parent widget (usually the ui object) @type QWidget - @param filename optional filename of a project file to open - @type str + @param filename optional filename of a project file to open (defaults to None) + @type str (optional) + @param remoteServer reference to the 'eric-ide' server interface object + @type EricServerInterface """ super().__init__(parent) self.ui = parent + self.__remoteServer = remoteServer + self.__remotefsInterface = remoteServer.getServiceInterface("FileSystem") self.__progLanguages = [ "Python3", @@ -210,7 +215,7 @@ else: self.vcs = self.initVCS() - self.__model = ProjectBrowserModel(self) + self.__model = ProjectBrowserModel(self, fsInterface=self.__remotefsInterface) self.codemetrics = None self.codecoverage = None @@ -1098,10 +1103,18 @@ """ removed = False removelist = [] - for file in self.__pdata[index]: - if not os.path.exists(os.path.join(self.ppath, file)): - removelist.append(file) - removed = True + if FileSystemUtilities.isRemoteFileName(self.ppath): + for file in self.__pdata[index]: + if not self.__remotefsInterface.exists( + self.__remotefsInterface.join(self.ppath, file) + ): + removelist.append(file) + removed = True + else: + for file in self.__pdata[index]: + if not os.path.exists(os.path.join(self.ppath, file)): + removelist.append(file) + removed = True if removed: for file in removelist: @@ -1110,7 +1123,7 @@ def __readProject(self, fn): """ - Private method to read in a project (.epj) file. + Private method to read in a project file (.epj). @param fn filename of the project file to be read @type str @@ -1121,8 +1134,16 @@ res = self.__projectFile.readFile(fn) if res: - self.pfile = os.path.abspath(fn) - self.ppath = os.path.abspath(os.path.dirname(fn)) + if FileSystemUtilities.isRemoteFileName(fn): + self.pfile = fn + self.ppath = self.__remotefsInterface.dirname(fn) + self.name = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(fn) + )[0] + else: + self.pfile = os.path.abspath(fn) + self.ppath = os.path.abspath(os.path.dirname(fn)) + self.name = os.path.splitext(os.path.basename(fn))[0] # insert filename into list of recently opened projects self.__syncRecent() @@ -1133,15 +1154,22 @@ )[0] elif self.__pdata["MAINSCRIPT"]: self.translationsRoot = os.path.splitext(self.__pdata["MAINSCRIPT"])[0] - if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)): - dn = self.translationsRoot + + if FileSystemUtilities.isRemoteFileName(self.ppath): + if self.__remotefsInterface.isdir( + self.__remotefsInterface.join(self.ppath, self.translationsRoot) + ): + dn = self.translationsRoot + else: + dn = self.__remotefsInterface.dirname(self.translationsRoot) else: - dn = os.path.dirname(self.translationsRoot) - if dn not in self.subdirs: + if os.path.isdir(os.path.join(self.ppath, self.translationsRoot)): + dn = self.translationsRoot + else: + dn = os.path.dirname(self.translationsRoot) + if dn and dn not in self.subdirs: self.subdirs.append(dn) - self.name = os.path.splitext(os.path.basename(fn))[0] - # check, if the files of the project still exist in the # project directory for fileCategory in self.getFileCategories(): @@ -1150,14 +1178,22 @@ # get the names of subdirectories the files are stored in for fileCategory in [c for c in self.getFileCategories() if c != "OTHERS"]: for fn in self.__pdata[fileCategory]: - dn = os.path.dirname(fn) - if dn not in self.subdirs: + dn = ( + self.__remotefsInterface.dirname(fn) + if FileSystemUtilities.isRemoteFileName(fn) + else os.path.dirname(fn) + ) + if dn and dn not in self.subdirs: self.subdirs.append(dn) # get the names of other subdirectories for fn in self.__pdata["OTHERS"]: - dn = os.path.dirname(fn) - if dn not in self.otherssubdirs: + dn = ( + self.__remotefsInterface.dirname(fn) + if FileSystemUtilities.isRemoteFileName(fn) + else os.path.dirname(fn) + ) + if dn and dn not in self.otherssubdirs: self.otherssubdirs.append(dn) return res @@ -1196,9 +1232,16 @@ res = self.__projectFile.writeFile(fn) if res: - self.pfile = os.path.abspath(fn) - self.ppath = os.path.abspath(os.path.dirname(fn)) - self.name = os.path.splitext(os.path.basename(fn))[0] + if FileSystemUtilities.isRemoteFileName(fn): + self.pfile = fn + self.ppath = self.__remotefsInterface.dirname(fn) + self.name = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(fn) + )[0] + else: + self.pfile = os.path.abspath(fn) + self.ppath = os.path.abspath(os.path.dirname(fn)) + self.name = os.path.splitext(os.path.basename(fn))[0] self.setDirty(False) # insert filename into list of recently opened projects @@ -1213,10 +1256,22 @@ if self.pfile is None: return - fn1, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn1)) - if os.path.exists(fn): - self.__userProjectFile.readFile(fn) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}.eqj" + ) + if not self.__remotefsInterface.exists(fn): + return + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.eqj") + if not os.path.exists(fn): + return + + self.__userProjectFile.readFile(fn) def __writeUserProperties(self): """ @@ -1225,8 +1280,16 @@ if self.pfile is None: return - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.eqj".format(fn)) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}.eqj" + ) + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.eqj") with EricOverrideCursor(): self.__userProjectFile.writeFile(fn) @@ -1239,9 +1302,18 @@ if self.pfile is None: enable = False else: - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - fn_sess = os.path.join(self.getProjectManagementDir(), "{0}.esj".format(fn)) - enable = os.path.exists(fn_sess) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn_sess = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn}.esj" + ) + enable = self.__remotefsInterface.exists(fn) + else: + fn, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn_sess = os.path.join(self.getProjectManagementDir(), f"{fn}.esj") + enable = os.path.exists(fn_sess) self.sessActGrp.findChild(QAction, "project_load_session").setEnabled(enable) self.sessActGrp.findChild(QAction, "project_delete_session").setEnabled(enable) @@ -1265,12 +1337,22 @@ ) return - fn1, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join( - self.getProjectManagementDir(), "{0}{1}.esj".format(fn1, indicator) - ) - if os.path.exists(fn): - self.__sessionFile.readFile(fn) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}{indicator}.esj" + ) + if not self.__remotefsInterface.exists(fn): + return + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}{indicator}.esj") + if not os.path.exists(fn): + return + + self.__sessionFile.readFile(fn) @pyqtSlot() def __writeSession(self, quiet=False, indicator=""): @@ -1292,10 +1374,16 @@ ) return - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join( - self.getProjectManagementDir(), "{0}{1}.esj".format(fn, indicator) - ) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}{indicator}.esj" + ) + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}{indicator}.esj") self.__sessionFile.writeFile(fn) @@ -1311,21 +1399,32 @@ ) return - fname, ext = os.path.splitext(os.path.basename(self.pfile)) - - fn = os.path.join(self.getProjectManagementDir(), f"{fname}.esj") - if os.path.exists(fn): - try: - os.remove(fn) - except OSError: - EricMessageBox.critical( - self.ui, - self.tr("Delete Project Session"), - self.tr( - "<p>The project session file <b>{0}</b> could" - " not be deleted.</p>" - ).format(fn), + try: + if FileSystemUtilities.isRemoteFileName(self.pfile): + title = self.tr("Delete Remote Project Session") + fname, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fname}.esj" ) + if self.__remotefsInterface.exists(fn): + self.__remotefsInterface.remove(fn) + else: + title = self.tr("Delete Project Session") + fname, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fname}.esj") + if os.path.exists(fn): + os.remove(fn) + except OSError: + EricMessageBox.critical( + self.ui, + title, + self.tr( + "<p>The project session file <b>{0}</b> could" + " not be deleted.</p>" + ).format(fn), + ) def __readTasks(self): """ @@ -1339,10 +1438,22 @@ ) return - base, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(base)) - if os.path.exists(fn): - self.__tasksFile.readFile(fn) + if FileSystemUtilities.isRemoteFileName(self.pfile): + base, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{base}.etj" + ) + if not self.__remotefsInterface.exists(fn): + return + else: + base, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{base}.etj") + if not os.path.exists(fn): + return + + self.__tasksFile.readFile(fn) def writeTasks(self): """ @@ -1351,9 +1462,17 @@ if self.pfile is None: return - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - - fn = os.path.join(self.getProjectManagementDir(), "{0}.etj".format(fn)) + if FileSystemUtilities.isRemoteFileName(self.pfile): + base, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{base}.etj" + ) + else: + base, ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{base}.etj") + self.__tasksFile.writeFile(fn) def __showContextMenuDebugger(self): @@ -1364,9 +1483,18 @@ if self.pfile is None: enable = False else: - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn)) - enable = os.path.exists(fn) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}.edj" + ) + enable = self.__remotefsInterface.exists(fn) + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj") + enable = os.path.exists(fn) self.dbgActGrp.findChild( QAction, "project_debugger_properties_load" ).setEnabled(enable) @@ -1392,12 +1520,21 @@ ) return - fn1, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn1)) - if ( - os.path.exists(fn) - and self.__debuggerPropertiesFile.readFile(fn) - ): + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}.edj" + ) + if not self.__remotefsInterface.exists(fn): + return + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj") + if not os.path.exists(fn): + return + if self.__debuggerPropertiesFile.readFile(fn): self.debugPropertiesLoaded = True self.debugPropertiesChanged = False @@ -1419,8 +1556,16 @@ ) return - fn, ext = os.path.splitext(os.path.basename(self.pfile)) - fn = os.path.join(self.getProjectManagementDir(), "{0}.edj".format(fn)) + if FileSystemUtilities.isRemoteFileName(self.pfile): + fn1, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fn1}.edj" + ) + else: + fn1, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fn1}.edj") with EricOverrideCursor(): self.__debuggerPropertiesFile.writeFile(fn) @@ -1437,21 +1582,32 @@ ) return - fname, ext = os.path.splitext(os.path.basename(self.pfile)) - - fn = os.path.join(self.getProjectManagementDir(), f"{fname}.edj") - if os.path.exists(fn): - try: - os.remove(fn) - except OSError: - EricMessageBox.critical( - self.ui, - self.tr("Delete Debugger Properties"), - self.tr( - "<p>The project debugger properties file" - " <b>{0}</b> could not be deleted.</p>" - ).format(fn), + try: + if FileSystemUtilities.isRemoteFileName(self.pfile): + title = self.tr("Delete Remote Debugger Properties") + fname, _ext = self.__remotefsInterface.splitext( + self.__remotefsInterface.basename(self.pfile) + ) + fn = self.__remotefsInterface.join( + self.getProjectManagementDir(), f"{fname}.edj" ) + if self.__remotefsInterface.exists(fn): + self.__remotefsInterface.remove(fn) + else: + title = self.tr("Delete Debugger Properties") + fname, _ext = os.path.splitext(os.path.basename(self.pfile)) + fn = os.path.join(self.getProjectManagementDir(), f"{fname}.edj") + if os.path.exists(fn): + os.remove(fn) + except OSError: + EricMessageBox.critical( + self.ui, + title, + self.tr( + "<p>The project debugger properties file" + " <b>{0}</b> could not be deleted.</p>" + ).format(fn), + ) def __initDebugProperties(self): """ @@ -1672,16 +1828,34 @@ for langFile in self.__pdata["TRANSLATIONS"][:]: qmFile = self.__binaryTranslationFile(langFile) if qmFile: - if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists( - os.path.join(self.ppath, qmFile) - ): - self.appendFile(qmFile) - if tbPath: - qmFile = os.path.join(tbPath, os.path.basename(qmFile)) + if FileSystemUtilities.isRemoteFileName(self.ppath): + if qmFile not in self.__pdata[ + "TRANSLATIONS" + ] and self.__remotefsInterface.exists( + self.__remotefsInterface.join(self.ppath, qmFile) + ): + self.appendFile(qmFile) + if tbPath: + qmFile = self.__remotefsInterface.join( + tbPath, self.__remotefsInterface.basename(qmFile) + ) + if qmFile not in self.__pdata[ + "TRANSLATIONS" + ] and self.__remotefsInterface.exists( + self.__remotefsInterface.join(self.ppath, qmFile) + ): + self.appendFile(qmFile) + else: if qmFile not in self.__pdata["TRANSLATIONS"] and os.path.exists( os.path.join(self.ppath, qmFile) ): self.appendFile(qmFile) + if tbPath: + qmFile = os.path.join(tbPath, os.path.basename(qmFile)) + if qmFile not in self.__pdata[ + "TRANSLATIONS" + ] and os.path.exists(os.path.join(self.ppath, qmFile)): + self.appendFile(qmFile) def removeLanguageFile(self, langFile): """ @@ -1700,12 +1874,18 @@ if qmFile: with contextlib.suppress(ValueError): if self.__pdata["TRANSLATIONSBINPATH"]: - qmFile = self.getRelativePath( - os.path.join( + if FileSystemUtilities.isRemoteFileName(self.ppath): + qmFile = self.__remotefsInterface.join( self.__pdata["TRANSLATIONSBINPATH"], - os.path.basename(qmFile), + self.__remotefsInterface.basename(qmFile), ) - ) + else: + qmFile = self.getRelativePath( + os.path.join( + self.__pdata["TRANSLATIONSBINPATH"], + os.path.basename(qmFile), + ) + ) self.__model.removeItem(qmFile) self.__pdata["TRANSLATIONS"].remove(qmFile) self.setDirty(True) @@ -1721,13 +1901,18 @@ qmFile = self.__binaryTranslationFile(langFile) try: - fn = os.path.join(self.ppath, langFile) - if os.path.exists(fn): - os.remove(fn) + if FileSystemUtilities.isRemoteFileName(self.ppath): + fn = self.__remotefsInterface.join(self.ppath, langFile) + if self.__remotefsInterface.exists(fn): + self.__remotefsInterface.remove(fn) + else: + fn = os.path.join(self.ppath, langFile) + if os.path.exists(fn): + os.remove(fn) except OSError as err: EricMessageBox.critical( self.ui, - self.tr("Delete translation"), + self.tr("Delete Translation"), self.tr( "<p>The selected translation file <b>{0}</b> could not be" " deleted.</p><p>Reason: {1}</p>" @@ -1778,14 +1963,22 @@ # make it relative to the project root, if it starts with that path # assume relative paths are relative to the project root - newfn = self.getRelativePath(fn) if os.path.isabs(fn) else fn - newdir = os.path.dirname(newfn) + if FileSystemUtilities.isRemoteFileName(self.ppath): + newfn = self.getRelativePath(fn) if fn.startswith(self.ppath) else fn + newdir = self.__remotefsInterface.dirname(newfn) + else: + newfn = self.getRelativePath(fn) if os.path.isabs(fn) else fn + newdir = os.path.dirname(newfn) if isSourceFile: filetype = "SOURCES" else: filetype = "OTHERS" - bfn = os.path.basename(newfn) + bfn = ( + self.__remotefsInterface.basename(newfn) + if FileSystemUtilities.isRemoteFileName(self.ppath) + else os.path.basename(newfn) + ) if fnmatch.fnmatch(bfn, "*.ts") or fnmatch.fnmatch(bfn, "*.qm"): filetype = "TRANSLATIONS" else: @@ -1842,6 +2035,7 @@ @param startdir start directory for the selection dialog @type str """ + # TODO: adapt to remote server from .AddFileDialog import AddFileDialog if not startdir: @@ -1906,6 +2100,7 @@ @param quiet flag indicating quiet operations @type bool """ + # TODO: adapt to remote server # get all relevant filename patterns patterns = [] ignorePatterns = [] @@ -1989,6 +2184,7 @@ @param target target directory @type str """ + # TODO: adapt to remote server # first perform the addition of source self.__addSingleDirectory(filetype, source, target, True) @@ -2027,6 +2223,7 @@ @param startdir start directory for the selection dialog @type str """ + # TODO: adapt to remote server from .AddDirectoryDialog import AddDirectoryDialog if not startdir: @@ -2067,6 +2264,7 @@ @param fn file name or directory name to add @type str """ + # TODO: adapt to remote server if fn: # if it is below the project directory, make it relative to that fn = self.getRelativePath(fn) @@ -2112,6 +2310,7 @@ @return flag indicating success @rtype bool """ + # TODO: adapt to remote server fn = self.getRelativePath(oldfn) isSourceFile = fn in self.__pdata["SOURCES"] @@ -2170,6 +2369,7 @@ even if it doesn't have the source extension @type bool """ + # TODO: adapt to remote server fn = self.getRelativePath(oldname) if os.path.dirname(oldname) == os.path.dirname(newname): if self.__isInPdata(oldname): @@ -2192,6 +2392,7 @@ @return list of files starting with a common prefix @rtype list of str """ + # TODO: adapt to remote server filelist = [] start = self.getRelativePath(start) for fileCategory in [ @@ -2461,6 +2662,10 @@ self.reloadAct.setEnabled(True) self.closeAct.setEnabled(True) self.saveasAct.setEnabled(True) + self.saveasRemoteAct.setEnabled( + self.__remoteServer.isServerConnected() + and FileSystemUtilities.isRemoteFileName(self.pfile) + ) self.actGrp2.setEnabled(True) self.propsAct.setEnabled(True) self.userPropsAct.setEnabled(True) @@ -3192,6 +3397,10 @@ self.reloadAct.setEnabled(True) self.closeAct.setEnabled(True) self.saveasAct.setEnabled(True) + self.saveasRemoteAct.setEnabled( + self.__remoteServer.isServerConnected() + and FileSystemUtilities.isRemoteFileName(self.pfile) + ) self.actGrp2.setEnabled(True) self.propsAct.setEnabled(True) self.userPropsAct.setEnabled(True) @@ -3331,7 +3540,7 @@ if fpath.exists(): res = EricMessageBox.yesNo( self.ui, - self.tr("Save File"), + self.tr("Save Project"), self.tr( """<p>The file <b>{0}</b> already exists.""" """ Overwrite it?</p>""" @@ -3341,7 +3550,6 @@ if not res: return False - self.name = fpath.stem ok = self.__writeProject(str(fpath)) if ok: @@ -3476,6 +3684,7 @@ self.__initData() self.reloadAct.setEnabled(False) self.closeAct.setEnabled(False) + self.saveasRemoteAct.setEnabled(False) self.saveasAct.setEnabled(False) self.saveAct.setEnabled(False) self.actGrp2.setEnabled(False) @@ -3792,8 +4001,7 @@ @rtype str """ if self.pfile: - name = os.path.splitext(self.pfile)[0] - return os.path.basename(name) + return self.name else: return "" @@ -3804,7 +4012,10 @@ @return path of the management directory @rtype str """ - return os.path.join(self.ppath, ".eric7project") + if FileSystemUtilities.isRemoteFileName(self.ppath): + return self.__remotefsInterface.join(self.ppath, ".eric7project") + else: + return os.path.join(self.ppath, ".eric7project") def createProjectManagementDir(self): """ @@ -3814,8 +4025,10 @@ """ # create management directory if not present mgmtDir = self.getProjectManagementDir() - if not os.path.exists(mgmtDir): - os.makedirs(mgmtDir) + if FileSystemUtilities.isRemoteFileName(mgmtDir): + self.__remotefsInterface.makedirs(mgmtDir, exist_ok=True) + else: + os.makedirs(mgmtDir, exist_ok=True) def getHash(self): """ @@ -3841,7 +4054,13 @@ return "" try: - return str(pathlib.Path(path).relative_to(self.ppath)) + if FileSystemUtilities.isRemoteFileName(self.ppath): + if self.__remotefsInterface.separator() == "\\": + return str(pathlib.PureWindowsPath(path).relative_to(self.ppath)) + else: + return str(pathlib.PurePosixPath(path).relative_to(self.ppath)) + else: + return str(pathlib.PurePath(path).relative_to(self.ppath)) except ValueError: return path @@ -3856,7 +4075,12 @@ belong to the project @rtype str """ - return FileSystemUtilities.fromNativeSeparators(self.getRelativePath(path)) + if FileSystemUtilities.isRemoteFileName(self.ppath): + return self.__remotefsInterface.fromNativeSeparators( + self.getRelativePath(path) + ) + else: + return FileSystemUtilities.fromNativeSeparators(self.getRelativePath(path)) def getAbsolutePath(self, fn): """ @@ -3868,8 +4092,11 @@ @return absolute path @rtype str """ - if not os.path.isabs(fn): - fn = os.path.join(self.ppath, fn) + if not fn.startswith(self.ppath): + if FileSystemUtilities.isRemoteFileName(self.ppath): + fn = self.__remotefsInterface.join(self.ppath, fn) + else: + fn = os.path.join(self.ppath, fn) return fn def getAbsoluteUniversalPath(self, fn): @@ -3882,8 +4109,15 @@ @return absolute path @rtype str """ - if not os.path.isabs(fn): - fn = os.path.join(self.ppath, FileSystemUtilities.toNativeSeparators(fn)) + if not fn.startswith(self.ppath): + if FileSystemUtilities.isRemoteFileName(self.ppath): + fn = self.__remotefsInterface.join( + self.ppath, self.__remotefsInterface.fromNativeSeparators(fn) + ) + else: + fn = os.path.join( + self.ppath, FileSystemUtilities.fromNativeSeparators(fn) + ) return fn def getEolString(self): @@ -4106,7 +4340,7 @@ @return flag indicating membership @rtype bool """ - newfn = os.path.abspath(fn) + newfn = fn if FileSystemUtilities.isRemoteFileName(fn) else os.path.abspath(fn) newfn = self.getRelativePath(newfn) if newfn in self.__pdata[group] or ( group == "OTHERS" @@ -4192,6 +4426,25 @@ act.triggered.connect(self.openProject) self.actions.append(act) + self.openRemoteAct = EricAction( + self.tr("Open remote project"), + EricPixmapCache.getIcon("projectOpen-remote"), + self.tr("Open (Remote)..."), + 0, + 0, + self.actGrp1, + "project_open_remote", + ) + self.openRemoteAct.setStatusTip(self.tr("Open an existing remote project")) + self.openRemoteAct.setWhatsThis( + self.tr( + "<b>Open (Remote)...</b><p>This opens an existing remote project.</p>" + ) + ) + self.openRemoteAct.triggered.connect(self.__openRemoteProject) + self.actions.append(self.openRemoteAct) + self.openRemoteAct.setEnabled(False) # server is not connected initially + self.reloadAct = EricAction( self.tr("Reload project"), EricPixmapCache.getIcon("projectReload"), @@ -4259,6 +4512,28 @@ self.saveasAct.triggered.connect(self.saveProjectAs) self.actions.append(self.saveasAct) + self.saveasRemoteAct = EricAction( + self.tr("Save project as (Remote)"), + EricPixmapCache.getIcon("projectSaveAs-remote"), + self.tr("Save as (Remote)..."), + 0, + 0, + self, + "project_save_as_remote", + ) + self.saveasRemoteAct.setStatusTip( + self.tr("Save the current project to a new remote file") + ) + self.saveasRemoteAct.setWhatsThis( + self.tr( + """<b>Save as (Remote)</b>""" + """<p>This saves the current project to a new remote file.</p>""" + ) + ) + self.saveasRemoteAct.triggered.connect(self.__saveRemoteProjectAs) + self.actions.append(self.saveasRemoteAct) + self.saveasRemoteAct.setEnabled(False) # server is not connected initially + ################################################################### ## Project management actions ################################################################### @@ -5345,6 +5620,7 @@ menu.addSeparator() menu.addAction(self.saveAct) menu.addAction(self.saveasAct) + menu.addAction(self.saveasRemoteAct) menu.addSeparator() menu.addActions(self.actGrp2.actions()) menu.addSeparator() @@ -5417,6 +5693,7 @@ tb.addSeparator() tb.addAction(self.saveAct) tb.addAction(self.saveasAct) + tb.addAction(self.saveasRemoteAct) toolbarManager.addToolBar(tb, tb.windowTitle()) toolbarManager.addAction(self.addFilesAct, tb.windowTitle()) @@ -7159,6 +7436,90 @@ # the configuration file does not exist or is invalid JSON self.__initVenvConfiguration() + ############################################################################# + ## Below are methods implementing the support for 'eric-ide server projects + ############################################################################# + + @pyqtSlot(bool) + def remoteConnectionChanged(self, connected): + """ + Public slot to handle a change of the 'eric-ide' server connection state. + + @param connected flag indicating the connection state + @type bool + """ + self.openRemoteAct.setEnabled(connected) + self.saveasRemoteAct.setEnabled( + connected + and self.opened + and FileSystemUtilities.isRemoteFileName(self.pfile) + ) + + @pyqtSlot() + def __openRemoteProject(self): + """ + Private slot to open a project of an 'eric-ide' server. + """ + fn = EricServerFileDialog.getOpenFileName( + self.parent(), + self.tr("Open Remote Project"), + "", + self.tr("Project Files (*.epj)"), + ) + if fn: + self.openProject(fn=fn) + + @pyqtSlot() + def __saveRemoteProjectAs(self): + """ + Private slot to save the current remote project to different remote file. + """ + defaultFilter = self.tr("Project Files (*.epj)") + defaultPath = self.ppath if self.ppath else "" + fn, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter( + self.parent(), + self.tr("Save Remote Project"), + defaultPath, + self.tr("Project Files (*.epj)"), + defaultFilter, + EricFileDialog.DontConfirmOverwrite, + ) + + if fn: + fname, ext = self.__remotefsInterface.splitext(fn) + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fn = f"{fname}{ex}" + if self.__remotefsInterface.exists(fn): + res = EricMessageBox.yesNo( + self.ui, + self.tr("Save Remote Project"), + self.tr( + """<p>The file <b>{0}</b> already exists.""" + """ Overwrite it?</p>""" + ).format(fn), + icon=EricMessageBox.Warning, + ) + if not res: + return + + ok = self.__writeProject(fn) + + if ok: + # create management directory if not present + self.createProjectManagementDir() + + # now save the tasks + self.writeTasks() + + self.sessActGrp.setEnabled(ok) + self.menuSessionAct.setEnabled(ok) + self.projectClosedHooks.emit() + self.projectClosed.emit(False) + self.projectOpenedHooks.emit() + self.projectOpened.emit() + # # eflag: noqa = M601
--- a/src/eric7/Project/ProjectBrowserModel.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Project/ProjectBrowserModel.py Fri Feb 23 10:46:46 2024 +0100 @@ -138,7 +138,9 @@ Class implementing the data structure for project browser directory items. """ - def __init__(self, parent, dinfo, projectType, full=True, bold=False): + def __init__( + self, parent, dinfo, projectType, full=True, bold=False, fsInterface=None + ): """ Constructor @@ -152,8 +154,11 @@ @type bool @param bold flag indicating a highlighted font @type bool + @param fsInterface reference to the 'eric-ide' server file system interface + (defaults to None) + @type EricServerFileSystemInterface (optional) """ - BrowserDirectoryItem.__init__(self, parent, dinfo, full) + BrowserDirectoryItem.__init__(self, parent, dinfo, full, fsInterface) ProjectBrowserItemMixin.__init__(self, projectType, bold) self.type_ = BrowserItemType.PbDirectory @@ -165,7 +170,14 @@ """ def __init__( - self, parent, finfo, projectType, full=True, bold=False, sourceLanguage="" + self, + parent, + finfo, + projectType, + full=True, + bold=False, + sourceLanguage="", + fsInterface=None, ): """ Constructor @@ -182,8 +194,11 @@ @type bool @param sourceLanguage source code language of the project @type str + @param fsInterface reference to the 'eric-ide' server file system interface + (defaults to None) + @type EricServerFileSystemInterface (optional) """ - BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage) + BrowserFileItem.__init__(self, parent, finfo, full, sourceLanguage, fsInterface) ProjectBrowserItemMixin.__init__(self, projectType, bold) self.type_ = BrowserItemType.PbFile @@ -198,12 +213,15 @@ vcsStateChanged = pyqtSignal(str) - def __init__(self, parent): + def __init__(self, parent, fsInterface=None): """ Constructor @param parent reference to parent object @type Project.Project + @param fsInterface reference to the 'eric-ide' server interface object + (defaults to None) + @type EricServerFileSystemInterface (optional) """ super().__init__(parent, nopopulate=True) @@ -215,6 +233,8 @@ self.project = parent self.__projectBrowser = None + self.__remotefsInterface = fsInterface + self.watchedItems = {} self.__watcherActive = True self.watcher = QFileSystemWatcher(self) @@ -418,7 +438,16 @@ ) for fn in self.project.getProjectData(dataKey=fileCategory): - fname = os.path.join(self.project.ppath, fn) + fname = ( + self.__remotefsInterface.join(self.project.ppath, fn) + if FileSystemUtilities.isRemoteFileName(self.project.ppath) + else os.path.join(self.project.ppath, fn) + ) + isdir = ( + self.__remotefsInterface.isdir(fname) + if FileSystemUtilities.isRemoteFileName(fname) + else os.path.isdir(fname) + ) parentItem, dt = self.findParentItemByName( self.__projectBrowser.getProjectBrowserFilter(fileCategory), fn ) @@ -429,8 +458,9 @@ self.__projectBrowser.getProjectBrowserFilter(fileCategory), False, bold, + fsInterface=self.__remotefsInterface, ) - if os.path.isdir(fname) + if isdir else ProjectBrowserFileItem( parentItem, fname, @@ -438,6 +468,7 @@ False, bold, sourceLanguage=sourceLanguage, + fsInterface=self.__remotefsInterface, ) ) self._addItem(itm, parentItem)
--- a/src/eric7/Project/ProjectFile.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Project/ProjectFile.py Fri Feb 23 10:46:46 2024 +0100 @@ -17,6 +17,7 @@ from eric7 import Preferences from eric7.EricGui.EricOverrideCursor import EricOverridenCursor from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp from eric7.SystemUtilities import FileSystemUtilities Project = typing.TypeVar("Project") @@ -48,6 +49,11 @@ @return flag indicating a successful write @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + isRemote = FileSystemUtilities.isRemoteFileName(filename) + projectDict = { "header": { "comment": "eric project file for project {0}".format( @@ -85,19 +91,28 @@ "SOURCESDIR", ): with contextlib.suppress(KeyError): - projectDict["project"][key] = FileSystemUtilities.fromNativeSeparators( - projectDict["project"][key] + projectDict["project"][key] = ( + fsInterface.fromNativeSeparators(projectDict["project"][key]) + if isRemote + else FileSystemUtilities.fromNativeSeparators( + projectDict["project"][key] + ) ) try: jsonString = json.dumps(projectDict, indent=2, sort_keys=True) + "\n" - with open(filename, "w", newline="") as f: - f.write(jsonString) + if isRemote: + title = self.tr("Save Remote Project File") + fsInterface.writeFile(filename, jsonString.encode("utf-8")) + else: + title = self.tr("Save Project File") + with open(filename, "w", newline="") as f: + f.write(jsonString) except (OSError, TypeError) as err: with EricOverridenCursor(): EricMessageBox.critical( None, - self.tr("Save Project File"), + title, self.tr( "<p>The project file <b>{0}</b> could not be " "written.</p><p>Reason: {1}</p>" @@ -116,14 +131,24 @@ @return flag indicating a successful read @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + + isRemote = FileSystemUtilities.isRemoteFileName(filename) try: - with open(filename, "r") as f: - jsonString = f.read() + if isRemote: + title = self.tr("Read Remote Project File") + jsonString = fsInterface.readFile(filename).decode("utf-8") + else: + title = self.tr("Read Project File") + with open(filename, "r") as f: + jsonString = f.read() projectDict = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.critical( None, - self.tr("Read Project File"), + title, self.tr( "<p>The project file <b>{0}</b> could not be " "read.</p><p>Reason: {1}</p>" @@ -134,10 +159,17 @@ # modify paths to contain native separators for key in self.__project.getFileCategories() + ["TRANSLATIONEXCEPTIONS"]: with contextlib.suppress(KeyError): - projectDict["project"][key] = [ - FileSystemUtilities.toNativeSeparators(f) - for f in projectDict["project"][key] - ] + projectDict["project"][key] = ( + [ + fsInterface.toNativeSeparators(f) + for f in projectDict["project"][key] + ] + if isRemote + else [ + FileSystemUtilities.toNativeSeparators(f) + for f in projectDict["project"][key] + ] + ) for key in ( "SPELLWORDS", "SPELLEXCLUDES", @@ -148,8 +180,12 @@ "SOURCESDIR", ): with contextlib.suppress(KeyError): - projectDict["project"][key] = FileSystemUtilities.toNativeSeparators( - projectDict["project"][key] + projectDict["project"][key] = ( + fsInterface.toNativeSeparators(projectDict["project"][key]) + if isRemote + else FileSystemUtilities.toNativeSeparators( + projectDict["project"][key] + ) ) self.__project.setProjectData(projectDict["project"])
--- a/src/eric7/Project/UserProjectFile.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Project/UserProjectFile.py Fri Feb 23 10:46:46 2024 +0100 @@ -16,6 +16,8 @@ from eric7 import Preferences from eric7.EricGui.EricOverrideCursor import EricOverridenCursor from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities Project = typing.TypeVar("Project") @@ -47,6 +49,10 @@ @return flag indicating a successful write @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + userProjectDict = { "header": { "comment": "eric user project file for project {0}".format( @@ -62,13 +68,18 @@ try: jsonString = json.dumps(userProjectDict, indent=2) + "\n" - with open(filename, "w") as f: - f.write(jsonString) + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Save Remote User Project Properties") + fsInterface.writeFile(filename, jsonString.encode("utf-8")) + else: + title = self.tr("Save User Project Properties") + with open(filename, "w") as f: + f.write(jsonString) except (OSError, TypeError) as err: with EricOverridenCursor(): EricMessageBox.critical( None, - self.tr("Save User Project Properties"), + title, self.tr( "<p>The user specific project properties file" " <b>{0}</b> could not be written.</p>" @@ -89,14 +100,23 @@ @return flag indicating a successful read @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + try: - with open(filename, "r") as f: - jsonString = f.read() + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Read Remote User Project Properties") + jsonString = fsInterface.readFile(filename).decode("utf-8") + else: + title = self.tr("Read User Project Properties") + with open(filename, "r") as f: + jsonString = f.read() userProjectDict = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.critical( None, - self.tr("Read User Project Properties"), + title, self.tr( "<p>The user specific project properties file <b>{0}</b>" " could not be read.</p><p>Reason: {1}</p>"
--- a/src/eric7/QScintilla/Editor.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/QScintilla/Editor.py Fri Feb 23 10:46:46 2024 +0100 @@ -67,6 +67,7 @@ from eric7.EricWidgets import EricFileDialog, EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.Globals import recentNameBreakpointConditions +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.SystemUtilities import FileSystemUtilities, OSUtilities, PythonUtilities from eric7.UI import PythonDisViewer from eric7.Utilities import MouseUtilities @@ -264,6 +265,10 @@ self.project = ericApp().getObject("Project") self.setFileName(fn) + self.__remotefsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + # clear some variables self.lastHighlight = None # remember the last highlighted line self.lastErrorMarker = None # remember the last error line @@ -478,7 +483,10 @@ if not res: raise OSError() - self.readFile(self.fileName, True) + self.readFile(self.fileName, createIt=True) + + elif FileSystemUtilities.isRemoteFileName(self.fileName): + self.readFile(self.fileName, createIt=True, isRemote=True) self.__bindLexer(self.fileName) self.__bindCompleter(self.fileName) @@ -1050,6 +1058,11 @@ self.tr("Save As..."), self.__contextSaveAs, ) + self.menuActs["SaveAsRemote"] = self.menu.addAction( + EricPixmapCache.getIcon("fileSaveAsRemote"), + self.tr("Save as (Remote)..."), + self.__contextSaveAsRemote, + ) self.menu.addAction( EricPixmapCache.getIcon("fileSaveCopy"), self.tr("Save Copy..."), @@ -1177,7 +1190,9 @@ """ menu = QMenu(self.tr("Show")) - menu.addAction(self.tr("Code metrics..."), self.__showCodeMetrics) + self.codeMetricsAct = menu.addAction( + self.tr("Code metrics..."), self.__showCodeMetrics + ) self.coverageMenuAct = menu.addAction( self.tr("Code coverage..."), self.__showCodeCoverage ) @@ -2156,8 +2171,8 @@ @param m modification status @type bool """ - if not m and bool(self.fileName) and pathlib.Path(self.fileName).exists(): - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime + if not m: + self.recordModificationTime() self.modificationStatusChanged.emit(m, self) self.undoAvailable.emit(self.isUndoAvailable()) self.redoAvailable.emit(self.isRedoAvailable()) @@ -3455,9 +3470,7 @@ self, self.tr("File Modified"), self.tr("<p>The file <b>{0}</b> has unsaved changes.</p>").format(fn), - self.saveFile - if not FileSystemUtilities.isRemoteFileName(self.fileName) - else None, + self.saveFile, ) if res: self.vm.setEditorName(self, self.fileName) @@ -3485,7 +3498,7 @@ break # Couldn't find the unmodified state - def readFile(self, fn, createIt=False, encoding="", noempty=False): + def readFile(self, fn, createIt=False, encoding="", noempty=False, isRemote=False): """ Public method to read the text from a file. @@ -3499,26 +3512,46 @@ @type str (optional) @param noempty flag indicating to not set an empty text (defaults to False) @type bool (optional) - """ - self.__loadEditorConfig(fileName=fn) + @param isRemote flag indicating a remote file (defaults to False) + @type bool (optional) + """ + if FileSystemUtilities.isPlainFileName(fn): + self.__loadEditorConfig(fileName=fn) try: with EricOverrideCursor(): - if createIt and not os.path.exists(fn): - with open(fn, "w"): - pass - if encoding == "": - encoding = self.__getEditorConfig("DefaultEncoding", nodefault=True) - if encoding: - txt, self.encoding = Utilities.readEncodedFileWithEncoding( - fn, encoding - ) + if FileSystemUtilities.isRemoteFileName(fn) or isRemote: + title = self.tr("Open Remote File") + if encoding: + ( + txt, + self.encoding, + ) = self.__remotefsInterface.readEncodedFileWithEncoding( + fn, encoding, create=True + ) + else: + txt, self.encoding = self.__remotefsInterface.readEncodedFile( + fn, create=True + ) else: - txt, self.encoding = Utilities.readEncodedFile(fn) + title = self.tr("Open File") + if createIt and not os.path.exists(fn): + with open(fn, "w"): + pass + if encoding == "": + encoding = self.__getEditorConfig( + "DefaultEncoding", nodefault=True + ) + if encoding: + txt, self.encoding = Utilities.readEncodedFileWithEncoding( + fn, encoding + ) + else: + txt, self.encoding = Utilities.readEncodedFile(fn) except (OSError, UnicodeDecodeError) as why: EricMessageBox.critical( self.vm, - self.tr("Open File"), + title, self.tr( "<p>The file <b>{0}</b> could not be opened.</p>" "<p>Reason: {1}</p>" @@ -3548,7 +3581,7 @@ self.extractTasks() - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime(filename=fn) @pyqtSlot() def __convertTabs(self): @@ -3600,7 +3633,11 @@ @return flag indicating success @rtype bool """ - config = self.__loadEditorConfigObject(fn) + config = ( + None + if self.fileName and fn == self.fileName + else self.__loadEditorConfigObject(fn) + ) eol = self.__getEditorConfig("EOLMode", nodefault=True, config=config) if eol is not None: @@ -3622,7 +3659,7 @@ # create a backup file, if the option is set createBackup = backup and Preferences.getEditor("CreateBackupFile") - if createBackup: + if createBackup and FileSystemUtilities.isPlainFileName(fn): if os.path.islink(fn): fn = os.path.realpath(fn) bfn = "{0}~".format(fn) @@ -3642,28 +3679,41 @@ editorConfigEncoding = self.__getEditorConfig( "DefaultEncoding", nodefault=True, config=config ) - self.encoding = Utilities.writeEncodedFile( - fn, txt, self.encoding, forcedEncoding=editorConfigEncoding - ) - if createBackup and perms_valid: - os.chmod(fn, permissions) + if FileSystemUtilities.isPlainFileName(fn): + title = self.tr("Save File") + self.encoding = Utilities.writeEncodedFile( + fn, txt, self.encoding, forcedEncoding=editorConfigEncoding + ) + if createBackup and perms_valid: + os.chmod(fn, permissions) + else: + title = self.tr("Save Remote File") + self.encoding = self.__remotefsInterface.writeEncodedFile( + fn, + txt, + self.encoding, + forcedEncoding=editorConfigEncoding, + createBackup=createBackup, + ) return True except (OSError, UnicodeError, Utilities.CodingError) as why: EricMessageBox.critical( self, - self.tr("Save File"), + title, self.tr( "<p>The file <b>{0}</b> could not be saved.<br/>Reason: {1}</p>" ).format(fn, str(why)), ) return False - def __getSaveFileName(self, path=None): + def __getSaveFileName(self, path=None, remote=False): """ Private method to get the name of the file to be saved. - @param path directory to save the file in - @type str + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return file name @rtype str """ @@ -3677,7 +3727,12 @@ if not path and self.fileName: path = os.path.dirname(self.fileName) if not path: - path = Preferences.getMultiProject("Workspace") or OSUtilities.getHomeDir() + if remote: + path = "" + else: + path = ( + Preferences.getMultiProject("Workspace") or OSUtilities.getHomeDir() + ) if self.fileName: filterPattern = "(*{0})".format(os.path.splitext(self.fileName)[1]) @@ -3689,14 +3744,26 @@ defaultFilter = Preferences.getEditor("DefaultSaveFilter") else: defaultFilter = Preferences.getEditor("DefaultSaveFilter") - fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( - self, - self.tr("Save File"), - path, - Lexers.getSaveFileFiltersList(True, True), - defaultFilter, - EricFileDialog.DontConfirmOverwrite, - ) + + if remote or FileSystemUtilities.isRemoteFileName(path): + title = self.tr("Save Remote File") + fn, selectedFilter = EricServerFileDialog.getSaveFileNameAndFilter( + self, + title, + path, + Lexers.getSaveFileFiltersList(True, True), + defaultFilter, + ) + else: + title = self.tr("Save File") + fn, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( + self, + title, + path, + Lexers.getSaveFileFiltersList(True, True), + defaultFilter, + EricFileDialog.DontConfirmOverwrite, + ) if fn: if fn.endswith("."): @@ -3707,10 +3774,13 @@ ex = selectedFilter.split("(*")[1].split(")")[0] if ex: fpath = fpath.with_suffix(ex) - if fpath.exists(): + if ( + FileSystemUtilities.isRemoteFileName(str(fpath)) + and self.__remotefsInterface.exists(str(fpath)) + ) or (FileSystemUtilities.isPlainFileName(str(fpath)) and fpath.exists()): res = EricMessageBox.yesNo( self, - self.tr("Save File"), + title, self.tr( "<p>The file <b>{0}</b> already exists. Overwrite it?</p>" ).format(fpath), @@ -3743,21 +3813,21 @@ return res - def saveFile(self, saveas=False, path=None): + def saveFile(self, saveas=False, path=None, remote=False): """ Public method to save the text to a file. - @param saveas flag indicating a 'save as' action - @type bool - @param path directory to save the file in - @type str + @param saveas flag indicating a 'save as' action (defaults to False) + @type bool (optional) + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return flag indicating success @rtype bool """ - if not saveas and ( - not self.isModified() or FileSystemUtilities.isRemoteFileName(self.fileName) - ): - # do nothing if text wasn't changed or is a remote file + if not saveas and not self.isModified(): + # do nothing if text was not changed return False if FileSystemUtilities.isDeviceFileName(self.fileName): @@ -3767,7 +3837,7 @@ if saveas or self.fileName == "": saveas = True - fn = self.__getSaveFileName(path) + fn = self.__getSaveFileName(path=path, remote=remote) if not fn: return False @@ -3786,13 +3856,13 @@ else: fn = self.fileName - self.__loadEditorConfig(fn) self.editorAboutToBeSaved.emit(self.fileName) if self.__autosaveTimer.isActive(): self.__autosaveTimer.stop() if self.writeFile(fn): if saveas: self.__clearBreakpoints(self.fileName) + self.__loadEditorConfig(fileName=fn) self.setFileName(fn) self.setModified(False) self.setReadOnly(False) @@ -3814,7 +3884,7 @@ self.setLanguage(self.fileName) - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime() if newName is not None: self.vm.addToRecentList(newName) self.editorSaved.emit(self.fileName) @@ -3824,21 +3894,21 @@ self.__checkEncoding() return True else: - self.lastModified = ( - pathlib.Path(fn).stat().st_mtime if pathlib.Path(fn).exists() else 0 - ) + self.recordModificationTime(filename=fn) return False - def saveFileAs(self, path=None): + def saveFileAs(self, path=None, remote=False): """ Public method to save a file with a new name. - @param path directory to save the file in - @type str + @param path directory to save the file in (defaults to None) + @type str (optional) + @param remote flag indicating to save as a remote file (defaults to False) + @type bool (optional) @return tuple containing a success indicator and the name of the saved file @rtype tuple of (bool, str) """ - return self.saveFile(True, path) + return self.saveFile(True, path=path, remote=remote) def __saveDeviceFile(self, saveas=False): """ @@ -3899,7 +3969,7 @@ if self.lexer_ is None: self.setLanguage(self.fileName) - self.lastModified = pathlib.Path(fn).stat().st_mtime + self.recordModificationTime() self.vm.setEditorName(self, self.fileName) self.__updateReadOnly(True) @@ -6329,6 +6399,9 @@ not self.isModified() and bool(self.fileName) ) self.menuActs["Save"].setEnabled(self.isModified()) + self.menuActs["SaveAsRemote"].setEnabled( + ericApp().getObject("EricServer").isServerConnected() + ) self.menuActs["Undo"].setEnabled(self.isUndoAvailable()) self.menuActs["Redo"].setEnabled(self.isRedoAvailable()) self.menuActs["Revert"].setEnabled(self.isModified()) @@ -6346,6 +6419,9 @@ self.menuActs["Diagrams"].setEnabled(True) else: self.menuActs["Diagrams"].setEnabled(False) + self.menuActs["Formatting"].setEnabled( + not FileSystemUtilities.isRemoteFileName(self.fileName) + ) if not self.miniMenu: if self.lexer_ is not None: self.menuActs["Comment"].setEnabled(self.lexer_.canBlockComment()) @@ -6437,6 +6513,7 @@ ) self.coverageHideAnnotationMenuAct.setEnabled(len(self.notcoveredMarkers) > 0) + # TODO: disable action in Radon plugin for server files self.showMenu.emit("Show", self.menuShow, self) @pyqtSlot() @@ -6607,6 +6684,15 @@ self.vm.setEditorName(self, self.fileName) @pyqtSlot() + def __contextSaveAsRemote(self): + """ + Private slot handling the save as (remote) context menu entry. + """ + ok = self.saveFileAs(remote=True) + if ok: + self.vm.setEditorName(self, self.fileName) + + @pyqtSlot() def __contextSaveCopy(self): """ Private slot handling the save copy context menu entry. @@ -6962,9 +7048,26 @@ self.__coverageFile = fn if fn: - cover = Coverage(data_file=fn) - cover.load() - missing = cover.analysis2(self.fileName)[3] + if FileSystemUtilities.isRemoteFileName(fn): + coverageInterface = ( + ericApp().getObject("EricServer").getServiceInterface("Coverage") + ) + ok, error = coverageInterface.loadCoverageData(fn) + if not ok and not silent: + EricMessageBox.critical( + self, + self.tr("Load Coverage Data"), + self.tr( + "<p>The coverage data could not be loaded from file" + " <b>{0}</b>.</p><p>Reason: {1}</p>" + ).format(self.cfn, error), + ) + return + missing = coverageInterface.analyzeFile(self.fileName)[3] + else: + cover = Coverage(data_file=fn) + cover.load() + missing = cover.analysis2(self.fileName)[3] if missing: for line in missing: handle = self.markerAdd(line - 1, self.notcovered) @@ -8008,9 +8111,19 @@ with contextlib.suppress(AttributeError): self.setCaretWidth(self.caretWidth) - self.__updateReadOnly(False) + if not self.dbs.isDebugging: + self.__updateReadOnly(False) self.setCursorFlashTime(QApplication.cursorFlashTime()) + if ( + self.fileName + and FileSystemUtilities.isRemoteFileName(self.fileName) + and not self.inReopenPrompt + ): + self.inReopenPrompt = True + self.checkRereadFile() + self.inReopenPrompt = False + super().focusInEvent(event) def focusOutEvent(self, event): @@ -8020,7 +8133,11 @@ @param event the event object @type QFocusEvent """ - if Preferences.getEditor("AutosaveOnFocusLost") and self.__shouldAutosave(): + if ( + Preferences.getEditor("AutosaveOnFocusLost") + and self.__shouldAutosave() + and not self.inReopenPrompt + ): self.saveFile() self.vm.editorActGrp.setEnabled(False) @@ -8193,9 +8310,7 @@ signal if there was an attribute change. @type bool """ - if self.fileName == "" or not FileSystemUtilities.isPlainFileName( - self.fileName - ): + if self.fileName == "" or FileSystemUtilities.isDeviceFileName(self.fileName): return readOnly = self.checkReadOnly() @@ -8217,9 +8332,18 @@ @rtype bool """ return ( - FileSystemUtilities.isPlainFileName(self.fileName) - and not os.access(self.fileName, os.W_OK) - ) or self.isReadOnly() + ( + FileSystemUtilities.isPlainFileName(self.fileName) + and not os.access(self.fileName, os.W_OK) + ) + or ( + FileSystemUtilities.isRemoteFileName(self.fileName) + and not self.__remotefsInterface.access( + FileSystemUtilities.plainFileName(self.fileName), "write" + ) + ) + or self.isReadOnly() + ) @pyqtSlot() def checkRereadFile(self): @@ -8227,13 +8351,9 @@ Public slot to check, if the file needs to be re-read, and refresh it if needed. """ - if ( - self.fileName - and pathlib.Path(self.fileName).exists() - and pathlib.Path(self.fileName).stat().st_mtime != self.lastModified - ): + if self.checkModificationTime(): if Preferences.getEditor("AutoReopen") and not self.isModified(): - self.refresh() + self.__refresh() else: msg = self.tr( """<p>The file <b>{0}</b> has been changed while it""" @@ -8254,23 +8374,79 @@ yesDefault=yesDefault, ) if res: - self.refresh() + self.__refresh() else: # do not prompt for this change again... - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime - - @pyqtSlot() - def recordModificationTime(self): + self.recordModificationTime() + + @pyqtSlot() + def recordModificationTime(self, filename=""): """ Public slot to record the modification time of our file. - """ - if self.fileName and pathlib.Path(self.fileName).exists(): - self.lastModified = pathlib.Path(self.fileName).stat().st_mtime - - @pyqtSlot() - def refresh(self): - """ - Public slot to refresh the editor contents. + + @param filename name of the file to record the modification tome for + (defaults to "") + @type str (optional) + """ + if not filename: + filename = self.fileName + + if filename: + if FileSystemUtilities.isRemoteFileName(filename): + filename = FileSystemUtilities.plainFileName(filename) + if self.__remotefsInterface.exists(filename): + mtime = self.__remotefsInterface.stat( + FileSystemUtilities.plainFileName(filename), ["st_mtime"] + )["st_mtime"] + self.lastModified = mtime if mtime is not None else 0 + else: + self.lastModified = 0 + elif pathlib.Path(filename).exists(): + self.lastModified = pathlib.Path(filename).stat().st_mtime + else: + self.lastModified = 0 + + else: + self.lastModified = 0 + + def checkModificationTime(self, filename=""): + """ + Public method to check, if the modification time of the file is different + from the recorded one. + + @param filename name of the file to check against (defaults to "") + @type str (optional) + @return flag indicating that the file modification time is different. For + non-existent files a 'False' value will be reported. + @rtype bool + """ + if not filename: + filename = self.fileName + + if filename: + if ( + FileSystemUtilities.isRemoteFileName(filename) + and not self.dbs.isDebugging + ): + plainFilename = FileSystemUtilities.plainFileName(filename) + if self.__remotefsInterface.exists(plainFilename): + mtime = self.__remotefsInterface.stat(plainFilename, ["st_mtime"])[ + "st_mtime" + ] + return mtime != self.lastModified + + elif ( + FileSystemUtilities.isPlainFileName(filename) + and pathlib.Path(filename).exists() + ): + return pathlib.Path(filename).stat().st_mtime != self.lastModified + + return False + + @pyqtSlot() + def __refresh(self): + """ + Private slot to refresh the editor contents. """ # save cursor position cline, cindex = self.getCursorPosition() @@ -8290,11 +8466,6 @@ self.markerDeleteHandle(handle) self.breaks.clear() - if not os.path.exists(self.fileName): - # close the file, if it was deleted in the background - self.close() - return - # reread the file try: self.readFile(self.fileName, noempty=True) @@ -8569,11 +8740,18 @@ if not self.checkDirty(): return - package = ( - os.path.isdir(self.fileName) - and self.fileName - or os.path.dirname(self.fileName) - ) + if FileSystemUtilities.isRemoteFileName(self.fileName): # noqa: Y108 + package = ( + self.fileName + if self.__remotefsInterface.isdir(self.fileName) + else self.__remotefsInterface.dirname(self.fileName) + ) + else: + package = ( + self.fileName + if os.path.isdir(self.fileName) + else os.path.dirname(self.fileName) + ) res = EricMessageBox.yesNo( self, self.tr("Package Diagram"), @@ -9695,7 +9873,7 @@ """ editorConfig = {} - if fileName and FileSystemUtilities.isPlainFileName(self.fileName): + if fileName and FileSystemUtilities.isPlainFileName(fileName): try: editorConfig = editorconfig.get_properties(fileName) except editorconfig.EditorConfigError: @@ -9733,16 +9911,6 @@ if config is None: config = self.__editorConfig - if not config: - if nodefault: - return None - else: - value = self.__getOverrideValue(option) - if value is None: - # no override - value = Preferences.getEditor(option) - return value - try: if option == "EOLMode": value = config["end_of_line"]
--- a/src/eric7/QScintilla/Shell.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/QScintilla/Shell.py Fri Feb 23 10:46:46 2024 +0100 @@ -391,10 +391,16 @@ self.lmenu.clear() venvManager = ericApp().getObject("VirtualEnvManager") for venvName in sorted(venvManager.getVirtualenvNames()): - self.lmenu.addAction(venvName) + act = self.lmenu.addAction(venvName) + act.setData(venvName) if self.__project.isOpen(): self.lmenu.addSeparator() - self.lmenu.addAction(self.tr("Project")) + act = self.lmenu.addAction(self.tr("Project")) + act.setData("<<project>>") + if ericApp().getObject("EricServer").isServerConnected(): + self.lmenu.addSeparator() + act = self.lmenu.addAction(self.tr("eric-ide Server")) + act.setData("<<eric-server>>") def __resizeLinenoMargin(self): """ @@ -2153,13 +2159,15 @@ @param action context menu action that was triggered @type QAction """ - venvName = action.text() - if venvName == self.tr("Project"): + venvName = action.data() + if venvName == "<<project>>": if self.__project.isOpen(): self.__currentWorkingDirectory = self.__project.getProjectPath() self.dbs.startClient( False, forProject=True, workingDir=self.__currentWorkingDirectory ) + elif venvName == "<<eric-server>>": + self.dbs.startClient(False, startRemote=True) else: self.dbs.startClient(False, venvName=venvName) self.__getBanner() @@ -2634,6 +2642,27 @@ self.dbs.startClient(False) self.__getBanner() + ################################################################# + ## eric-ide Server Support + ################################################################# + + @pyqtSlot(bool) + def remoteConnectionChanged(self, connected): + """ + Public slot handling a change of the connection state to an eric-ide server. + + @param connected flag indicating the connection state + @type bool + """ + if connected: + if Preferences.getEricServer("AutostartShell"): + self.dbs.startClient(False, startRemote=True) + else: + if self.__currentVenv == self.dbs.getEricServerEnvironmentString(): + # start default backend + self.dbs.startClient(False) + self.__getBanner() + # # eflag: noqa = M601
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/EricRequestCategory.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing an enum for the various service categories. +""" + +import enum + + +class EricRequestCategory(enum.IntEnum): + """ + Class defining the service categories of the eric remote server. + """ + + FileSystem = 0 + Project = 1 + Debugger = 2 + Coverage = 3 + + Echo = 253 + Server = 254 + Error = 255 # only sent by the server to report an issue + + # user/plugins may define own categories starting with this value + UserCategory = 1024
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/EricServer.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,562 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the eric remote server. +""" + +import io +import json +import selectors +import socket +import struct +import sys +import traceback +import types +import zlib + +from eric7.UI.Info import Version + +from .EricRequestCategory import EricRequestCategory +from .EricServerCoverageRequestHandler import EricServerCoverageRequestHandler +from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler +from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler + +# TODO: remove dependency on 'eric7.UI.Info' + + +class EricServer: + """ + Class implementing the eric remote server. + """ + + def __init__(self, port=42024, useIPv6=False): + """ + Constructor + + @param port port to listen on (defaults to 42024) + @type int (optional) + @param useIPv6 flag indicating to use IPv6 protocol (defaults to False) + @type bool (optional) + """ + self.__requestCategoryHandlerRegistry = {} + # Dictionary containing the defined and registered request category + # handlers. The key is the request category and the value is the respective + # handler method. This method must have the signature: + # handler(request:str, params:dict, reqestUuid:str) -> None + self.__registerInternalHandlers() + + self.__connection = None + + self.__selector = selectors.DefaultSelector() + + # create and register the 'Debugger' request handler + self.__debuggerRequestHandler = EricServerDebuggerRequestHandler(self) + self.registerRequestHandler( + EricRequestCategory.Debugger, + self.__debuggerRequestHandler.handleRequest, + ) + + # create and register the 'File System' request handler + self.__fileSystemRequestHandler = EricServerFileSystemRequestHandler(self) + self.registerRequestHandler( + EricRequestCategory.FileSystem, + self.__fileSystemRequestHandler.handleRequest, + ) + + # create and register the 'Coverage' request handler + self.__coverageRequestHandler = EricServerCoverageRequestHandler(self) + self.registerRequestHandler( + EricRequestCategory.Coverage, + self.__coverageRequestHandler.handleRequest, + ) + + # TODO: 'Project' handler not implemented yet + # TODO: implement an 'EditorConfig' handler (?) + + self.__address = ("", port) + self.__useIPv6 = useIPv6 + + def getSelector(self): + """ + Public method to get a reference to the selector object. + + @return reference to the selector object + @rtype selectors.BaseSelector + """ + return self.__selector + + ####################################################################### + ## Methods for receiving requests and sending the results. + ####################################################################### + + def sendJson(self, category, reply, params, reqestUuid=""): + """ + Public method to send a single refactoring command to the server. + + @param category service category + @type EricRequestCategory + @param reply reply name to be sent + @type str + @param params dictionary of named parameters for the request + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + (defaults to "", i.e. no UUID received) + @type str + """ + if self.__connection is not None: + commandDict = { + "jsonrpc": "2.0", + "category": category, + "reply": reply, + "params": params, + "uuid": reqestUuid, + } + self.sendJsonCommand(commandDict, self.__connection) + + def sendJsonCommand(self, jsonCommand, sock): + """ + Public method to send a JSON encoded command/response via a given socket. + + @param jsonCommand dictionary containing the command data or a JSON encoded + command string + @type dict or str + @param sock reference to the socket to send the data to + @type socket.socket + @return flag indicating a successful transmission + @rtype bool + """ + if isinstance(jsonCommand, dict): + jsonCommand = json.dumps(jsonCommand) + print("Eric Server Send:", jsonCommand) + + data = jsonCommand.encode("utf8", "backslashreplace") + header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF) + try: + sock.sendall(header) + sock.sendall(data) + return True + except BrokenPipeError: + return False + + def __receiveBytes(self, length, sock): + """ + Private method to receive the given length of bytes. + + @param length bytes to receive + @type int + @param sock reference to the socket to receive the data from + @type socket.socket + @return received bytes or None if connection closed + @rtype bytes + """ + data = bytearray() + while sock is not None and len(data) < length: + try: + newData = sock.recv(length - len(data)) + if not newData: + print(str(newData)) + return None + + data += newData + except OSError as err: + if err.errno != 11: + data = None # in case some data was received already + break + return data + + def receiveJsonCommand(self, sock): + """ + Public method to receive a JSON encoded command and data. + + @param sock reference to the socket to receive the data from + @type socket.socket + @return dictionary containing the JSON command data or None to signal + an issue while receiving data + @rtype dict + """ + if self.isSocketClosed(sock): + return None + + header = self.__receiveBytes(struct.calcsize(b"!II"), sock) + if not header: + return {} + + length, datahash = struct.unpack(b"!II", header) + + length = int(length) + data = self.__receiveBytes(length, sock) + if data is None: + return None + + if not data or zlib.adler32(data) & 0xFFFFFFFF != datahash: + self.sendJson( + category=EricRequestCategory.Error, + reply="EricServerChecksumException", + params={ + "ExceptionType": "ProtocolChecksumError", + "ExceptionValue": "The checksum of the data does not match.", + "ProtocolData": data.decode("utf8", "backslashreplace"), + }, + ) + return {} + + jsonStr = data.decode("utf8", "backslashreplace") + print("Eric Server Receive:", jsonStr) + try: + return json.loads(jsonStr.strip()) + except (TypeError, ValueError) as err: + self.sendJson( + category=EricRequestCategory.Error, + reply="EricServerException", + params={ + "ExceptionType": "ProtocolError", + "ExceptionValue": str(err), + "ProtocolData": jsonStr.strip(), + }, + ) + return {} + + def __receiveJson(self): + """ + Private method to receive a JSON encoded command and data from the + server. + + @return tuple containing the received service category, the command, + a dictionary containing the associated data and the UUID of the + request + @rtype tuple of (int, str, dict, str) + """ + requestDict = self.receiveJsonCommand(self.__connection) + + if not requestDict: + return EricRequestCategory.Error, None, None, None + + category = requestDict["category"] + request = requestDict["request"] + params = requestDict["params"] + reqestUuid = requestDict["uuid"] + + return category, request, params, reqestUuid + + def isSocketClosed(self, sock): + """ + Public method to check, if a given socket is closed. + + @param sock reference to the socket to be checked + @type socket.socket + @return flag indicating a closed state + @rtype bool + """ + try: + # this will try to read bytes without removing them from buffer (peek only) + data = sock.recv(16, socket.MSG_PEEK) + if len(data) == 0: + return True + except BlockingIOError: + return False # socket is open and reading from it would block + except ConnectionError: + return True # socket was closed for some other reason + except Exception: + return False + return False + + ####################################################################### + ## Methods for the server main loop. + ####################################################################### + + def __initializeIdeSocket(self): + """ + Private method to initialize and register the eric-ide server socket. + """ + if socket.has_dualstack_ipv6() and self.__useIPv6: + self.__socket = socket.create_server( + self.__address, family=socket.AF_INET6, backlog=0, dualstack_ipv6=True + ) + else: + self.__socket = socket.create_server( + self.__address, family=socket.AF_INET, backlog=0 + ) + + self.__socket.listen(0) + self.__socket.setblocking(False) + print(f"Listening for 'eric-ide' connections on {self.__socket.getsockname()}") + data = types.SimpleNamespace( + name="server", acceptHandler=self.__acceptIdeConnection + ) + self.__selector.register(self.__socket, selectors.EVENT_READ, data=data) + + def __unregisterIdeSocket(self): + """ + Private method to unregister the eric-ide server socket because only one + connection is allowed. + """ + self.__selector.unregister(self.__socket) + self.__socket.shutdown(socket.SHUT_RDWR) + self.__socket.close() + self.__socket = None + + def __shutdown(self): + """ + Private method to shut down the server. + """ + self.__closeIdeConnection(shutdown=True) + + print("Stop listening for 'eric-ide' connections.") + if self.__socket is not None: + self.__socket.shutdown(socket.SHUT_RDWR) + self.__socket.close() + + self.__selector.close() + + def __acceptIdeConnection(self, sock): + """ + Private method to accept the connection on the listening IDE server socket. + + @param sock reference to the listening socket + @type socket.socket + """ + connection, address = sock.accept() # Should be ready to read + if self.__connection is None: + print(f"'eric-ide' connection from {address[0]}, port {address[1]}") + self.__connection = connection + self.__connection.setblocking(False) + data = types.SimpleNamespace( + name="eric-ide", address=address, handler=self.__serviceIdeConnection + ) + events = selectors.EVENT_READ + self.__selector.register(self.__connection, events, data=data) + + self.__unregisterIdeSocket() + else: + print( + f"'eric-ide' connection from {address[0]}, port {address[1]} rejected" + ) + connection.close() + + def __closeIdeConnection(self, shutdown=False): + """ + Private method to close the connection to an eric-ide. + + @param shutdown flag indicating a shutdown process + @type bool + """ + if self.__connection is not None: + self.__selector.unregister(self.__connection) + try: + print( + f"Closing 'eric-ide' connection to" + f" {self.__connection.getpeername()}." + ) + self.__connection.shutdown(socket.SHUT_RDWR) + self.__connection.close() + except OSError: + print("'eric-ide' connection gone.") + self.__connection = None + + self.__debuggerRequestHandler.shutdownClients() + + if not shutdown: + self.__initializeIdeSocket() + + def __serviceIdeConnection(self, key): + """ + Private method to service the eric-ide connection. + + @param key reference to the SelectorKey object associated with the connection + to be serviced + @type selectors.SelectorKey + """ + if key.data.name == "eric-ide": + category, request, params, reqestUuid = self.__receiveJson() + if category == EricRequestCategory.Error or request is None: + self.__closeIdeConnection() + return + + if category == EricRequestCategory.Server and request.lower() == "shutdown": + self.__shouldStop = True + return + + self.__handleRequest(category, request, params, reqestUuid) + + def run(self): + """ + Public method implementing the remote server main loop. + + Exiting the inner loop, that receives and dispatches the requests, will + cause the server to stop and exit. The main loop handles these requests. + <ul> + <li>exit - exit the handler loop and wait for the next connection</li> + <li>shutdown - exit the handler loop and perform a clean shutdown</li> + </ul> + + @return flag indicating a clean shutdown + @rtype bool + """ + cleanExit = True + self.__shouldStop = False + + # initialize the eric-ide server socket and listen for new connections + self.__initializeIdeSocket() + + # initialize the debug client server socket + self.__debuggerRequestHandler.initServerSocket() + + while True: + try: + events = self.__selector.select(timeout=None) + for key, _mask in events: + if key.data.name == "server": + # it is an event for a server socket + key.data.acceptHandler(key.fileobj) + else: + key.data.handler(key) + + except KeyboardInterrupt: + # intercept user pressing Ctrl+C + self.__shouldStop = True + + except Exception: + exctype, excval, exctb = sys.exc_info() + tbinfofile = io.StringIO() + traceback.print_tb(exctb, None, tbinfofile) + tbinfofile.seek(0) + tbinfo = tbinfofile.read() + + print(f"{str(exctype)} / {str(excval)} / {tbinfo}") + + self.__shouldStop = True + cleanExit = False + + if self.__shouldStop: + # exit the outer loop and shut down the server + self.__shutdown() + break + + return cleanExit + + ####################################################################### + ## Methods for registering and unregistering handlers. + ####################################################################### + + def registerRequestHandler(self, requestCategory, handler): + """ + Public method to register a request handler method for the given request + category. + + @param requestCategory request category to be registered + @type EricRequestCategory or int (>= EricRequestCategory.UserCategory) + @param handler reference to the handler method. This handler must accept + the parameters 'request', 'params', and 'requestUuid' + @type function(request:str, params:dict, requestUuid:str) + @exception ValueError raised to signal a request category collision + """ + if requestCategory in self.__requestCategoryHandlerRegistry: + raise ValueError(f"Request category '{requestCategory} already registered.") + + self.__requestCategoryHandlerRegistry[requestCategory] = handler + + def unregisterRequestHandler(self, requestCategory, ignoreError=False): + """ + Public method to unregister a handler for the given request category. + + Note: This method will raise a KeyError exception in case the request + category has not been registered and ignoreError is False (the default). + + @param requestCategory request category to be unregistered + @type EricRequestCategory or int (>= EricRequestCategory.UserCategory) + @param ignoreError flag indicating to ignore errors (defaults to False) + @type bool (optional) + """ + try: + del self.__requestCategoryHandlerRegistry[requestCategory] + except KeyError: + if not ignoreError: + raise + + def __registerInternalHandlers(self): + """ + Private method to register request handler categories of this class. + """ + self.registerRequestHandler(EricRequestCategory.Echo, self.__handleEchoRequest) + self.registerRequestHandler( + EricRequestCategory.Server, self.__handleServerRequest + ) + self.registerRequestHandler(EricRequestCategory.Error, None) + # Register a None handler to indicate we are not expecting a request of the + # 'Error' category. + + ####################################################################### + ## Request handler methods. + ####################################################################### + + def __handleRequest(self, category, request, params, reqestUuid): + """ + Private method handling or dispatching the received requests. + + @param category category of the request + @type EricRequestCategory + @param request request name + @type str + @param params request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + @type str + """ + try: + handler = self.__requestCategoryHandlerRegistry[category] + handler(request=request, params=params, reqestUuid=reqestUuid) + except KeyError: + self.sendJson( + category=EricRequestCategory.Error, + reply="UnsupportedServiceCategory", + params={"Category": category}, + ) + + def __handleEchoRequest(self, request, params, reqestUuid): # noqa: U100 + """ + Private method to handle an 'Echo' request. + + @param request request name + @type str + @param params request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + (defaults to "", i.e. no UUID received) + @type str + """ + self.sendJson( + category=EricRequestCategory.Echo, + reply="Echo", + params=params, + reqestUuid=reqestUuid, + ) + + def __handleServerRequest(self, request, params, reqestUuid): # noqa: U100 + """ + Private method to handle a 'Server' request. + + @param request request name + @type str + @param params request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + (defaults to "", i.e. no UUID received) + @type str + """ + # 'Exit' and 'Shutdown' are handled in the 'run()' method. + + if request.lower() == "versions": + self.sendJson( + category=EricRequestCategory.Server, + reply="Versions", + params={ + "python": sys.version.split()[0], + "py_bitsize": "64-Bit" if sys.maxsize > 2**32 else "32-Bit", + "version": Version, + "hostname": socket.gethostname(), + }, + reqestUuid=reqestUuid, + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/EricServerCoverageRequestHandler.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the code coverage request handler of the eric-ide server. +""" + +from coverage import Coverage +from coverage.misc import CoverageException + +from eric7.SystemUtilities import FileSystemUtilities + +from .EricRequestCategory import EricRequestCategory + + +class EricServerCoverageRequestHandler: + """ + Class implementing the code coverage request handler of the eric-ide server. + """ + + def __init__(self, server): + """ + Constructor + + @param server reference to the eric-ide server object + @type EricServer + """ + self.__server = server + + self.__requestMethodMapping = { + "LoadData": self.__loadCoverageData, + "AnalyzeFile": self.__analyzeFile, + "AnalyzeFiles": self.__analyzeFiles, + "AnalyzeDirectory": self.__analyzeDirectory, + } + + self.__cover = None + + def handleRequest(self, request, params, reqestUuid): + """ + Public method handling the received file system requests. + + @param request request name + @type str + @param params dictionary containing the request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + @type str + """ + try: + result = self.__requestMethodMapping[request](params) + self.__server.sendJson( + category=EricRequestCategory.Coverage, + reply=request, + params=result, + reqestUuid=reqestUuid, + ) + + except KeyError: + self.__server.sendJson( + category=EricRequestCategory.Coverage, + reply=request, + params={"Error": f"Request type '{request}' is not supported."}, + ) + + def __loadCoverageData(self, params): + """ + Private method to load the data collected by a code coverage run. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + if self.__cover is not None: + del self.__cover + self.__cover = None + + try: + self.__cover = Coverage(data_file=params["data_file"]) + self.__cover.load() + if params["exclude"]: + self.__cover.exclude(params["exclude"]) + return {"ok": True} + except CoverageException as err: + return { + "ok": False, + "error": str(err), + } + + def __analyzeFile(self, params): + """ + Private method to analyze a single file. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + if self.__cover is None: + return { + "ok": False, + "error": "Coverage data has to be loaded first.", + } + + try: + return { + "ok": True, + "result": self.__cover.analysis2(params["filename"]), + } + except CoverageException as err: + return { + "ok": False, + "error": str(err), + } + + def __analyzeFiles(self, params): + """ + Private method to analyze a list of files. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + # TODO: not implemented yet + if self.__cover is None: + return { + "ok": False, + "error": "Coverage data has to be loaded first.", + } + + try: + return { + "ok": True, + "results": [self.__cover.analysis2(f) for f in params["filenames"]], + } + except CoverageException as err: + return { + "ok": False, + "error": str(err), + } + + def __analyzeDirectory(self, params): + """ + Private method to analyze files of a directory tree. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + # TODO: not implemented yet + if self.__cover is None: + return { + "ok": False, + "error": "Coverage data has to be loaded first.", + } + + files = FileSystemUtilities.direntries(params["directory"], True, "*.py", False) + + try: + return { + "ok": True, + "results": [self.__cover.analysis2(f) for f in files], + } + except CoverageException as err: + return { + "ok": False, + "error": str(err), + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the debugger request handler of the eric-ide server. +""" + +import json +import os +import selectors +import socket +import subprocess +import sys +import types + +from .EricRequestCategory import EricRequestCategory + + +class EricServerDebuggerRequestHandler: + """ + Class implementing the debugger request handler of the eric-ide server. + """ + + def __init__(self, server): + """ + Constructor + + @param server reference to the eric-ide server object + @type EricServer + """ + self.__server = server + + self.__requestMethodMapping = { + "StartClient": self.__startClient, + "StopClient": self.__stopClient, + "DebugClientCommand": self.__relayDebugClientCommand, + } + + self.__mainClientId = None + self.__client = None + self.__inStartClient = False + self.__pendingConnections = [] + self.__connections = {} + + address = ("127.0.0.1", 0) + self.__socket = socket.create_server(address, family=socket.AF_INET) + + def initServerSocket(self): + """ + Public method to initialize the server socket listening for debug client + connections. + """ + # listen on the debug server socket + self.__socket.listen() + self.__socket.setblocking(False) + print( + f"Listening for 'debug client' connections on" + f" {self.__socket.getsockname()}" + ) + data = types.SimpleNamespace( + name="server", acceptHandler=self.__acceptDbgClientConnection + ) + self.__server.getSelector().register( + self.__socket, selectors.EVENT_READ, data=data + ) + + def handleRequest(self, request, params, reqestUuid): + """ + Public method handling the received debugger requests. + + @param request request name + @type str + @param params dictionary containing the request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + @type str + """ + try: + result = self.__requestMethodMapping[request](params) + if result: + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply=request, + params=result, + reqestUuid=reqestUuid, + ) + + except KeyError: + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply="DebuggerRequestError", + params={"Error": f"Request type '{request}' is not supported."}, + ) + + ####################################################################### + ## DebugServer like methods. + ####################################################################### + + def __acceptDbgClientConnection(self, sock): + """ + Private method to accept the connection on the listening debug server socket. + + @param sock reference to the listening socket + @type socket.socket + """ + connection, address = sock.accept() # Should be ready to read + print(f"'Debug client' connection from {address[0]}, port {address[1]}") + connection.setblocking(False) + self.__pendingConnections.append(connection) + + data = types.SimpleNamespace( + name="debug_client", + address=address, + handler=self.__serviceDbgClientConnection, + ) + self.__server.getSelector().register( + connection, selectors.EVENT_READ, data=data + ) + + def __serviceDbgClientConnection(self, key): + """ + Private method to service the debug client connection. + + @param key reference to the SelectorKey object associated with the connection + to be serviced + @type selectors.SelectorKey + """ + sock = key.fileobj + data = self.__server.receiveJsonCommand(sock) + + if data is None: + # socket was closed by debug client + self.__clientSocketDisconnected(sock) + elif data: + method = data["method"] + + # 1. process debug client messages before relaying + if method == "DebuggerId" and sock in self.__pendingConnections: + debuggerId = data["params"]["debuggerId"] + self.__connections[debuggerId] = sock + self.__pendingConnections.remove(sock) + if self.__mainClientId is None: + self.__mainClientId = debuggerId + + elif method == "ResponseBanner": + # add an indicator for the eric-ide server + data["params"]["platform"] += " (eric-ide Server)" + + # 2. pass on the data to the eric-ide + jsonStr = json.dumps(data) + print("Client Response:", jsonStr) + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply="DebugClientResponse", + params={"response": jsonStr}, + ) + + # 3. process debug client messages after relaying + if method == "ResponseExit": + for sock in list(self.__connections.values()): + if not self.__server.isSocketClosed(sock): + self.__clientSocketDisconnected(sock) + + def __clientSocketDisconnected(self, sock): + """ + Private method handling a socket disconnecting. + + @param sock reference to the disconnected socket + @type socket.socket + """ + self.__server.getSelector().unregister(sock) + + for debuggerId in list(self.__connections): + if self.__connections[debuggerId] is sock: + del self.__connections[debuggerId] + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply="DebugClientDisconnected", + params={"debugger_id": debuggerId}, + ) + + if debuggerId == self.__mainClientId: + self.__mainClientId = None + + break + else: + if sock in self.__pendingConnections: + self.__pendingConnections.remove(sock) + + sock.close() + + def __mainClientExited(self): + """ + Private method to handle exiting of the main debug client. + """ + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply="MainClientExited", + params={"debugger_id": self.__mainClientId if self.__mainClientId else ""}, + ) + + def shutdownClients(self): + """ + Public method to shut down all connected clients. + """ + if not self.__client: + # no client started yet + return + + while self.__pendingConnections: + sock = self.__pendingConnections.pop() + commandDict = self.__prepareClientCommand("RequestShutdown", {}) + self.__server.sendJsonCommand(commandDict, sock) + self.__shutdownSocket("", sock) + + while self.__connections: + debuggerId, sock = self.__connections.popitem() + commandDict = self.__prepareClientCommand("RequestShutdown", {}) + self.__server.sendJsonCommand(commandDict, sock) + self.__shutdownSocket(debuggerId, sock) + + # reinitialize + self.__mainClientId = None + self.__client = None + + def __shutdownSocket(self, debuggerId, sock): + """ + Private method to shut down a socket. + + @param debuggerId ID of the debugger the socket belongs to + @type str + @param sock reference to the socket + @type socket.socket + """ + self.__server.getSelector().unregister(sock) + sock.shutdown(socket.SHUT_RDWR) + sock.close() + + if debuggerId: + self.__server.sendJson( + category=EricRequestCategory.Debugger, + reply="DebugClientDisconnected", + params={"debugger_id": debuggerId}, + ) + + def __prepareClientCommand(self, command, params): + """ + Private method to prepare a command dictionary for the debug client. + + @param command command to be sent + @type str + @param params dictionary containing the command parameters + @type dict + @return completed command dictionary to be sent to the debug client + @rtype dict + """ + return { + "jsonrpc": "2.0", + "method": command, + "params": params, + } + + ####################################################################### + ## Individual request handler methods. + ####################################################################### + + def __startClient(self, params): + """ + Private method to start a debug client process. + + @param params dictionary containing the request data + @type dict + """ + self.__inStartClient = True + + # start a debug client + debugClient = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", + "DebugClients", + "Python", + "DebugClient.py", + ) + ) + ipaddr, port = self.__socket.getsockname() + args = [sys.executable, debugClient] + args.extend(params["arguments"]) + args.extend([str(port), "True", ipaddr]) + + self.__client = subprocess.Popen( + args, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + def __stopClient(self, params): # noqa: U100 + """ + Private method to stop the current debug client process. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + self.shutdownClients() + + return {"ok": True} + + def __relayDebugClientCommand(self, params): + """ + Private method to relay a debug client command to the client. + + @param params dictionary containing the request data + @type dict + """ + debuggerId = params["debugger_id"] + jsonStr = params["command"] + print(debuggerId, "->", jsonStr) + + if not debuggerId and self.__mainClientId and "RequestBanner" in jsonStr: + # modify the target for the 'RequestBanner' request + debuggerId = self.__mainClientId + + if debuggerId == "<<all>>": + # broadcast to all connected debug clients + for sock in self.__connections.values(): + self.__server.sendJsonCommand(jsonStr, sock) + else: + try: + sock = self.__connections[debuggerId] + self.__server.sendJsonCommand(jsonStr, sock) + except KeyError: + print(f"Command for unknown debugger ID '{debuggerId}' received.")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the file system request handler of the eric-ide server. +""" + +import base64 +import contextlib +import os +import stat +import time + +from eric7.SystemUtilities import FileSystemUtilities + +from .EricRequestCategory import EricRequestCategory + + +class EricServerFileSystemRequestHandler: + """ + Class implementing the file system request handler of the eric-ide server. + """ + + def __init__(self, server): + """ + Constructor + + @param server reference to the eric-ide server object + @type EricServer + """ + self.__server = server + + self.__requestMethodMapping = { + "GetPathSep": self.__getPathSeparator, + "Chdir": self.__chdir, + "Getcwd": self.__getcwd, + "Listdir": self.__listdir, + "Mkdir": self.__mkdir, + "MakeDirs": self.__makedirs, + "Rmdir": self.__rmdir, + "Replace": self.__replace, + "Remove": self.__remove, + "Stat": self.__stat, + "Exists": self.__exists, + "Access": self.__access, + "ReadFile": self.__readFile, + "WriteFile": self.__writeFile, + "DirEntries": self.__dirEntries, + } + + def handleRequest(self, request, params, reqestUuid): + """ + Public method handling the received file system requests. + + @param request request name + @type str + @param params dictionary containing the request parameters + @type dict + @param reqestUuid UUID of the associated request as sent by the eric IDE + @type str + """ + try: + result = self.__requestMethodMapping[request](params) + self.__server.sendJson( + category=EricRequestCategory.FileSystem, + reply=request, + params=result, + reqestUuid=reqestUuid, + ) + + except KeyError: + self.__server.sendJson( + category=EricRequestCategory.FileSystem, + reply=request, + params={ + "ok": False, + "error": f"Request type '{request}' is not supported.", + "info": list(self.__requestMethodMapping.keys()), + }, + reqestUuid=reqestUuid, + ) + + def __getPathSeparator(self, params): # noqa: U100 + """ + Private method to report the path separator. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + return {"separator": os.sep} + + def __chdir(self, params): + """ + Private method to change the current working directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.chdir(params["directory"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __getcwd(self, params): # noqa: U100 + """ + Private method to report the current working directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + return {"directory": os.getcwd()} + + def __listdir(self, params): + """ + Private method to report a directory listing. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + directory = params["directory"] + if not directory: + directory = os.getcwd() + + try: + listing = [] + for dirEntry in os.scandir(directory): + filestat = dirEntry.stat() + entry = { + "name": dirEntry.name, + "path": dirEntry.path, + "is_dir": dirEntry.is_dir(), + "is_file": dirEntry.is_file(), + "is_link": dirEntry.is_symlink(), + "mode": filestat.st_mode, + "mode_str": stat.filemode(filestat.st_mode), + "size": filestat.st_size, + "mtime": time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(filestat.st_mtime) + ), + } + listing.append(entry) + + return { + "ok": True, + "directory": directory, + "listing": listing, + "separator": os.sep, + } + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __stat(self, params): + """ + Private method to get the status of a file. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + result = os.stat(params["filename"]) + resultDict = {st: getattr(result, st) for st in params["st_names"]} + return {"ok": True, "result": resultDict} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __exists(self, params): + """ + Private method to check if a file or directory of the given name exists. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + return {"exists": os.path.exists(params["name"])} + + def __access(self, params): + """ + Private method to test, if the eric-ide server has the given access rights + to a file or directory.. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + mode = os.F_OK + for modeStr in params["modes"]: + if modeStr == "read": + mode |= os.R_OK + elif modeStr == "write": + mode |= os.W_OK + elif modeStr == "execute": + mode |= os.X_OK + + return {"ok": os.access(params["name"], mode)} + + def __mkdir(self, params): + """ + Private method to create a new directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.mkdir(params["directory"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __makedirs(self, params): + """ + Private method to create a new directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.makedirs(params["directory"], exist_ok=params["exist_ok"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __rmdir(self, params): + """ + Private method to delete a directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.rmdir(params["directory"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __replace(self, params): + """ + Private method to replace (rename) a file or directory. + + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.replace(params["old_name"], params["new_name"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __remove(self, params): + """ + Private method to delete a file. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + try: + os.remove(params["filename"]) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __readFile(self, params): + """ + Private method to read the contents of a file. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + filename = params["filename"] + + if params["create"] and not os.path.exists(filename): + with open(filename, "wb"): + pass + + try: + with open(filename, "rb") as f: + data = f.read() + return { + "ok": True, + "filedata": str(base64.b85encode(data), encoding="ascii"), + } + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __writeFile(self, params): + """ + Private method to write data into a file. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + filename = params["filename"] + data = base64.b85decode(bytes(params["filedata"], encoding="ascii")) + + # 1. create backup file if asked for + if params["with_backup"]: + if os.path.islink(filename): + filename = os.path.realpath(filename) + backupFilename = "{0}~".format(filename) + try: + permissions = os.stat(filename).st_mode + perms_valid = True + except OSError: + # if there was an error, ignore it + perms_valid = False + with contextlib.suppress(OSError): + os.remove(backupFilename) + with contextlib.suppress(OSError): + os.rename(filename, backupFilename) + + # 2. write the data to the file and reset the permissions + try: + with open(filename, "wb") as f: + f.write(data) + if params["with_backup"] and perms_valid: + os.chmod(filename, permissions) + return {"ok": True} + except OSError as err: + return { + "ok": False, + "error": str(err), + } + + def __dirEntries(self, params): + """ + Private method to get a list of all files and directories of a given directory. + + @param params dictionary containing the request data + @type dict + @return dictionary containing the reply data + @rtype dict + """ + directory = params["directory"] + result = FileSystemUtilities.direntries( + directory, + filesonly=params["files_only"], + pattern=params["pattern"], + followsymlinks=params["follow_symlinks"], + ignore=params["ignore"], + recursive=params["recursive"], + dirsonly=params["dirs_only"], + ) + return { + "ok": True, + "result": result, + }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServer/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the components of the eric-ide remote server. +"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + + +""" +Module implementing a dialog to enter the parameters for a connection to an eric-ide +server. +""" + +import ipaddress + +from PyQt6.QtCore import pyqtSlot +from PyQt6.QtWidgets import QDialog, QDialogButtonBox + +from eric7 import Preferences + +from .Ui_EricServerConnectionDialog import Ui_EricServerConnectionDialog + + +class EricServerConnectionDialog(QDialog, Ui_EricServerConnectionDialog): + """ + Class implementing a dialog to enter the parameters for a connection to an eric-ide + server. + """ + + def __init__(self, profileNames=None, parent=None): + """ + Constructor + + @param profileNames list of defined connection profile names (defaults to None) + @type list of str (optional) + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.timeoutSpinBox.setToolTip( + self.tr( + "Enter the timeout for the connection attempt (default: {0} s." + ).format(Preferences.getEricServer("ConnectionTimeout")) + ) + + self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False) + + if profileNames is None: + self.nameLabel.setVisible(False) + self.nameEdit.setVisible(False) + self.nameEdit.setEnabled(False) + + self.__profileNames = profileNames[:] if bool(profileNames) else [] + self.__originalName = "" + + self.nameEdit.textChanged.connect(self.__updateOK) + self.hostnameEdit.textChanged.connect(self.__updateOK) + + msh = self.minimumSizeHint() + self.resize(max(self.width(), msh.width()), msh.height()) + + @pyqtSlot() + def __updateOK(self): + """ + Private slot to update the enabled state of the OK button. + """ + hostname = self.hostnameEdit.text() + + if hostname and hostname[0] in "0123456789" and ":" not in hostname: + # possibly an IPv4 address + try: + ipaddress.IPv4Address(hostname) + valid = True + except ipaddress.AddressValueError: + # leading zeros are not allowed + valid = False + elif ":" in hostname: + # possibly an IPv6 address + try: + ipaddress.IPv6Address(hostname) + valid = True + except ipaddress.AddressValueError: + # leading zeros are not allowed + valid = False + elif ":" not in hostname: + valid = bool(hostname) + else: + valid = False + + if self.nameEdit.isEnabled(): + # connection profile mode + name = self.nameEdit.text() + valid &= name == self.__originalName or name not in self.__profileNames + + self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(valid) + + def getData(self): + """ + Public method to get the entered data. + + @return tuple containing the entered host name or IP address, the port number + and the timeout (in seconds) + @rtype tuple of (str, int, int) + """ + port = self.portSpinBox.value() + if port == self.portSpinBox.minimum(): + port = None + + timeout = self.timeoutSpinBox.value() + if timeout == self.timeoutSpinBox.minimum(): + timeout = None + + return self.hostnameEdit.text(), port, timeout + + def getProfileData(self): + """ + Public method to get the entered data for connection profile mode. + + @return tuple containing the profile name, host name or IP address, + the port number and the timeout (in seconds) + @rtype tuple of (str, str, int, int) + """ + port = self.portSpinBox.value() + if port == self.portSpinBox.minimum(): + port = 0 + + timeout = self.timeoutSpinBox.value() + if timeout == self.timeoutSpinBox.minimum(): + timeout = 0 + + return self.nameEdit.text(), self.hostnameEdit.text(), port, timeout + + def setProfileData(self, name, hostname, port, timeout): + """ + Public method to set the connection profile data to be edited. + + @param name profile name + @type str + @param hostname host name or IP address + @type str + @param port port number + @type int + @param timeout timeout value in seconds + @type int + """ + # adjust some values + if not bool(port): + port = self.portSpinBox.minimum() + if not bool(timeout): + timeout = self.timeoutSpinBox.minimum() + + self.__originalName = name + + self.nameEdit.setText(name) + self.hostnameEdit.setText(hostname) + self.portSpinBox.setValue(port) + self.timeoutSpinBox.setValue(timeout)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerConnectionDialog.ui Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,181 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EricServerConnectionDialog</class> + <widget class="QDialog" name="EricServerConnectionDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>169</height> + </rect> + </property> + <property name="windowTitle"> + <string>eric-ide Server Connection</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>Name:</string> + </property> + </widget> + </item> + <item row="0" column="1" colspan="2"> + <widget class="QLineEdit" name="nameEdit"> + <property name="toolTip"> + <string>Enter the name for the eric-ide server connection profile.</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Hostname:</string> + </property> + </widget> + </item> + <item row="1" column="1" colspan="2"> + <widget class="QLineEdit" name="hostnameEdit"> + <property name="toolTip"> + <string>Enter the hostname or IP address of the eric-ide server to connect to.</string> + </property> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Port:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QSpinBox" name="portSpinBox"> + <property name="toolTip"> + <string>Enter the port number the eric-ide server listens on (default: 42024).</string> + </property> + <property name="wrapping"> + <bool>true</bool> + </property> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="specialValueText"> + <string>default</string> + </property> + <property name="accelerated"> + <bool>true</bool> + </property> + <property name="showGroupSeparator" stdset="0"> + <bool>true</bool> + </property> + <property name="minimum"> + <number>1024</number> + </property> + <property name="maximum"> + <number>65535</number> + </property> + <property name="value"> + <number>1024</number> + </property> + </widget> + </item> + <item row="2" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>240</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Timeout:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QSpinBox" name="timeoutSpinBox"> + <property name="alignment"> + <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> + </property> + <property name="specialValueText"> + <string>default</string> + </property> + <property name="suffix"> + <string> s</string> + </property> + <property name="maximum"> + <number>60</number> + </property> + </widget> + </item> + <item row="4" column="0" colspan="3"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + <tabstop>hostnameEdit</tabstop> + <tabstop>portSpinBox</tabstop> + <tabstop>timeoutSpinBox</tabstop> + </tabstops> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EricServerConnectionDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EricServerConnectionDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerCoverageInterface.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,250 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the code coverage interface to the eric-ide server. +""" + +import contextlib + +from PyQt6.QtCore import QEventLoop, QObject + +from eric7.RemoteServer.EricRequestCategory import EricRequestCategory +from eric7.SystemUtilities import FileSystemUtilities + + +class EricServerCoverageError(Exception): + """ + Class defining a substitute exception for coverage errors of the server. + """ + + pass + + +class EricServerCoverageInterface(QObject): + """ + Class implementing the code coverage interface to the eric-ide server. + """ + + def __init__(self, serverInterface): + """ + Constructor + + @param serverInterface reference to the eric-ide server interface + @type EricServerInterface + """ + super().__init__(parent=serverInterface) + + self.__serverInterface = serverInterface + + def loadCoverageData(self, dataFile, excludePattern=""): + """ + Public method to tell the server to load the coverage data for a later analysis. + + @param dataFile name of the data file to be loaded + @type str + @param excludePattern regular expression determining files to be excluded + (defaults to "") + @type str (optional) + @return tuple containing a success flag and an error message + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "LoadData": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.Coverage, + request="LoadData", + params={ + "data_file": FileSystemUtilities.plainFileName(dataFile), + "exclude": excludePattern, + }, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def analyzeFile(self, filename): + """ + Public method to analyze the code coverage of one file. + + @param filename name of the file to be analyzed + @type str + @return list containing coverage result as reported by Coverage.analysis2() + @rtype list of [str, list of int, list of int, list of int, str] + @exception EricServerCoverageError raised to indicate a coverage exception + @exception OSError raised to indicate that server is not connected + """ + loop = QEventLoop() + ok = False + error = "" + result = None + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error, result + + if reply == "AnalyzeFile": + ok = params["ok"] + if ok: + result = params["result"] + else: + error = params["error"] + loop.quit() + + if not self.__serverInterface.isServerConnected(): + raise OSError("Not connected to an 'eric-ide' server.") + + else: + self.__serverInterface.sendJson( + category=EricRequestCategory.Coverage, + request="AnalyzeFile", + params={"filename": FileSystemUtilities.plainFileName(filename)}, + callback=callback, + ) + + loop.exec() + if not ok: + raise EricServerCoverageError(error) + + return result + + def analyzeFiles(self, filenames): + """ + Public method to analyze the code coverage of a list of files. + + @param filenames list of file names to be analyzed + @type str + @return lists containing coverage results as reported by Coverage.analysis2() + @rtype list of [list of [str, list of int, list of int, list of int, str]] + @exception EricServerCoverageError raised to indicate a coverage exception + @exception OSError raised to indicate that server is not connected + """ + loop = QEventLoop() + ok = False + error = "" + result = None + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error, result + + if reply == "AnalyzeFiles": + ok = params["ok"] + if ok: + result = params["results"] + else: + error = params["error"] + loop.quit() + + if not self.__serverInterface.isServerConnected(): + raise OSError("Not connected to an 'eric-ide' server.") + + else: + self.__serverInterface.sendJson( + category=EricRequestCategory.Coverage, + request="AnalyzeFiles", + params={ + "filenames": [ + FileSystemUtilities.plainFileName(f) for f in filenames + ] + }, + callback=callback, + ) + + loop.exec() + if not ok: + raise EricServerCoverageError(error) + + return result + + def analyzeDirectory(self, directory): + """ + Public method to analyze the code coverage of a directory. + + @param directory directory name to be analyzed + @type str + @return lists containing coverage results as reported by Coverage.analysis2() + @rtype list of [list of [str, list of int, list of int, list of int, str]] + @exception EricServerCoverageError raised to indicate a coverage exception + @exception OSError raised to indicate that server is not connected + """ + loop = QEventLoop() + ok = False + error = "" + result = None + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error, result + + if reply == "AnalyzeDirectory": + ok = params["ok"] + if ok: + result = params["results"] + else: + error = params["error"] + loop.quit() + + if not self.__serverInterface.isServerConnected(): + raise OSError("Not connected to an 'eric-ide' server.") + + else: + self.__serverInterface.sendJson( + category=EricRequestCategory.Coverage, + request="AnalyzeDirectory", + params={"directory": FileSystemUtilities.plainFileName(directory)}, + callback=callback, + ) + + loop.exec() + if not ok: + raise EricServerCoverageError(error) + + return result
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the file system interface to the eric-ide server. +""" + +from PyQt6.QtCore import QEventLoop, QObject, pyqtSignal, pyqtSlot + +from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.RemoteServer.EricRequestCategory import EricRequestCategory +from eric7.SystemUtilities import FileSystemUtilities + + +# TODO: sanitize all file names with FileSystemUtilities.plainFileName() +class EricServerDebuggerInterface(QObject): + """ + Class implementing the file system interface to the eric-ide server. + + @signal debugClientResponse(response:str) emitted to relay a response of + the remote debug client + @signal debugClientDisconnected(debuggerId:str) emitted when a remote debug + client did disconnect from the eric-ide server + @signal lastClientExited() emitted to indicate that the last debug client of + the eric-ide server exited + """ + + debugClientResponse = pyqtSignal(str) + debugClientDisconnected = pyqtSignal(str) + lastClientExited = pyqtSignal() + + def __init__(self, serverInterface): + """ + Constructor + + @param serverInterface reference to the eric-ide server interface + @type EricServerInterface + """ + super().__init__(parent=serverInterface) + + self.__serverInterface = serverInterface + self.__clientStarted = False + + self.__replyMethodMapping = { + "DebuggerRequestError": self.__handleDbgRequestError, + "DebugClientResponse": self.__handleDbgClientResponse, + "DebugClientDisconnected": self.__handleDbgClientDisconnected, + "LastDebugClientExited": self.__handleLastDbgClientExited, + "MainClientExited": self.__handleMainClientExited, + } + + # connect some signals + self.__serverInterface.remoteDebuggerReply.connect(self.__handleDebuggerReply) + + def sendClientCommand(self, debuggerId, jsonCommand): + """ + Public method to rely a debug client command via the eric-ide server. + + @param debuggerId id of the debug client to send the command to + @type str + @param jsonCommand JSON encoded command dictionary to be relayed + @type str + """ + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.Debugger, + request="DebugClientCommand", + params={"debugger_id": debuggerId, "command": jsonCommand}, + ) + + @pyqtSlot(str, dict) + def __handleDebuggerReply(self, reply, params): + """ + Private slot to handle a debugger reply from the eric-ide server. + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + if self.__serverInterface.isServerConnected(): + try: + self.__replyMethodMapping[reply](params) + except KeyError: + EricMessageBox.critical( + None, + self.tr("Unknown Server Reply"), + self.tr( + "<p>The eric-ide server debugger interface sent the unknown" + " reply <b>{0}</b>.</p>" + ).format(reply), + ) + + ####################################################################### + ## Methods for handling of debug client replies. + ####################################################################### + + def __handleDbgRequestError(self, params): + """ + Private method to handle an error reported by the debugger interface of + the eric-ide server. + + @param params dictionary containing the reply data + @type dict + """ + EricMessageBox.warning( + None, + self.tr("Debug Client Command"), + self.tr( + "<p>The IDE received an error message.</p><p>Error: {0}</p>" + ).format(params["Error"]), + ) + + def __handleDbgClientResponse(self, params): + """ + Private method to handle a response from a debug client connected to the + eric-ide server. + + @param params dictionary containing the reply data + @type dict + """ + self.debugClientResponse.emit(params["response"]) + + def __handleDbgClientDisconnected(self, params): + """ + Private method to handle a debug client disconnect report of the + eric-ide server. + + @param params dictionary containing the reply data + @type dict + """ + self.debugClientDisconnected.emit(params["debugger_id"]) + + def __handleLastDbgClientExited(self, params): # noqa: U100 + """ + Private method to handle a report of the eric-ide server, that the last + debug client has disconnected. + + @param params dictionary containing the reply data + @type dict + """ + self.__clientStarted = False + self.lastClientExited.emit() + + def __handleMainClientExited(self, params): # noqa: U100 + """ + Private method to handle the main client exiting. + + @param params dictionary containing the reply data + @type dict + """ + self.__clientStarted = False + ericApp().getObject("DebugServer").signalMainClientExit() + + ####################################################################### + ## Methods for sending debug server commands to the eric-ide server. + ####################################################################### + + def startClient(self, interpreter, originalPathString, args, workingDir=""): + """ + Public method to send a command to start a debug client. + + @param interpreter path of the remote interpreter to be used + @type str + @param originalPathString original PATH environment variable + @type str + @param args list of command line parameters for the debug client + @type list of str + @param workingDir directory to start the debugger client in (defaults to "") + @type str (optional) + """ + self.__serverInterface.sendJson( + category=EricRequestCategory.Debugger, + request="StartClient", + params={ + "interpreter": FileSystemUtilities.plainFileName(interpreter), + "path": originalPathString, + "arguments": args, + "working_dir": FileSystemUtilities.plainFileName(workingDir), + }, + ) + self.__clientStarted = True + + def stopClient(self): + """ + Public method to stop the debug client synchronously. + """ + if self.__serverInterface.isServerConnected() and self.__clientStarted: + loop = QEventLoop() + + def callback(reply, params): # noqa: U100 + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + if reply == "StopClient": + loop.quit() + + self.__serverInterface.sendJson( + category=EricRequestCategory.Debugger, + request="StopClient", + params={}, + callback=callback, + ) + + loop.exec() + self.__clientStarted = False
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerFileDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,1021 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a file dialog showing the file system of the eric-ide server. +""" + +import enum +import fnmatch +import re + +from PyQt6.QtCore import QLocale, QPoint, Qt, pyqtSlot +from PyQt6.QtWidgets import ( + QAbstractItemView, + QCompleter, + QDialog, + QInputDialog, + QLineEdit, + QMenu, + QTreeWidgetItem, +) + +from eric7.EricGui import EricPixmapCache +from eric7.EricGui.EricFileIconProvider import EricFileIconProvider +from eric7.EricWidgets import EricMessageBox +from eric7.EricWidgets.EricApplication import ericApp +from eric7.Globals import dataString +from eric7.SystemUtilities import FileSystemUtilities + +from .Ui_EricServerFileDialog import Ui_EricServerFileDialog + + +class AcceptMode(enum.Enum): + """ + Class defining the dialog accept modes. + """ + + AcceptOpen = 0 + AcceptSave = 1 + + +class FileMode(enum.Enum): + """ + Class defining what the user may select in the file dialog. + """ + + AnyFile = 0 + ExistingFile = 1 + Directory = 2 + ExistingFiles = 3 + + +class EricServerFileDialog(QDialog, Ui_EricServerFileDialog): + """ + Class implementing a file dialog showing the file system of the eric-ide server. + """ + + IsDirectoryRole = Qt.ItemDataRole.UserRole + + def __init__(self, parent=None, caption="", directory="", filter=""): # noqa: M132 + """ + Constructor + + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + @param caption dialog title (defaults to "") + @type str (optional) + @param directory initial directory (defaults to "") + @type str (optional) + @param filter Qt file filter string (defaults to "") + @type str (optional) + """ + super().__init__(parent) + self.setupUi(self) + + # finish UI setup + self.backButton.setIcon(EricPixmapCache.getIcon("1leftarrow")) + self.forwardButton.setIcon(EricPixmapCache.getIcon("1rightarrow")) + self.upButton.setIcon(EricPixmapCache.getIcon("1uparrow")) + self.newDirButton.setIcon(EricPixmapCache.getIcon("dirNew")) + self.reloadButton.setIcon(EricPixmapCache.getIcon("reload")) + self.cancelButton.setIcon(EricPixmapCache.getIcon("dialog-cancel")) + + self.setWindowTitle(caption) + + self.__iconProvider = EricFileIconProvider() + + self.__nameCompleter = QCompleter() + self.__nameCompleter.setModel(self.listing.model()) + self.__nameCompleter.setCompletionColumn(0) + self.__nameCompleter.activated.connect(self.__nameCompleterActivated) + self.nameEdit.setCompleter(self.__nameCompleter) + + self.__contextMenu = QMenu(self) + + self.__fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + + # set some default values + self.__fileMode = FileMode.ExistingFile + self.__dirsOnly = False + self.__acceptMode = AcceptMode.AcceptOpen + self.__showHidden = False + self.__sep = "/" + self.__filters = [] + + self.__history = [] + self.__currentHistoryIndex = -1 # empty history + self.__updateHistoryButtons() + + self.__filenameCache = [] + self.__directoryCache = [] + self.__selectedDirectory = None + + self.setNameFilters(filter.split(";;")) + + self.reloadButton.clicked.connect(self.__reload) + self.cancelButton.clicked.connect(self.reject) + + self.treeCombo.currentTextChanged.connect(self.setDirectory) + + self.setDirectory(FileSystemUtilities.plainFileName(directory)) + + def acceptMode(self): + """ + Public method to get the accept mode of the dialog. + + @return accept mode + @rtype AcceptMode + """ + return self.__acceptMode + + def setAcceptMode(self, mode): + """ + Public method to set the accept mode of the dialog. + + @param mode accept mode + @type AcceptMode + """ + self.__acceptMode = mode + + self.__updateOkButton() + + def fileMode(self): + """ + Public method to get the current file mode of the dialog. + + @return file mode + @rtype FileMode + """ + return self.__fileMode + + def setFileMode(self, mode): + """ + Public method to set the file mode of the dialog. + + @param mode file mode + @type FileMode + """ + self.__fileMode = mode + + self.listing.clearSelection() + if mode == FileMode.ExistingFiles: + self.listing.setSelectionMode( + QAbstractItemView.SelectionMode.ExtendedSelection + ) + else: + self.listing.setSelectionMode( + QAbstractItemView.SelectionMode.SingleSelection + ) + + if mode == FileMode.Directory: + self.setNameFilters([self.tr("Directories")]) + + self.__updateOkButton() + + def setNameFilters(self, filters): + """ + Public method to set the list of file/directory name filters. + + @param filters list of filter expressions + ("filter_name (pattern1 ... patternN)") + @type list of str + """ + self.__filters = [] + for f in filters: + if " (" in f and ")" in f: + self.__filters.append(f.split(" (", 1)[1].split(")", 1)[0].split()) + elif f: + self.__filters.append(f) + + self.filterCombo.clear() + self.filterCombo.addItems([f for f in filters if f]) + + def setNameFilter(self, filter): # noqa: M132 + """ + Public method to set the current name filter. + + @param filter filter text to make current + @type str + """ + self.filterCombo.setCurrentText(filter) + + def setDirectoriesOnly(self, dirsOnly): + """ + Public method to set a flag to just show directories. + + @param dirsOnly flag indicating to just show directories + @type bool + """ + self.__dirsOnly = dirsOnly + + filters = self.__filters[self.filterCombo.currentIndex()] + self.__filterList(filters) + + def __addToHistory(self, entry): + """ + Private method to add a directory to the history list. + + @param entry name of the directory to be added + @type str + """ + try: + # is in the history already? + index = self.__history.index(entry) + self.__currentHistoryIndex = index + except ValueError: + # new entry + self.__history.append(entry) + self.__currentHistoryIndex = len(self.__history) - 1 + + self.__updateHistoryButtons() + + @pyqtSlot() + def __updateHistoryButtons(self): + """ + Private method to update the enabled state of the back and forward buttons. + """ + if not self.__history: + self.backButton.setEnabled(False) + self.forwardButton.setEnabled(False) + else: + self.backButton.setEnabled(self.__currentHistoryIndex > 0) + self.forwardButton.setEnabled( + self.__currentHistoryIndex < len(self.__history) - 1 + ) + + @pyqtSlot() + def on_backButton_clicked(self): + """ + Private slot to move back in history of visited directories. + """ + self.setDirectory(self.__history[self.__currentHistoryIndex - 1]) + + @pyqtSlot() + def on_forwardButton_clicked(self): + """ + Private slot to move forward in history of visited directories. + """ + self.setDirectory(self.__history[self.__currentHistoryIndex + 1]) + + @pyqtSlot() + def __updateUpButton(self): + """ + Private slot to update the enabled state of the 'Up' button. + """ + self.upButton.setEnabled( + self.treeCombo.currentIndex() < self.treeCombo.count() - 1 + ) + + @pyqtSlot() + def on_upButton_clicked(self): + """ + Private slot to move up one level in the hierarchy. + """ + self.treeCombo.setCurrentIndex(self.treeCombo.currentIndex() + 1) + + @pyqtSlot() + def on_newDirButton_clicked(self): + """ + Private slot to create a new directory. + """ + newDir, ok = QInputDialog.getText( + self, + self.tr("New Directory"), + self.tr("Enter the name for the new directory:"), + QLineEdit.EchoMode.Normal, + ) + if ok and newDir: + if newDir in self.__directoryCache or newDir in self.__filenameCache: + EricMessageBox.warning( + self, + self.tr("New Directory"), + self.tr( + "<p>A file or directory with the name <b>{0}</b> exists" + " already. Aborting...</p>" + ).format(newDir), + ) + return + + ok, error = self.__fsInterface.mkdir(self.__getFullPath(newDir)) + if ok: + # refresh + self.__reload() + else: + EricMessageBox.critical( + self, + self.tr("New Directory"), + self.tr( + "<p>The directory <b>{0}</b> could not be created.</p>" + "<p>Reason: {1}</p>" + ).format( + self.__getFullPath(newDir), + error if error else self.tr("Unknown"), + ), + ) + + @pyqtSlot() + def __reload(self): + """ + Private slot to reload the directory listing. + """ + self.setDirectory(self.treeCombo.currentText()) + + @pyqtSlot(QTreeWidgetItem, int) + def on_listing_itemActivated(self, item, column): + """ + Private slot to handle the activation of an item in the list. + + @param item reference to the activated item + @type QTreeWidgetItem + @param column column number (unused) + @type int + """ + if item.data(0, EricServerFileDialog.IsDirectoryRole): + self.setDirectory(self.__getFullPath(item.text(0))) + else: + self.accept() + + @pyqtSlot() + def on_listing_itemSelectionChanged(self): + """ + Private slot to handle the selection of listed items. + """ + for itm in self.listing.selectedItems(): + if itm.data(0, EricServerFileDialog.IsDirectoryRole): + self.__selectedDirectory = itm.text(0) + break + else: + self.__selectedDirectory = None + + selected = [] + for itm in self.listing.selectedItems(): + isDir = itm.data(0, EricServerFileDialog.IsDirectoryRole) + if self.__fileMode == FileMode.Directory and isDir: + selected.append(itm.text(0)) + elif not isDir: + selected.append(itm.text(0)) + + if len(selected) == 1: + self.nameEdit.setText(selected[0]) + elif len(selected) > 1: + self.nameEdit.setText('"{0}"'.format('" "'.join(selected))) + + self.__updateOkButton() + + @pyqtSlot() + def __nameCompleterActivated(self): + """ + Private slot handling the activation of the completer. + """ + if self.okButton.isEnabled(): + self.okButton.animateClick() + + @pyqtSlot(str) + def on_nameEdit_textChanged(self, name): + """ + Private slot handling the editing of a file or directory name. + + @param name current text of the name edit + @type str + """ + self.listing.clearSelection() + items = self.listing.findItems(name, Qt.MatchFlag.MatchExactly) + for itm in items: + itm.setSelected(True) + + self.__updateOkButton() + + def __getNames(self): + """ + Private method to get the selected names list. + + @return list containing the selected names + @rtype list of str + """ + namesStr = self.nameEdit.text() + if namesStr.startswith('"'): + namesStr = namesStr[1:] + if namesStr.endswith('"'): + namesStr = namesStr[:-1] + names = re.split(r'"\s+"', namesStr) + return names + + def __getFullPath(self, name): + """ + Private method to get the full path for a given file or directory name. + + @param name name of the file or directory + @type str + @return full path of the file or directory + @rtype str + """ + return "{0}{1}{2}".format(self.treeCombo.currentText(), self.__sep, name) + + @pyqtSlot() + def __updateOkButton(self): + """ + Private slot to set the 'OK' button state, icon and label. + """ + # 1. adjust icon and label + if ( + self.__acceptMode == AcceptMode.AcceptOpen + or self.__selectedDirectory is not None + ): + self.okButton.setIcon(EricPixmapCache.getIcon("dialog-ok")) + if self.__fileMode != FileMode.Directory: + self.okButton.setText(self.tr("Open")) + else: + self.okButton.setText(self.tr("Choose")) + else: + self.okButton.setIcon(EricPixmapCache.getIcon("fileSave")) + self.okButton.setText(self.tr("Save")) + + # 2. adjust enabled state + if self.__selectedDirectory and self.__fileMode != FileMode.Directory: + self.okButton.setEnabled(True) + elif self.__fileMode == FileMode.AnyFile: + self.okButton.setEnabled(bool(self.nameEdit.text())) + elif self.__fileMode == FileMode.ExistingFile: + self.okButton.setEnabled(self.nameEdit.text() in self.__filenameCache) + elif self.__fileMode == FileMode.ExistingFiles: + names = self.__getNames() + self.okButton.setEnabled(all(n in self.__filenameCache for n in names)) + elif self.__fileMode == FileMode.Directory: + self.okButton.setEnabled(True) + else: + self.okButton.setEnabled(False) + + @pyqtSlot() + def on_okButton_clicked(self): + """ + Private slot handling the press of the OK button. + """ + if self.__selectedDirectory and self.__fileMode != FileMode.Directory: + self.setDirectory(self.__getFullPath(self.__selectedDirectory)) + else: + self.accept() + + @pyqtSlot(int) + def on_filterCombo_currentIndexChanged(self, index): + """ + Private slot handling the selection of a new file filter.. + + @param index index of the selected entry + @type int + """ + filters = self.__filters[index] + self.__filterList(filters) + + @pyqtSlot(str) + def setDirectory(self, directory): + """ + Public slot to set the current directory and populate the tree list. + + @param directory directory to be set as current. An empty directory sets the + server's current directory. + @type str + """ + self.__filenameCache.clear() + self.__directoryCache.clear() + + try: + directory, sep, dirListing = self.__fsInterface.listdir(directory) + + self.__sep = sep + + # 1. populate the directory tree combo box + self.treeCombo.blockSignals(True) + self.treeCombo.clear() + if len(directory) > 1 and directory.endswith(sep): + directory = directory[:-1] + if len(directory) > 2 and directory[1] == ":": + # starts with a Windows drive letter + directory = directory[2:] + directoryParts = directory.split(sep) + while directoryParts: + if directoryParts[-1]: + self.treeCombo.addItem(sep.join(directoryParts)) + directoryParts.pop() + self.treeCombo.addItem(sep) + self.treeCombo.blockSignals(False) + + # 2. populate the directory listing + self.listing.clear() + for dirEntry in sorted( + dirListing, + key=lambda d: ( + " " + d["name"].lower() if d["is_dir"] else d["name"].lower() + ), + ): + if dirEntry["is_dir"]: + type_ = self.tr("Directory") + iconName = "dirClosed" + sizeStr = "" + self.__directoryCache.append(dirEntry["name"]) + else: + type_ = self.tr("File") + iconName = self.__iconProvider.fileIconName(dirEntry["name"]) + sizeStr = dataString(dirEntry["size"], QLocale.system()) + self.__filenameCache.append(dirEntry["name"]) + itm = QTreeWidgetItem( + self.listing, [dirEntry["name"], sizeStr, type_, dirEntry["mtime"]] + ) + itm.setIcon(0, EricPixmapCache.getIcon(iconName)) + itm.setTextAlignment(1, Qt.AlignmentFlag.AlignRight) + itm.setTextAlignment(2, Qt.AlignmentFlag.AlignHCenter) + itm.setData(0, EricServerFileDialog.IsDirectoryRole, dirEntry["is_dir"]) + + currentFilterIndex = self.filterCombo.currentIndex() + filters = ( + [] if currentFilterIndex == -1 else self.__filters[currentFilterIndex] + ) + self.__filterList(filters) + + # 3. add the directory to the history + self.__addToHistory(directory) + + except OSError as err: + EricMessageBox.critical( + self, + self.tr("Remote Directory Listung"), + self.tr( + "<p>The directory <b>{0}</b> could not be listed due to an error" + " reported by the eric-ide server.</p><p>Reason: {1}</p>" + ).format(directory, str(err)), + ) + + # 4. update some dependent states + self.nameEdit.clear() + self.__updateUpButton() + + @pyqtSlot(QPoint) + def on_listing_customContextMenuRequested(self, pos): + """ + Private slot to show a context menu. + + @param pos mouse pointer position to show the menu at + @type QPoint + """ + self.__contextMenu.clear() + + itm = self.listing.itemAt(pos) + if itm is not None: + self.__contextMenu.addAction( + self.tr("Rename"), lambda: self.__renameItem(itm) + ) + self.__contextMenu.addAction( + self.tr("Delete"), lambda: self.__deleteItem(itm) + ) + self.__contextMenu.addSeparator() + act = self.__contextMenu.addAction(self.tr("Show Hidden Files")) + act.setCheckable(True) + act.setChecked(self.__showHidden) + act.toggled.connect(self.__showHiddenToggled) + self.__contextMenu.addAction( + self.tr("New Directory"), self.on_newDirButton_clicked + ) + + self.__contextMenu.popup(self.listing.mapToGlobal(pos)) + + @pyqtSlot(QTreeWidgetItem) + def __renameItem(self, item): + """ + Private slot to rename the given file/directory item. + + @param item reference to the item to be renamed + @type QTreeWidgetItem + """ + title = ( + self.tr("Rename Directory") + if item.data(0, EricServerFileDialog.IsDirectoryRole) + else self.tr("Rename File") + ) + + newName, ok = QInputDialog.getText( + self, + title, + self.tr("<p>Enter the new name <b>{0}</b>:</p>").format(item.text(0)), + QLineEdit.EchoMode.Normal, + item.text(0), + ) + if ok and newName: + if newName in self.__directoryCache or newName in self.__filenameCache: + EricMessageBox.warning( + self, + title, + self.tr( + "<p>A file or directory with the name <b>{0}</b> exists" + " already. Aborting...</p>" + ).format(newName), + ) + return + + ok, error = self.__fsInterface.replace( + self.__getFullPath(item.text(0)), self.__getFullPath(newName) + ) + if ok: + # refresh + self.__reload() + else: + EricMessageBox.critical( + self, + title, + self.tr( + "<p>The renaming operation failed.</p><p>Reason: {0}</p>" + ).format(error if error else self.tr("Unknown")), + ) + + @pyqtSlot(QTreeWidgetItem) + def __deleteItem(self, item): + """ + Private slot to delete the given file/directory item. + + @param item reference to the item to be deleted + @type QTreeWidgetItem + """ + isDir = item.data(0, EricServerFileDialog.IsDirectoryRole) + if isDir: + title = self.tr("Delete Directory") + itemType = self.tr("directory") + else: + title = self.tr("Delete File") + itemType = self.tr("file") + + yes = EricMessageBox.yesNo( + self, + title, + self.tr("Shall the selected {0} really be deleted?").format(itemType), + ) + if yes: + ok, error = ( + self.__fsInterface.rmdir(self.__getFullPath(item.text(0))) + if isDir + else self.__fsInterface.remove(self.__getFullPath(item.text(0))) + ) + if ok: + # refresh + self.__reload() + else: + EricMessageBox.critical( + self, + title, + self.tr( + "<p>The deletion operation failed.</p><p>Reason: {0}</p>" + ).format(error if error else self.tr("Unknown")), + ) + + @pyqtSlot(bool) + def __showHiddenToggled(self, on): + """ + Private slot to handle toggling the display of hidden files/directories. + + @param on flag indicating to show hidden files and directories + @type bool + """ + self.__showHidden = on + filters = self.__filters[self.filterCombo.currentIndex()] + self.__filterList(filters) + + def selectedFiles(self): + """ + Public method to get the selected files or the current viewport path. + + @return selected files or current viewport path + @rtype str + """ + if self.__fileMode == FileMode.Directory and not self.nameEdit.text(): + return [self.treeCombo.currentText()] + else: + return [self.__getFullPath(n) for n in self.__getNames()] + + def selectedNameFilter(self): + """ + Public method to get the selected name filter. + + @return selected name filter + @rtype str + """ + return self.filterCombo.currentText() + + def __isHidden(self, name): + """ + Private method to check, if the given name is indicating a hidden file or + directory. + + @param name name of the file or directory + @type str + @return flag indicating a hidden file or directory + @rtype bool + """ + return name.startswith(".") or name.endswith("~") + + def __filterList(self, filters): + """ + Private method to filter the files and directories list based on the given + filters and whether hidden files/directories should be shown. + + @param filters list of filter patterns (only applied to files + @type list of str + """ + self.listing.clearSelection() + for row in range(self.listing.topLevelItemCount()): + itm = self.listing.topLevelItem(row) + name = itm.text(0) + if self.__dirsOnly and not itm.data( + 0, EricServerFileDialog.IsDirectoryRole + ): + itm.setHidden(True) + elif not self.__showHidden and self.__isHidden(name): + # applies to files and directories + itm.setHidden(True) + elif not itm.data(0, EricServerFileDialog.IsDirectoryRole): + # it is a file item, apply the filter + itm.setHidden(not any(fnmatch.fnmatch(name, pat) for pat in filters)) + else: + itm.setHidden(False) + + # resize the columns + for column in range(4): + self.listing.resizeColumnToContents(column) + + +########################################################################### +## Module functions mimicing the interface of EricFileDialog/QFileDialog +########################################################################### + + +def getOpenFileName( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get the name of a file for opening it. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return name of file to be opened + @rtype str + """ + return getOpenFileNameAndFilter( + parent, caption, directory, filterStr, initialFilter, withRemote + )[0] + + +def getOpenFileNameAndFilter( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get the name of a file for opening it and the selected + file name filter. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return tuple containing the list of file names to be opened and the + selected file name filter + @rtype tuple of (list of str, str) + """ + dlg = EricServerFileDialog( + parent=parent, caption=caption, directory=directory, filter=filterStr + ) + dlg.setFileMode(FileMode.ExistingFile) + dlg.setNameFilter(initialFilter) + if dlg.exec() == QDialog.DialogCode.Accepted: + if withRemote: + fileName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0]) + else: + fileName = dlg.selectedFiles()[0] + selectedFilter = dlg.selectedNameFilter() + else: + fileName = "" + selectedFilter = "" + + return fileName, selectedFilter + + +def getOpenFileNames( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get a list of names of files for opening. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return list of file names to be opened + @rtype list of str + """ + return getOpenFileNamesAndFilter( + parent, caption, directory, filterStr, initialFilter, withRemote + )[0] + + +def getOpenFileNamesAndFilter( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get a list of names of files for opening and the + selected file name filter. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return tuple containing the list of file names to be opened and the + selected file name filter + @rtype tuple of (list of str, str) + """ + dlg = EricServerFileDialog( + parent=parent, caption=caption, directory=directory, filter=filterStr + ) + dlg.setFileMode(FileMode.ExistingFiles) + dlg.setNameFilter(initialFilter) + if dlg.exec() == QDialog.DialogCode.Accepted: + if withRemote: + filesList = [ + FileSystemUtilities.remoteFileName(f) for f in dlg.selectedFiles() + ] + else: + filesList = dlg.selectedFiles() + selectedFilter = dlg.selectedNameFilter() + else: + filesList = [] + selectedFilter = "" + + return filesList, selectedFilter + + +def getSaveFileName( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get the name of a file for saving. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return name of file to be saved + @rtype str + """ + return getSaveFileNameAndFilter( + parent, caption, directory, filterStr, initialFilter, withRemote + )[0] + + +def getSaveFileNameAndFilter( + parent=None, + caption="", + directory="", + filterStr="", + initialFilter="", + withRemote=True, +): + """ + Module function to get the name of a file for saving and the selected file name + filter. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param filterStr filter string for the dialog (defaults to "") + @type str (optional) + @param initialFilter initial filter for the dialog (defaults to "") + @type str (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return name of file to be saved and selected filte + @rtype tuple of (str, str) + """ + dlg = EricServerFileDialog( + parent=parent, caption=caption, directory=directory, filter=filterStr + ) + dlg.setFileMode(FileMode.AnyFile) + dlg.setNameFilter(initialFilter) + if dlg.exec() == QDialog.DialogCode.Accepted: + if withRemote: + fileName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0]) + else: + fileName = dlg.selectedFiles()[0] + selectedFilter = dlg.selectedNameFilter() + else: + fileName = "" + selectedFilter = "" + + return fileName, selectedFilter + + +def getExistingDirectory( + parent=None, caption="", directory="", dirsOnly=True, withRemote=True +): + """ + Module function to get the name of a directory. + + @param parent parent widget of the dialog (defaults to None) + @type QWidget (optional) + @param caption window title of the dialog (defaults to "") + @type str (optional) + @param directory working directory of the dialog (defaults to "") + @type str (optional) + @param dirsOnly flag indicating to just show directories (defaults to True) + @type bool (optional) + @param withRemote flag indicating to create the file names with the remote + indicator (defaults to True) + @type bool (optional) + @return name of selected directory + @rtype str + """ + dlg = EricServerFileDialog(parent=parent, caption=caption, directory=directory) + dlg.setFileMode(FileMode.Directory) + dlg.setDirectoriesOnly(dirsOnly) + if dlg.exec() == QDialog.DialogCode.Accepted: + if withRemote: + dirName = FileSystemUtilities.remoteFileName(dlg.selectedFiles()[0]) + else: + dirName = dlg.selectedFiles()[0] + else: + dirName = "" + + return dirName
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerFileDialog.ui Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,187 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EricServerFileDialog</class> + <widget class="QDialog" name="EricServerFileDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>600</width> + <height>450</height> + </rect> + </property> + <property name="windowTitle"> + <string>Open Files</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Look in:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="treeCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="backButton"> + <property name="toolTip"> + <string>Press to move back in history.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="forwardButton"> + <property name="toolTip"> + <string>Press to move forward in history.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="upButton"> + <property name="toolTip"> + <string>Press to move up one level.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="newDirButton"> + <property name="toolTip"> + <string>Press to create a new directory.</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="reloadButton"> + <property name="toolTip"> + <string>Press to reload the directory listing.</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTreeWidget" name="listing"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="rootIsDecorated"> + <bool>false</bool> + </property> + <property name="itemsExpandable"> + <bool>false</bool> + </property> + <property name="sortingEnabled"> + <bool>false</bool> + </property> + <column> + <property name="text"> + <string>Name</string> + </property> + </column> + <column> + <property name="text"> + <string>Size</string> + </property> + </column> + <column> + <property name="text"> + <string>Type</string> + </property> + </column> + <column> + <property name="text"> + <string>Date Modified</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QLabel" name="nameLabel"> + <property name="text"> + <string>File Name:</string> + </property> + </widget> + </item> + <item row="0" column="1"> + <widget class="QLineEdit" name="nameEdit"> + <property name="clearButtonEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QPushButton" name="okButton"> + <property name="text"> + <string>Open</string> + </property> + <property name="default"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="filterLabel"> + <property name="text"> + <string>Files of Type:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QComboBox" name="filterCombo"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="2"> + <widget class="QPushButton" name="cancelButton"> + <property name="text"> + <string>Cancel</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + <tabstops> + <tabstop>nameEdit</tabstop> + <tabstop>filterCombo</tabstop> + <tabstop>okButton</tabstop> + <tabstop>cancelButton</tabstop> + <tabstop>treeCombo</tabstop> + <tabstop>backButton</tabstop> + <tabstop>forwardButton</tabstop> + <tabstop>upButton</tabstop> + <tabstop>newDirButton</tabstop> + <tabstop>reloadButton</tabstop> + <tabstop>listing</tabstop> + </tabstops> + <resources/> + <connections/> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,1102 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the file system interface to the eric-ide server. +""" + +import base64 +import contextlib +import os +import re +import stat + +from PyQt6.QtCore import QEventLoop, QObject, pyqtSlot + +from eric7 import Utilities +from eric7.RemoteServer.EricRequestCategory import EricRequestCategory +from eric7.SystemUtilities import FileSystemUtilities + + +class EricServerFileSystemInterface(QObject): + """ + Class implementing the file system interface to the eric-ide server. + """ + + _MagicCheck = re.compile("([*?[])") + + def __init__(self, serverInterface): + """ + Constructor + + @param serverInterface reference to the eric-ide server interface + @type EricServerInterface + """ + super().__init__(parent=serverInterface) + + self.__serverInterface = serverInterface + self.__serverInterface.connectionStateChanged.connect( + self.__connectionStateChanged + ) + + self.__serverPathSep = self.__getPathSep() + + def __hasMagic(self, pathname): + """ + Private method to check, if a given path contains glob style magic characters. + + Note: This was taken from 'glob.glob'. + + @param pathname path name to be checked + @type str + @return flag indicating the presence of magic characters + @rtype bool + """ + match = self._MagicCheck.search(pathname) + return match is not None + + @pyqtSlot(bool) + def __connectionStateChanged(self, connected): + """ + Private slot handling a change of the server connection state. + + @param connected flag indicating a connected state + @type bool + """ + if connected: + if not bool(self.__serverPathSep): + self.__serverPathSep = self.__getPathSep() + else: + self.__serverPathSep = "" + + def __getPathSep(self): + """ + Private method to get the path separator of the connected server. + + @return path separator character of the server + @rtype str + """ + loop = QEventLoop() + sep = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal sep + + if reply == "GetPathSep": + sep = params["separator"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="GetPathSep", + params={}, + callback=callback, + ) + + loop.exec() + + return sep + + def getcwd(self): + """ + Public method to get the current working directory of the eric-ide server. + + @return current working directory of the eric-ide server + @rtype str + """ + loop = QEventLoop() + cwd = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal cwd + + if reply == "Getcwd": + cwd = params["directory"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Getcwd", + params={}, + callback=callback, + ) + + loop.exec() + + return cwd + + def chdir(self, directory): + """ + Public method to change the current working directory of the eric-ide server. + + @param directory absolute path of the working directory to change to + @type str + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "Chdir": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Chdir", + params={"directory": FileSystemUtilities.plainFileName(directory)}, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def listdir(self, directory=""): + """ + 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) + @return tuple containing the listed directory, the path separartor and the + directory listing. Each directory listing entry contains a dictionary + with the relevant data. + @rtype tuple of (str, str, dict) + @exception OSError raised in case the server reported an issue + """ + if directory is None: + # sanitize the directory in case it is None + directory = "" + + loop = QEventLoop() + ok = False + error = "" + listedDirectory = "" + separator = "" + listing = [] + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal listedDirectory, listing, separator, ok, error + + if reply == "Listdir": + ok = params["ok"] + if ok: + listedDirectory = params["directory"] + listing = params["listing"] + separator = params["separator"] + else: + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Listdir", + params={"directory": FileSystemUtilities.plainFileName(directory)}, + callback=callback, + ) + + loop.exec() + if not ok: + raise OSError(error) + + return listedDirectory, separator, listing + + def direntries( + self, + directory, + filesonly=False, + pattern=None, + followsymlinks=True, + ignore=None, + recursive=True, + dirsonly=False, + ): + """ + Public method to get a list of all files and directories of a given directory. + + @param directory root of the tree to check + @type str + @param filesonly flag indicating that only files are wanted (defaults to False) + @type bool (optional) + @param pattern a filename pattern or list of filename patterns to check + against (defaults to None) + @type str or list of str (optional) + @param followsymlinks flag indicating whether symbolic links should be + followed (defaults to True) + @type bool (optional) + @param ignore list of entries to be ignored (defaults to None) + @type list of str (optional) + @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) + @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. + @rtype list of str + @exception OSError raised in case the server reported an issue + """ + loop = QEventLoop() + ok = False + error = "" + result = [] + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal result, ok, error + + if reply == "DirEntries": + ok = params["ok"] + if ok: + result = params["result"] + else: + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="DirEntries", + params={ + "directory": FileSystemUtilities.plainFileName(directory), + "files_only": filesonly, + "pattern": [] if pattern is None else pattern, + "follow_symlinks": followsymlinks, + "ignore": [] if ignore is None else ignore, + "recursive": recursive, + "dirs_only": dirsonly, + }, + callback=callback, + ) + + loop.exec() + if not ok: + raise OSError(error) + + return result + + def glob(self, pathname, recursive=False, includeHidden=False): + """ + Public method to get a list of of all files matching a given pattern + like 'glob.glob()'. + + @param pathname path name pattern with simple shell-style wildcards + @type str + @param recursive flag indicating a recursive list (defaults to False) + @type bool (optional) + @param includeHidden flag indicating to include hidden files (defaults to False) + @type bool (optional) + @return list of all files matching the pattern + @rtype list of str + """ + result = [] + + pathname = FileSystemUtilities.plainFileName(pathname) + dirname, basename = os.path.split(pathname) + if dirname and not self.__hasMagic(dirname): + with contextlib.suppress(OSError): + entries = self.direntries( + dirname, pattern=basename, recursive=recursive, filesonly=True + ) + result = ( + entries + if includeHidden + else [e for e in entries if not e.startswith(".")] + ) + + return result + + def stat(self, filename, stNames): + """ + Public method to get the status of a file. + + @param filename name of the file + @type str + @param stNames list of 'stat_result' members to retrieve + @type list of str + @return dictionary containing the requested status data + @rtype dict + @exception OSError raised in case the server reported an issue + """ + loop = QEventLoop() + ok = False + error = "" + stResult = {} + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error, stResult + + if reply == "Stat": + ok = params["ok"] + if ok: + stResult = params["result"] + else: + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Stat", + params={ + "filename": FileSystemUtilities.plainFileName(filename), + "st_names": stNames, + }, + callback=callback, + ) + + loop.exec() + if not ok: + raise OSError(error) + + return stResult + + def isdir(self, name): + """ + Public method to check, if the given name is a directory. + + @param name name to be checked + @type str + @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"]) + + return False + + def isfile(self, name): + """ + Public method to check, if the given name is a regular file. + + @param name name to be checked + @type str + @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"]) + + return False + + def exists(self, name): + """ + Public method the existence of a file or directory. + + @param name name of the file or directory + @type str + @return flag indicating the file existence + @rtype bool + """ + loop = QEventLoop() + nameExists = False + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal nameExists + + if reply == "Exists": + nameExists = params["exists"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Exists", + params={"name": FileSystemUtilities.plainFileName(name)}, + callback=callback, + ) + + loop.exec() + + return nameExists + + def access(self, name, modes): + """ + Public method to test the given access rights to a file or directory. + + The modes to check for are 'read', 'write' or 'execute' or any combination. + + @param name name of the file or directory + @type str + @param modes list of modes to check for + @type str or list of str + @return flag indicating the user has the asked for permissions + @rtype bool + @exception ValueError raised for an illegal modes list + """ + if not modes: + raise ValueError( + "At least one of 'read', 'write' or 'execute' must be specified." + ) + + if isinstance(modes, str): + # convert to a list with one element + modes = [modes] + + loop = QEventLoop() + accessOK = False + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal accessOK + + if reply == "Access": + accessOK = params["ok"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Access", + params={ + "name": FileSystemUtilities.plainFileName(name), + "modes": modes, + }, + callback=callback, + ) + + loop.exec() + + return accessOK + + def mkdir(self, directory): + """ + Public method to create a new directory on the eric-ide server. + + @param directory absolute path of the new directory + @type str + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "Mkdir": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Mkdir", + params={"directory": FileSystemUtilities.plainFileName(directory)}, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def makedirs(self, directory, exist_ok=False): + """ + Public method to create a new directory on the eric-ide serverincluding all + intermediate-level directories. + + @param directory absolute path of the new directory + @type str + @param exist_ok flag indicating that the existence of the directory is + acceptable (defaults to False) + @type bool (optional) + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "MakeDirs": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="MakeDirs", + params={ + "directory": FileSystemUtilities.plainFileName(directory), + "exist_ok": exist_ok, + }, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def rmdir(self, directory): + """ + Public method to delete a directory on the eric-ide server. + + @param directory absolute path of the directory + @type str + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "Rmdir": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Rmdir", + params={"directory": FileSystemUtilities.plainFileName(directory)}, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def replace(self, oldName, newName): + """ + Public method to rename a file or directory. + + @param oldName current name of the file or directory + @type str + @param newName new name for the file or directory + @type str + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "Replace": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Replace", + params={ + "old_name": FileSystemUtilities.plainFileName(oldName), + "new_name": FileSystemUtilities.plainFileName(newName), + }, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + def remove(self, filename): + """ + Public method to delete a file on the eric-ide server. + + @param filename absolute path of the file + @type str + @return tuple containing an OK flag and an error string in case of an issue + @rtype tuple of (bool, str) + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "Remove": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if self.__serverInterface.isServerConnected(): + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="Remove", + params={"filename": FileSystemUtilities.plainFileName(filename)}, + callback=callback, + ) + + loop.exec() + return ok, error + + else: + return False, "Not connected to an 'eric-ide' server." + + ####################################################################### + ## Methods for splitting or joining remote path names. + ## + ## These are simplified variants of the os.path functions. If the + ## 'eric-ide' server is not connected, the os.path functions are used. + ####################################################################### + + def separator(self): + """ + Public method to return the server side path separator string. + + @return path separator + @rtype str + """ + return self.__serverPathSep + + def join(self, a, *p): + """ + Public method to join two or more path name components using the path separator + of the server side. + + @param a first path component + @type str + @param *p list of additional path components + @type list of str + @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) + + def split(self, p): + """ + Public method to split a path name. + + @param p path name to be split + @type str + @return tuple containing head and tail, where tail is everything after the last + 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("\\", "/") + + 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) + + def splitext(self, p): + """ + Public method to split a path name into a root part and an extension. + + @param p path name to be split + @type str + @return tuple containing the root part and the extension + @rtype tuple of (str, str) + """ + return os.path.splitext(p) + + def splitdrive(self, p): + """ + Public method to split a path into drive and path. + + @param p path name to be split + @type str + @return tuple containing the drive letter (incl. colon) and the path + @rtype tuple of (str, str) + """ + if self.__serverInterface.isServerConnected(): + 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 + else: + # remote is a Posix system + normp = plainp.replace("\\", "/") + return "", normp + + else: + return os.path.splitdrive(p) + + def dirname(self, p): + """ + Public method to extract the directory component of a path name. + + @param p path name + @type str + @return directory component + @rtype str + """ + return self.split(p)[0] + + def basename(self, p): + """ + Public method to extract the final component of a path name. + + @param p path name + @type str + @return final component + @rtype str + """ + return self.split(p)[1] + + def toNativeSeparators(self, p): + """ + Public method to convert a path to use server native separator characters. + + @param p path name to be converted + @type str + @return path name with converted separator characters + @rtype str + """ + if self.__serverPathSep == "/": + return p.replace("\\", "/") + else: + return p.replace("/", "\\") + + def fromNativeSeparators(self, p): + """ + Public method to convert a path using server native separator characters to + use "/" separator characters. + + @param p path name to be converted + @type str + @return path name with converted separator characters + @rtype str + """ + return p.replace(self.__serverPathSep, "/") + + ####################################################################### + ## Methods for reading and writing files + ####################################################################### + + def readFile(self, filename, create=False): + """ + Public method to read a file from the eric-ide server. + + @param filename name of the file to read + @type str + @param create flag indicating to create an empty file, if it does not exist + (defaults to False) + @type bool (optional) + @return bytes data read from the eric-ide server + @rtype bytes + @exception OSError raised in case the server reported an issue + """ + loop = QEventLoop() + ok = False + error = "" + bText = b"" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error, bText + + if reply == "ReadFile": + ok = params["ok"] + if ok: + bText = base64.b85decode( + bytes(params["filedata"], encoding="ascii") + ) + else: + error = params["error"] + loop.quit() + + if not self.__serverInterface.isServerConnected(): + raise OSError("Not connected to an 'eric-ide' server.") + + else: + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="ReadFile", + params={ + "filename": FileSystemUtilities.plainFileName(filename), + "create": create, + }, + callback=callback, + ) + + loop.exec() + if not ok: + raise OSError(error) + + return bText + + def writeFile(self, filename, data, withBackup=False): + """ + 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 + @param withBackup flag indicating to create a backup file first + (defaults to False) + @type bool (optional) + @exception OSError raised in case the server reported an issue + """ + loop = QEventLoop() + ok = False + error = "" + + def callback(reply, params): + """ + Function to handle the server reply + + @param reply name of the server reply + @type str + @param params dictionary containing the reply data + @type dict + """ + nonlocal ok, error + + if reply == "WriteFile": + ok = params["ok"] + with contextlib.suppress(KeyError): + error = params["error"] + loop.quit() + + if not self.__serverInterface.isServerConnected(): + raise OSError("Not connected to an 'eric-ide' server.") + + else: + self.__serverInterface.sendJson( + category=EricRequestCategory.FileSystem, + request="WriteFile", + params={ + "filename": FileSystemUtilities.plainFileName(filename), + "filedata": str(base64.b85encode(data), encoding="ascii"), + "with_backup": withBackup, + }, + callback=callback, + ) + + loop.exec() + if not ok: + raise OSError(error) + + def readEncodedFile(self, filename, create=False): + """ + Public method to read a file and decode its contents into proper text. + + @param filename name of the file to read + @type str + @param create flag indicating to create an empty file, if it does not exist + (defaults to False) + @type bool (optional) + @return tuple of decoded text and encoding + @rtype tuple of (str, str) + """ + data = self.readFile(filename, create=create) + return Utilities.decode(data) + + def readEncodedFileWithEncoding(self, filename, encoding, create=False): + """ + Public method to read a file and decode its contents into proper text. + + @param filename name of the file to read + @type str + @param encoding encoding to be used to read the file + @type str + @param create flag indicating to create an empty file, if it does not exist + (defaults to False) + @type bool (optional) + @return tuple of decoded text and encoding + @rtype tuple of (str, str) + """ + data = self.readFile(filename, create=create) + return Utilities.decodeWithEncoding(data, encoding) + + def writeEncodedFile( + self, filename, text, origEncoding, forcedEncoding="", withBackup=False + ): + """ + Public method to write a file with properly encoded text. + + @param filename name of the file to read + @type str + @param text text to be written + @type str + @param origEncoding type of the original encoding + @type str + @param forcedEncoding encoding to be used for writing, if no coding + line is present (defaults to "") + @type str (optional) + @param withBackup flag indicating to create a backup file first + (defaults to False) + @type bool (optional) + @return encoding used for writing the file + @rtype str + """ + data, encoding = Utilities.encode( + text, origEncoding, forcedEncoding=forcedEncoding + ) + self.writeFile(filename, data, withBackup=withBackup) + + return encoding
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerInterface.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,763 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the interface to the eric remote server. +""" + +import collections +import json +import struct +import uuid +import zlib + +from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt6.QtGui import QAction, QKeySequence +from PyQt6.QtNetwork import QAbstractSocket, QTcpSocket +from PyQt6.QtWidgets import QDialog, QMenu, QToolBar, QToolButton + +from eric7 import Preferences, Utilities +from eric7.EricGui import EricPixmapCache +from eric7.EricGui.EricAction import EricAction +from eric7.EricWidgets import EricMessageBox +from eric7.RemoteServer.EricRequestCategory import EricRequestCategory + + +class EricServerInterface(QObject): + """ + Class implementing the interface to the eric remote server. + + @signal showMenu(name:str, menu:QMenu) emitted when a menu is about to be shown. + The name of the menu and a reference to the menu are given. + + @signal connectionStateChanged(state:bool) emitted to indicate a change of the + connection state + @signal aboutToDisconnect() emitted just befor the remote server is disconnected + + @signal remoteReply(category:int, request:str, params:dict) emitted to deliver the + reply of an unknown category + @signal remoteCoverageReply(request:str, params:dict) emitted to deliver the reply + of a remote server code coverage request + @signal remoteDebuggerReply(request:str, params:dict) emitted to deliver the reply + of a remote server debugger request + @signal remoteEchoReply(request:str, params:dict) emitted to deliver the reply of + a remote server echo request + @signal remoteFileSystemReply(request:str, params:dict) emitted to deliver the + reply of a remote server file system request + @signal remoteProjectReply(request:str, params:dict) emitted to deliver the reply + of a remote server project related request + @signal remoteServerReply(request:str, params:dict) emitted to deliver the reply + of a remote server control request + """ + + showMenu = pyqtSignal(str, QMenu) + + aboutToDisconnect = pyqtSignal() + connectionStateChanged = pyqtSignal(bool) + + remoteReply = pyqtSignal(int, str, dict) + + remoteCoverageReply = pyqtSignal(str, dict) + remoteDebuggerReply = pyqtSignal(str, dict) + remoteEchoReply = pyqtSignal(str, dict) + remoteFileSystemReply = pyqtSignal(str, dict) + remoteProjectReply = pyqtSignal(str, dict) + remoteServerReply = pyqtSignal(str, dict) + + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent object (defaults to None) + @type QObject (optional) + """ + super().__init__(parent=parent) + + self.__ui = parent + + self.__categorySignalMapping = { + EricRequestCategory.Coverage: self.remoteCoverageReply, + EricRequestCategory.Debugger: self.remoteDebuggerReply, + EricRequestCategory.Echo: self.remoteEchoReply, + EricRequestCategory.FileSystem: self.remoteFileSystemReply, + EricRequestCategory.Project: self.remoteProjectReply, + EricRequestCategory.Server: self.remoteServerReply, + } + self.__serviceInterfaces = {} + # no specific service interfaces have been created yet + + self.__connection = None + self.__callbacks = {} # callback references indexed by UUID + self.__messageQueue = collections.deque() + self.__connected = False + + self.connectionStateChanged.connect(self.__connectionStateChanged) + + def getServiceInterface(self, name): + """ + Public method to get a references to a specific service interface by + service name. + + @param name service name + @type str + @return reference to the service interface + @rtype QObject + @exception ValueError raised to indicate an unsupported server interface + was requested + """ + lname = name.lower() + try: + return self.__serviceInterfaces[lname] + except KeyError: + if lname not in ("coverage", "debugger", "filesystem", "project"): + raise ValueError(f"no such service supported ({name})") + else: + # instantiate the service interface + if lname == "filesystem": + from .EricServerFileSystemInterface import ( # noqa: I101 + EricServerFileSystemInterface, + ) + + self.__serviceInterfaces[lname] = EricServerFileSystemInterface( + self + ) + elif lname == "debugger": + from .EricServerDebuggerInterface import ( # noqa: I101 + EricServerDebuggerInterface, + ) + + self.__serviceInterfaces[lname] = EricServerDebuggerInterface(self) + elif lname == "coverage": + from .EricServerCoverageInterface import ( # noqa: I101 + EricServerCoverageInterface, + ) + + self.__serviceInterfaces[lname] = EricServerCoverageInterface(self) + elif lname == "project": + # TODO: 'Project Interface' not implemented yet + pass + + return self.__serviceInterfaces[lname] + + ####################################################################### + ## Methods for handling the server connection. + ####################################################################### + + def connectToServer(self, host, port=None, timeout=None): + """ + Public method to connect to the given host and port. + + @param host host name or IP address of the eric remote server + @type str + @param port port number to connect to (defaults to None) + @type int (optional) + @param timeout timeout im seconds for the connection attempt + (defaults to None) + @type int (optional) + @return flag indicating success + @rtype bool + """ + if not bool(port): # None or 0 + # use default port + port = 42024 + + if not bool(timeout): # None or 0 + # use configured default timeout + timeout = Preferences.getEricServer("ConnectionTimeout") + timeout *= 1000 # convert to milliseconds + + if self.__connection is not None: + self.disconnectFromServer() + + self.__connection = QTcpSocket(self) + self.__connection.connectToHost(host, port) + if not self.__connection.waitForConnected(timeout): + EricMessageBox.critical( + None, + self.tr("Connect to eric-ide Server"), + self.tr( + "<p>The connection to the eric-ide server {0}:{1} could not be" + " established.</p><p>Reason: {2}</p>" + ).format( + host if ":" not in host else f"[{host}]", + port, + self.__connection.errorString(), + ), + ) + + self.__connection = None + return False + + self.__connection.readyRead.connect(self.__receiveJson) + self.__connection.disconnected.connect(self.__handleDisconnect) + + self.connectionStateChanged.emit(True) + + return True + + @pyqtSlot() + def disconnectFromServer(self): + """ + Public method to disconnect from the eric remote server. + """ + if self.__connection is not None and self.__connection.isValid(): + # signal we are abouzt to disconnect + self.aboutToDisconnect.emit() + + # disconnect from the eric-ide server + self.__connection.disconnectFromHost() + if self.__connection is not None: + # may have disconnected already + self.__connection.waitForDisconnected( + Preferences.getEricServer("ConnectionTimeout") * 1000 + ) + + self.connectionStateChanged.emit(False) + self.__connection = None + self.__callbacks.clear() + + def isServerConnected(self): + """ + Public method to check, if a connection to an eric-ide server has been + established. + + @return flag indicating the interface connection state + @rtype bool + """ + return ( + self.__connection is not None + and self.__connection.state() == QAbstractSocket.SocketState.ConnectedState + ) + + @pyqtSlot() + def __handleDisconnect(self): + """ + Private slot handling a disconnect of the client. + """ + if self.__connection is not None: + self.__connection.close() + + self.connectionStateChanged.emit(False) + self.__connection = None + self.__callbacks.clear() + + ####################################################################### + ## Methods for sending requests and receiving the replies. + ####################################################################### + + @pyqtSlot() + def __receiveJson(self): + """ + Private slot handling received data from the eric remote server. + """ + while self.__connection and self.__connection.bytesAvailable(): + header = self.__connection.read(struct.calcsize(b"!II")) + length, datahash = struct.unpack(b"!II", header) + + data = bytearray() + while len(data) < length: + maxSize = length - len(data) + if self.__connection.bytesAvailable() < maxSize: + self.__connection.waitForReadyRead(50) + if not self.__connection: + # connection to server is gone uncontrolled + break + newData = self.__connection.read(maxSize) + if newData: + data += newData + + if zlib.adler32(data) & 0xFFFFFFFF != datahash: + # corrupted data -> discard and continue + continue + + jsonString = data.decode("utf-8", "backslashreplace") + + # - print("Remote Server Interface Receive: {0}".format(jsonString)) + # - this is for debugging only + + try: + serverDataDict = json.loads(jsonString.strip()) + except (TypeError, ValueError) as err: + EricMessageBox.critical( + None, + self.tr("JSON Protocol Error"), + self.tr( + """<p>The response received from the remote server""" + """ could not be decoded. Please report""" + """ this issue with the received data to the""" + """ eric bugs email address.</p>""" + """<p>Error: {0}</p>""" + """<p>Data:<br/>{1}</p>""" + ).format(str(err), Utilities.html_encode(jsonString.strip())), + EricMessageBox.Ok, + ) + return + + reqUuid = serverDataDict["uuid"] + if reqUuid: + # It is a response to a synchronous request -> handle the call back + # immediately. + self.__callbacks[reqUuid]( + serverDataDict["reply"], serverDataDict["params"] + ) + del self.__callbacks[reqUuid] + else: + self.__messageQueue.append(serverDataDict) + + while self.__messageQueue: + serverDataDict = self.__messageQueue.popleft() # get the first message + try: + self.__categorySignalMapping[serverDataDict["category"]].emit( + serverDataDict["reply"], serverDataDict["params"] + ) + except KeyError: + if serverDataDict["category"] == EricRequestCategory.Error: + # handle server errors in here + self.__handleServerError( + serverDataDict["reply"], serverDataDict["params"] + ) + else: + self.remoteReply.emit( + serverDataDict["category"], + serverDataDict["reply"], + serverDataDict["params"], + ) + + def sendJson(self, category, request, params, callback=None, flush=False): + """ + Public method to send a single command to a client. + + @param category service category + @type EricRequestCategory + @param request request name to be sent + @type str + @param params dictionary of named parameters for the request + @type dict + @param callback callback function for the reply from the eric remote server + (defaults to None) + @type function (optional) + @param flush flag indicating to flush the data to the socket + (defaults to False) + @type bool (optional) + """ + if callback: + reqUuid = str(uuid.uuid4()) + self.__callbacks[reqUuid] = callback + else: + reqUuid = "" + + serviceDict = { + "jsonrpc": "2.0", + "category": category, + "request": request, + "params": params, + "uuid": reqUuid, + } + jsonString = json.dumps(serviceDict) + "\n" + + # - print("Remote Server Interface Send: {0}".format(jsonString)) + # - this is for debugging only + + if self.__connection is not None: + data = jsonString.encode("utf8", "backslashreplace") + header = struct.pack(b"!II", len(data), zlib.adler32(data) & 0xFFFFFFFF) + self.__connection.write(header) + self.__connection.write(data) + if flush: + self.__connection.flush() + + def shutdownServer(self): + """ + Public method shutdown the currebtly connected eric-ide remote server. + """ + if self.__connection: + self.sendJson( + category=EricRequestCategory.Server, + request="Shutdown", + params={}, + ) + + @pyqtSlot() + def serverVersions(self): + """ + Public slot to request the eric-ide version of the server. + """ + if self.__connection: + self.sendJson( + category=EricRequestCategory.Server, + request="Versions", + params={}, + callback=self.__handleServerVersionReply, + ) + + ####################################################################### + ## Callback methods + ####################################################################### + + def __handleServerVersionReply(self, reply, params): + """ + Private method to handle the reply of a 'Version' request. + + @param reply name of the eric-ide server reply + @type str + @param params dictionary containing the reply data + @type dict + @exception ValueError raised in case of an unsupported reply + """ + if reply != "Versions": + raise ValueError(f"unsupported reply received ({reply})") + + else: + hostname = params["hostname"] + versionText = self.tr("<h2>{0}Version Numbers</h2><table>").format( + self.tr("{0} - ").format(hostname) if hostname else "" + ) + + # Python version + versionText += ( + """<tr><td><b>Python</b></td><td>{0}, {1}</td></tr>""" + ).format(params["python"], params["py_bitsize"]) + + # eric7 version + versionText += ( + """<tr><td><b>eric7_server</b></td><td>{0}</td></tr>""" + ).format(params["version"]) + + versionText += self.tr("""</table>""") + + EricMessageBox.about( + None, + self.tr("eric-ide Server Versions"), + versionText, + ) + + ####################################################################### + ## Reply handler methods + ####################################################################### + + def __handleServerError(self, reply, params): + """ + Private method handling server error replies. + + @param reply name of the error reply + @type str + @param params dictionary containing the specific reply data + @type dict + """ + if reply == "ClientChecksumException": + self.__ui.appendToStderr( + self.tr( + "eric-ide Server Checksum Error\nError: {0}\nData:\n{1}\n" + ).format(params["ExceptionValue"], params["ProtocolData"]) + ) + + elif reply == "ClientException": + self.__ui.appendToStderr( + self.tr("eric-ide Server Data Error\nError: {0}\nData:\n{1}\n").format( + params["ExceptionValue"], params["ProtocolData"] + ) + ) + + elif reply == "UnsupportedServiceCategory": + self.__ui.appendToStderr( + self.tr( + "eric-ide Server Unsupported Category\n" + "Error: The server received the unsupported request category '{0}'." + ).format(params["Category"]) + ) + + ####################################################################### + ## User interface related methods + ####################################################################### + + def initActions(self): + """ + Public slot to initialize the eric-ide server actions. + """ + self.actions = [] + + self.connectServerAct = EricAction( + self.tr("Connect"), + EricPixmapCache.getIcon("linkConnect"), + self.tr("Connect..."), + QKeySequence(self.tr("Meta+Shift+C")), + 0, + self, + "remote_server_connect", + ) + self.connectServerAct.setStatusTip( + self.tr("Show a dialog to connect to an 'eric-ide' server") + ) + self.connectServerAct.setWhatsThis( + self.tr( + """<b>Connect...</b>""" + """<p>This opens a dialog to enter the connection parameters to""" + """ connect to a remote 'eric-ide' server.</p>""" + ) + ) + self.connectServerAct.triggered.connect(self.__connectToServer) + self.actions.append(self.connectServerAct) + + self.disconnectServerAct = EricAction( + self.tr("Disconnect"), + EricPixmapCache.getIcon("linkDisconnect"), + self.tr("Disconnect"), + QKeySequence(self.tr("Meta+Shift+D")), + 0, + self, + "remote_server_disconnect", + ) + self.disconnectServerAct.setStatusTip( + self.tr("Disconnect from the currently connected 'eric-ide' server") + ) + self.disconnectServerAct.setWhatsThis( + self.tr( + """<b>Disconnect</b>""" + """<p>This disconnects from the currently connected 'eric-ide'""" + """ server.</p>""" + ) + ) + self.disconnectServerAct.triggered.connect(self.disconnectFromServer) + self.actions.append(self.disconnectServerAct) + + self.stopServerAct = EricAction( + self.tr("Stop Server"), + EricPixmapCache.getIcon("stopScript"), + self.tr("Stop Server"), + QKeySequence(self.tr("Meta+Shift+S")), + 0, + self, + "remote_server_shutdown", + ) + self.stopServerAct.setStatusTip( + self.tr("Stop the currently connected 'eric-ide' server") + ) + self.stopServerAct.setWhatsThis( + self.tr( + """<b>Stop Server</b>""" + """<p>This stops the currently connected 'eric-ide server.</p>""" + ) + ) + self.stopServerAct.triggered.connect(self.__shutdownServer) + self.actions.append(self.stopServerAct) + + self.serverVersionsAct = EricAction( + self.tr("Show Server Versions"), + EricPixmapCache.getIcon("helpAbout"), + self.tr("Show Server Versions"), + 0, + 0, + self, + "remote_server_versions", + ) + self.serverVersionsAct.setStatusTip( + self.tr("Show the eric-ide server versions") + ) + self.serverVersionsAct.setWhatsThis( + self.tr( + """<b>Show Server Versions</b>""" + """<p>This opens a dialog to show the eric-ide server versions.</p>""" + ) + ) + self.serverVersionsAct.triggered.connect(self.serverVersions) + self.actions.append(self.serverVersionsAct) + + self.disconnectServerAct.setEnabled(False) + self.stopServerAct.setEnabled(False) + self.serverVersionsAct.setEnabled(False) + + def initMenu(self): + """ + Public slot to initialize the eric-ide server menu. + + @return the menu generated + @rtype QMenu + """ + self.__serverProfilesMenu = QMenu(self.tr("Connect to")) + self.__serverProfilesMenu.aboutToShow.connect(self.__showServerProfilesMenu) + self.__serverProfilesMenu.triggered.connect(self.__serverProfileTriggered) + + menu = QMenu(self.tr("eric-ide Server"), self.__ui) + menu.setTearOffEnabled(True) + menu.aboutToShow.connect(self.__showEricServerMenu) + menu.addAction(self.connectServerAct) + menu.addMenu(self.__serverProfilesMenu) + # TODO: add a 'Recent Connections' submenu + menu.addSeparator() + menu.addAction(self.disconnectServerAct) + menu.addSeparator() + menu.addAction(self.stopServerAct) + menu.addSeparator() + menu.addAction(self.serverVersionsAct) + + self.__menus = { + "Main": menu, + ##"Recent": self.recentMenu, + } + + return menu + + def initToolbar(self, toolbarManager): + """ + Public slot to initialize the eric-ide server toolbar. + + @param toolbarManager reference to a toolbar manager object + @type EricToolBarManager + @return the toolbar generated + @rtype QToolBar + """ + self.__connectButton = QToolButton() + self.__connectButton.setIcon(self.connectServerAct.icon()) + self.__connectButton.setToolTip(self.connectServerAct.toolTip()) + self.__connectButton.setWhatsThis(self.connectServerAct.whatsThis()) + self.__connectButton.setPopupMode( + QToolButton.ToolButtonPopupMode.MenuButtonPopup + ) + self.__connectButton.setMenu(self.__serverProfilesMenu) + self.connectServerAct.enabledChanged.connect(self.__connectButton.setEnabled) + self.__connectButton.clicked.connect(self.connectServerAct.triggered) + + tb = QToolBar(self.tr("eric-ide Server"), self.__ui) + tb.setObjectName("EricServerToolbar") + tb.setToolTip(self.tr("eric-ide Server")) + + act = tb.addWidget(self.__connectButton) + act.setText(self.connectServerAct.iconText()) + act.setIcon(self.connectServerAct.icon()) + tb.addAction(self.disconnectServerAct) + tb.addSeparator() + tb.addAction(self.stopServerAct) + tb.addSeparator() + tb.addAction(self.serverVersionsAct) + + toolbarManager.addToolBar(tb, tb.windowTitle()) + + return tb + + @pyqtSlot() + def __showEricServerMenu(self): + """ + Private slot to display the server menu. + """ + connected = self.isServerConnected() + self.connectServerAct.setEnabled(not connected) + self.disconnectServerAct.setEnabled(connected) + self.stopServerAct.setEnabled(connected) + self.serverVersionsAct.setEnabled(connected) + + self.showMenu.emit("Main", self.__menus["Main"]) + + @pyqtSlot() + def __showServerProfilesMenu(self): + """ + Private slot to prepare the eric server profiles menu. + """ + profiles = Preferences.getEricServer("ConnectionProfiles") + + self.__serverProfilesMenu.clear() + + if not self.isServerConnected(): + for profile in sorted(profiles): + act = self.__serverProfilesMenu.addAction(profile) + act.setData(profiles[profile]) + self.__serverProfilesMenu.addSeparator() + + self.__serverProfilesMenu.addAction( + self.tr("Manage Server Connections"), self.__manageServerProfiles + ) + + @pyqtSlot(bool) + def __connectionStateChanged(self, connected): + """ + Private slot to handle the connection state change. + + @param connected flag indicating the connection state + @type bool + """ + if connected != self.__connected: # prevent executing it twice in succession + self.__connected = connected + + self.connectServerAct.setEnabled(not connected) + self.disconnectServerAct.setEnabled(connected) + self.stopServerAct.setEnabled(connected) + self.serverVersionsAct.setEnabled(connected) + + if connected: + peerName = self.__connection.peerName() + EricMessageBox.information( + None, + self.tr("Connect to eric-ide Server"), + self.tr( + "<p>The eric-ide server at <b>{0}:{1}</b> was connected" + " successfully.</p>" + ).format( + f"[{peerName}]" if ":" in peerName else peerName, + self.__connection.peerPort(), + ), + ) + else: + EricMessageBox.information( + None, + self.tr("Disonnect from eric-ide Server"), + self.tr("""The eric-ide server was disconnected."""), + ) + + @pyqtSlot() + def __connectToServer(self): + """ + Private slot to connect to a remote eric-ide server. + """ + from .EricServerConnectionDialog import EricServerConnectionDialog + + dlg = EricServerConnectionDialog(parent=self.__ui) + if dlg.exec() == QDialog.DialogCode.Accepted: + hostname, port, timeout = dlg.getData() + self.connectToServer(hostname, port=port, timeout=timeout) + + @pyqtSlot() + def __shutdownServer(self): + """ + Private slot to shut down the currently connected eric-ide server. + """ + ok = EricMessageBox.yesNo( + None, + self.tr("Stop Server"), + self.tr( + "Do you really want to stop the currently connected eric-ide server?" + " No further connections will be possible without restarting the" + " server." + ), + ) + if ok: + self.shutdownServer() + + @pyqtSlot(QAction) + def __serverProfileTriggered(self, act): + """ + Private slot to handle the selection of a remote server connection. + + @param act reference to the triggered profile action + @type QAction + """ + data = act.data() + if data is not None: + # handle the connection + hostname, port, timeout = data + self.connectToServer(hostname, port=port, timeout=timeout) + + @pyqtSlot() + def __manageServerProfiles(self): + """ + Private slot to show a dialog to manage the eric-ide server connection + profiles. + """ + from .EricServerProfilesDialog import EricServerProfilesDialog + + dlg = EricServerProfilesDialog( + Preferences.getEricServer("ConnectionProfiles"), self.__ui + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + profiles = dlg.getConnectionProfiles() + Preferences.setEricServer("ConnectionProfiles", profiles)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a dialog to manage server connection profiles. +""" + +import copy + +from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtWidgets import QDialog, QListWidgetItem + +from eric7.EricWidgets import EricMessageBox + +from .EricServerConnectionDialog import EricServerConnectionDialog +from .Ui_EricServerProfilesDialog import Ui_EricServerProfilesDialog + + +class EricServerProfilesDialog(QDialog, Ui_EricServerProfilesDialog): + """ + Class implementing a dialog to manage server connection profiles. + """ + + def __init__(self, profiles, parent=None): + """ + Constructor + + @param profiles dictionary containing the server connection profiles + @type dict + @param parent reference to the parent widget (defaults to None) + @type QWidget (optional) + """ + super().__init__(parent) + self.setupUi(self) + + self.__profiles = copy.deepcopy(profiles) + self.__populateProfilesList() + + self.on_connectionsList_itemSelectionChanged() + + def __populateProfilesList(self): + """ + Private method to (re-) populate the list of server connection profiles. + """ + self.connectionsList.clear() + + for profile in self.__profiles: + itm = QListWidgetItem(profile, self.connectionsList) + itm.setData(Qt.ItemDataRole.UserRole, self.__profiles[profile]) + + def __getProfilesList(self): + """ + Private method to get the list of defined profile names. + + @return list of defined profile names + @rtype list of str + """ + profileNames = [] + for row in range(self.connectionsList.count()): + itm = self.connectionsList.item(row) + profileNames.append(itm.text()) + + return profileNames + + @pyqtSlot() + def on_connectionsList_itemSelectionChanged(self): + """ + Private slot to handle a change of selected items. + """ + selectedItems = self.connectionsList.selectedItems() + self.editButton.setEnabled(len(selectedItems) == 1) + self.removeButton.setEnabled(len(selectedItems) > 0) + + @pyqtSlot() + def on_addButton_clicked(self): + """ + Private slot add a new connection profile. + """ + dlg = EricServerConnectionDialog( + profileNames=self.__getProfilesList(), parent=self + ) + if dlg.exec() == QDialog.DialogCode.Accepted: + profileData = dlg.getProfileData() + itm = QListWidgetItem(profileData[0], self.connectionsList) + itm.setData(Qt.ItemDataRole.UserRole, profileData[1:]) + + @pyqtSlot() + def on_editButton_clicked(self): + """ + Private slot to edit the selected entry. + """ + selectedItems = self.connectionsList.selectedItems() + if selectedItems: + itm = selectedItems[0] + dlg = EricServerConnectionDialog( + profileNames=self.__getProfilesList(), parent=self + ) + data = itm.data(Qt.ItemDataRole.UserRole) + dlg.setProfileData(itm.text(), *data) + if dlg.exec() == QDialog.DialogCode.Accepted: + profileData = dlg.getProfileData() + itm.setText(profileData[0]) + itm.setData(Qt.ItemDataRole.UserRole, profileData[1:]) + + @pyqtSlot() + def on_removeButton_clicked(self): + """ + Private slot to remove the selected connection profiles. + """ + yes = EricMessageBox.yesNo( + self, + self.tr("Remove Selected Entries"), + self.tr("Do you really want to remove the selected entries from the list?"), + ) + if yes: + for itm in self.connectionsList.selectedItems()[:]: + self.connectionsList.takeItem(self.connectionsList.row(itm)) + del itm + + @pyqtSlot() + def on_resetButton_clicked(self): + """ + Private slot to reset all changes performed. + """ + yes = EricMessageBox.yesNo( + self, + self.tr("Reset Changes"), + self.tr( + "Do you really want to reset all changes performed up to this point?" + ), + ) + if yes: + self.__populateProfilesList() + + def getConnectionProfiles(self): + """ + Public method to get the configured connection profiles. + + @return dictionary containing the configured connection profiles + @rtype dict + """ + profiles = {} + + for row in range(self.connectionsList.count()): + itm = self.connectionsList.item(row) + profiles[itm.text()] = itm.data(Qt.ItemDataRole.UserRole) + + return profiles
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/EricServerProfilesDialog.ui Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,140 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>EricServerProfilesDialog</class> + <widget class="QDialog" name="EricServerProfilesDialog"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>441</width> + <height>281</height> + </rect> + </property> + <property name="windowTitle"> + <string>Manage Server Connections</string> + </property> + <property name="sizeGripEnabled"> + <bool>true</bool> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QListWidget" name="connectionsList"> + <property name="editTriggers"> + <set>QAbstractItemView::NoEditTriggers</set> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::ExtendedSelection</enum> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="0" column="1"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QPushButton" name="addButton"> + <property name="toolTip"> + <string>Press to open a dialog to add a new server connection.</string> + </property> + <property name="text"> + <string>Add...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="editButton"> + <property name="toolTip"> + <string>Press to open a dialog to edit the selected server connection.</string> + </property> + <property name="text"> + <string>Edit...</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="removeButton"> + <property name="toolTip"> + <string>Press to remove the selected server connections.</string> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + <item> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="resetButton"> + <property name="toolTip"> + <string>Press to reset all changes performed.</string> + </property> + <property name="text"> + <string>Reset</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0" colspan="2"> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="standardButtons"> + <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections> + <connection> + <sender>buttonBox</sender> + <signal>accepted()</signal> + <receiver>EricServerProfilesDialog</receiver> + <slot>accept()</slot> + <hints> + <hint type="sourcelabel"> + <x>248</x> + <y>254</y> + </hint> + <hint type="destinationlabel"> + <x>157</x> + <y>274</y> + </hint> + </hints> + </connection> + <connection> + <sender>buttonBox</sender> + <signal>rejected()</signal> + <receiver>EricServerProfilesDialog</receiver> + <slot>reject()</slot> + <hints> + <hint type="sourcelabel"> + <x>316</x> + <y>260</y> + </hint> + <hint type="destinationlabel"> + <x>286</x> + <y>274</y> + </hint> + </hints> + </connection> + </connections> +</ui>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/RemoteServerInterface/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Package implementing the components of the eric-ide remote server interface. +"""
--- a/src/eric7/Sessions/SessionFile.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Sessions/SessionFile.py Fri Feb 23 10:46:46 2024 +0100 @@ -16,6 +16,7 @@ from eric7.EricGui.EricOverrideCursor import EricOverridenCursor from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities class SessionFile(QObject): @@ -46,6 +47,10 @@ @rtype bool """ # get references to objects we need + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + project = ericApp().getObject("Project") projectBrowser = ericApp().getObject("ProjectBrowser") multiProject = ericApp().getObject("MultiProject") @@ -223,13 +228,18 @@ try: jsonString = json.dumps(sessionDict, indent=2) + "\n" - with open(filename, "w") as f: - f.write(jsonString) + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Save Remote Session") + fsInterface.writeFile(filename, jsonString.encode("utf-8")) + else: + title = self.tr("Save Session") + with open(filename, "w") as f: + f.write(jsonString) except (OSError, TypeError) as err: with EricOverridenCursor(): EricMessageBox.critical( None, - self.tr("Save Session"), + title, self.tr( "<p>The session file <b>{0}</b> could not be" " written.</p><p>Reason: {1}</p>" @@ -248,14 +258,23 @@ @return flag indicating a successful read @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + try: - with open(filename, "r") as f: - jsonString = f.read() + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Read Remote Session") + jsonString = fsInterface.readFile(filename).decode("utf-8") + else: + title = self.tr("Read Session") + with open(filename, "r") as f: + jsonString = f.read() sessionDict = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.critical( None, - self.tr("Read Session"), + title, self.tr( "<p>The session file <b>{0}</b> could not be read.</p>" "<p>Reason: {1}</p>"
--- a/src/eric7/SystemUtilities/FileSystemUtilities.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/SystemUtilities/FileSystemUtilities.py Fri Feb 23 10:46:46 2024 +0100 @@ -358,14 +358,18 @@ if f1 is None or f2 is None: return False - if followSymlinks: - if normcaseabspath(os.path.realpath(f1)) == normcaseabspath( - os.path.realpath(f2) - ): - return True + if isPlainFileName(f1) and isPlainFileName(f2): + if followSymlinks: + if normcaseabspath(os.path.realpath(f1)) == normcaseabspath( + os.path.realpath(f2) + ): + return True + else: + if normcaseabspath(f1) == normcaseabspath(f2): + return True + else: - if normcaseabspath(f1) == normcaseabspath(f2): - return True + return f1 == f2 return False @@ -388,14 +392,20 @@ if f1 is None or f2 is None: return False - if followSymlinks: - if normcaseabspath(os.path.dirname(os.path.realpath(f1))) == normcaseabspath( - os.path.dirname(os.path.realpath(f2)) - ): - return True + if isPlainFileName(f1) and isPlainFileName(f2): + if followSymlinks: + if normcaseabspath( + os.path.dirname(os.path.realpath(f1)) + ) == normcaseabspath(os.path.dirname(os.path.realpath(f2))): + return True + else: + if normcaseabspath(os.path.dirname(f1)) == normcaseabspath( + os.path.dirname(f2) + ): + return True + else: - if normcaseabspath(os.path.dirname(f1)) == normcaseabspath(os.path.dirname(f2)): - return True + return os.path.dirname(f1) == os.path.dirname(f2) return False @@ -491,30 +501,37 @@ followsymlinks=True, checkStop=None, ignore=None, + recursive=True, + dirsonly=False, ): """ Function returning a list of all files and directories. @param path root of the tree to check @type str - @param filesonly flag indicating that only files are wanted - @type bool + @param filesonly flag indicating that only files are wanted (defaults to False) + @type bool (optional) @param pattern a filename pattern or list of filename patterns to check - against - @type str or list of str + against (defaults to None) + @type str or list of str (optional) @param followsymlinks flag indicating whether symbolic links - should be followed + should be followed (defaults to True) + @type bool (optional) + @param checkStop function to be called to check for a stop (defaults to None) + @type function (optional) + @param ignore list of entries to be ignored (defaults to None) + @type list of str (optional) + @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) @type bool - @param checkStop function to be called to check for a stop - @type function - @param ignore list of entries to be ignored - @type list of str @return list of all files and directories in the tree rooted at path. The names are expanded to start with path. @rtype list of str """ patterns = pattern if isinstance(pattern, list) else [pattern] - files = [] if filesonly else [path] + files = [] if (filesonly and not dirsonly) else [path] ignoreList = [ ".svn", ".hg", @@ -546,18 +563,21 @@ continue if dirEntry.is_dir(): - if dirEntry.path in ignoreList: - continue - if dirEntry.is_symlink() and not followsymlinks: + if dirEntry.path in ignoreList or ( + dirEntry.is_symlink() and not followsymlinks + ): continue - files += direntries( - dirEntry.path, - filesonly=filesonly, - pattern=pattern, - followsymlinks=followsymlinks, - checkStop=checkStop, - ignore=ignore, - ) + if recursive: + files += direntries( + dirEntry.path, + filesonly=filesonly, + pattern=pattern, + followsymlinks=followsymlinks, + checkStop=checkStop, + ignore=ignore, + ) + elif dirsonly: + files.append(dirEntry.path) else: files.append(dirEntry.path) return files
--- a/src/eric7/Tasks/TasksFile.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Tasks/TasksFile.py Fri Feb 23 10:46:46 2024 +0100 @@ -16,6 +16,7 @@ from eric7.EricGui.EricOverrideCursor import EricOverridenCursor from eric7.EricWidgets import EricMessageBox from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities from .Task import TaskPriority, TaskType @@ -46,6 +47,10 @@ @return flag indicating a successful write @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + # prepare the tasks data dictionary # step 0: header tasksDict = {} @@ -85,13 +90,18 @@ try: jsonString = json.dumps(tasksDict, indent=2) + "\n" - with open(filename, "w") as f: - f.write(jsonString) + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Save Remote Tasks") + fsInterface.writeFile(filename, jsonString.encode("utf-8")) + else: + title = self.tr("Save Tasks") + with open(filename, "w") as f: + f.write(jsonString) except (OSError, TypeError) as err: with EricOverridenCursor(): EricMessageBox.critical( None, - self.tr("Save Tasks"), + title, self.tr( "<p>The tasks file <b>{0}</b> could not be" " written.</p><p>Reason: {1}</p>" @@ -110,14 +120,23 @@ @return flag indicating a successful read @rtype bool """ + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + try: - with open(filename, "r") as f: - jsonString = f.read() + if FileSystemUtilities.isRemoteFileName(filename): + title = self.tr("Read Remote Tasks") + jsonString = fsInterface.readFile(filename).decode("utf-8") + else: + title = self.tr("Read Tasks") + with open(filename, "r") as f: + jsonString = f.read() tasksDict = json.loads(jsonString) except (OSError, json.JSONDecodeError) as err: EricMessageBox.critical( None, - self.tr("Read Tasks"), + title, self.tr( "<p>The tasks file <b>{0}</b> could not be read.</p>" "<p>Reason: {1}</p>"
--- a/src/eric7/UI/Browser.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/UI/Browser.py Fri Feb 23 10:46:46 2024 +0100 @@ -35,6 +35,7 @@ from eric7.EricWidgets import EricFileDialog, EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from eric7.Project.ProjectBrowserModel import ProjectBrowserSimpleDirectoryItem +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.SystemUtilities import FileSystemUtilities from eric7.UI.DeleteFilesConfirmationDialog import DeleteFilesConfirmationDialog from eric7.Utilities import MimeTypes @@ -106,10 +107,12 @@ pdfFile = pyqtSignal(str) testFile = pyqtSignal(str) - def __init__(self, parent=None): + def __init__(self, serverInterface, parent=None): """ Constructor + @param serverInterface reference to the 'eric-ide' server interface object + @type EricServerInterface @param parent parent widget @type QWidget """ @@ -118,7 +121,10 @@ self.setWindowTitle(QCoreApplication.translate("Browser", "File-Browser")) self.setWindowIcon(EricPixmapCache.getIcon("eric")) - self.__model = BrowserModel() + self.__ericServerInterface = serverInterface + self.__remotefsInterface = serverInterface.getServiceInterface("FileSystem") + + self.__model = BrowserModel(fsInterface=self.__remotefsInterface) self.__sortModel = BrowserSortFilterProxyModel() self.__sortModel.setSourceModel(self.__model) self.setModel(self.__sortModel) @@ -338,15 +344,19 @@ # create the directory menu self.dirMenu = QMenu(self) self.dirMenu.addAction( - QCoreApplication.translate("Browser", "New toplevel directory..."), - self.__newToplevelDir, + QCoreApplication.translate("Browser", "New Top Level Directory..."), + self.__newTopLevelDir, + ) + self.__dmRemoteTopLevelAct = self.dirMenu.addAction( + QCoreApplication.translate("Browser", "New Remote Top Level Directory..."), + self.__newRemoteTopLevelDir, ) self.addAsTopLevelAct = self.dirMenu.addAction( - QCoreApplication.translate("Browser", "Add as toplevel directory"), + QCoreApplication.translate("Browser", "Add as top level directory"), self.__addAsToplevelDir, ) self.removeFromToplevelAct = self.dirMenu.addAction( - QCoreApplication.translate("Browser", "Remove from toplevel"), + QCoreApplication.translate("Browser", "Remove from top level"), self.__removeToplevel, ) self.dirMenu.addSeparator() @@ -387,8 +397,12 @@ self.attributeMenu = QMenu(self) self.attributeMenu.addAction( - QCoreApplication.translate("Browser", "New toplevel directory..."), - self.__newToplevelDir, + QCoreApplication.translate("Browser", "New Top Level Directory..."), + self.__newTopLevelDir, + ) + self.__amRemoteTopLevelAct = self.attributeMenu.addAction( + QCoreApplication.translate("Browser", "New Remote Top Level Directory..."), + self.__newRemoteTopLevelDir, ) self.attributeMenu.addSeparator() self.attributeMenu.addMenu(self.gotoMenu) @@ -396,8 +410,12 @@ # create the background menu self.backMenu = QMenu(self) self.backMenu.addAction( - QCoreApplication.translate("Browser", "New toplevel directory..."), - self.__newToplevelDir, + QCoreApplication.translate("Browser", "New Top Level Directory..."), + self.__newTopLevelDir, + ) + self.__bmRemoteTopLevelAct = self.backMenu.addAction( + QCoreApplication.translate("Browser", "New Remote Top Level Directory..."), + self.__newRemoteTopLevelDir, ) self.backMenu.addSeparator() self.backMenu.addAction(self.showHiddenFilesAct) @@ -431,7 +449,7 @@ def _contextMenuRequested(self, coord): """ - Protected slot to show the context menu of the listview. + Protected slot to show the context menu of the list view. @param coord the position of the mouse pointer @type QPoint @@ -475,6 +493,9 @@ self.openInPdfViewerAct.setVisible(False) self.menu.popup(coord) elif isinstance(itm, BrowserClassAttributeItem): + self.__amRemoteTopLevelAct.setEnabled( + self.__ericServerInterface.isServerConnected() + ) self.attributeMenu.popup(coord) elif isinstance(itm, BrowserDirectoryItem): if not index.parent().isValid(): @@ -483,10 +504,19 @@ else: self.removeFromToplevelAct.setEnabled(False) self.addAsTopLevelAct.setEnabled(True) + self.__dmRemoteTopLevelAct.setEnabled( + self.__ericServerInterface.isServerConnected() + ) self.dirMenu.popup(coord) else: + self.__bmRemoteTopLevelAct.setEnabled( + self.__ericServerInterface.isServerConnected() + ) self.backMenu.popup(coord) else: + self.__bmRemoteTopLevelAct.setEnabled( + self.__ericServerInterface.isServerConnected() + ) self.backMenu.popup(self.mapToGlobal(coord)) def _showGotoMenu(self): @@ -755,6 +785,7 @@ # remember the current state Preferences.setUI("BrowsersListHiddenFiles", checked) + @pyqtSlot() def handleTesting(self): """ Public slot to handle the testing popup menu entry. @@ -769,13 +800,14 @@ if pyfn is not None: self.testFile.emit(pyfn) - def __newToplevelDir(self): + @pyqtSlot() + def __newTopLevelDir(self): """ - Private slot to handle the New toplevel directory popup menu entry. + Private slot to handle the New Top Level Directory popup menu entry. """ dname = EricFileDialog.getExistingDirectory( None, - QCoreApplication.translate("Browser", "New toplevel directory"), + QCoreApplication.translate("Browser", "New Top Level Directory"), "", EricFileDialog.ShowDirsOnly, ) @@ -783,22 +815,39 @@ dname = os.path.abspath(FileSystemUtilities.toNativeSeparators(dname)) self.__model.addTopLevelDir(dname) + @pyqtSlot() + def __newRemoteTopLevelDir(self): + """ + Private slot to handle the New Remote Top Level Directory popup menu entry. + """ + dname = EricServerFileDialog.getExistingDirectory( + None, + QCoreApplication.translate("Browser", "New Remote Top Level Directory"), + "", + dirsOnly=True, + ) + if dname: + self.__model.addTopLevelDir(dname) + + @pyqtSlot() def __removeToplevel(self): """ - Private slot to handle the Remove from toplevel popup menu entry. + Private slot to handle the Remove from top level popup menu entry. """ index = self.currentIndex() sindex = self.model().mapToSource(index) self.__model.removeToplevelDir(sindex) + @pyqtSlot() def __addAsToplevelDir(self): """ - Private slot to handle the Add as toplevel directory popup menu entry. + Private slot to handle the Add as top level directory popup menu entry. """ index = self.currentIndex() dname = self.model().item(index).dirName() self.__model.addTopLevelDir(dname) + @pyqtSlot() def __refreshDirectory(self): """ Private slot to refresh a directory entry. @@ -807,6 +856,7 @@ refreshDir = self.model().item(index).dirName() self.__model.directoryChanged(refreshDir) + @pyqtSlot() def __findInDirectory(self): """ Private slot to handle the Find in directory popup menu entry. @@ -816,6 +866,7 @@ ericApp().getObject("UserInterface").showFindFilesWidget(searchDir=searchDir) + @pyqtSlot() def __replaceInDirectory(self): """ Private slot to handle the Find&Replace in directory popup menu entry. @@ -825,6 +876,7 @@ ericApp().getObject("UserInterface").showReplaceFilesWidget(searchDir=searchDir) + @pyqtSlot(str) def handleProgramChange(self, fn): """ Public slot to handle the programChange signal. @@ -834,6 +886,7 @@ """ self.__model.programChange(os.path.dirname(fn)) + @pyqtSlot(str) def handleInterpreterChanged(self, interpreter): """ Public slot to handle a change of the debug client's interpreter.
--- a/src/eric7/UI/BrowserModel.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/UI/BrowserModel.py Fri Feb 23 10:46:46 2024 +0100 @@ -62,14 +62,17 @@ Class implementing the browser model. """ - def __init__(self, parent=None, nopopulate=False): + def __init__(self, parent=None, nopopulate=False, fsInterface=None): """ Constructor - @param parent reference to parent object - @type QObject - @param nopopulate flag indicating to not populate the model - @type bool + @param parent reference to parent object (defaults to None) + @type QObject (optional) + @param nopopulate flag indicating to not populate the model (defaults to False) + @type bool (optional) + @param fsInterface reference to the 'eric-ide' server interface object + (defaults to None) + @type EricServerFileSystemInterface (optional) """ super().__init__(parent) @@ -78,6 +81,8 @@ self.__sysPathInterpreter = "" self.__sysPathItem = None + self.__remotefsInterface = fsInterface + if not nopopulate: self.watchedItems = {} self.watchedFileItems = {} @@ -326,8 +331,8 @@ dirName = itm.dirName() if ( dirName != "" - and not dirName.startswith("//") - and not dirName.startswith("\\\\") + and not FileSystemUtilities.isRemoteFileName(dirName) + and not dirName.startswith(("//", "\\\\")) ): if dirName not in self.watcher.directories(): self.watcher.addPath(dirName) @@ -442,7 +447,9 @@ ) for d in self.toplevelDirs: - itm = BrowserDirectoryItem(self.rootItem, d) + itm = BrowserDirectoryItem( + self.rootItem, d, fsInterface=self.__remotefsInterface + ) self._addItem(itm, self.rootItem) def interpreterChanged(self, interpreter): @@ -502,7 +509,9 @@ self.endRemoveRows() self.progDir = None - itm = BrowserDirectoryItem(self.rootItem, dirname) + itm = BrowserDirectoryItem( + self.rootItem, dirname, fsInterface=self.__remotefsInterface + ) self.addItem(itm) self.progDir = itm @@ -514,7 +523,9 @@ @type str """ if dirname not in self.toplevelDirs: - itm = BrowserDirectoryItem(self.rootItem, dirname) + itm = BrowserDirectoryItem( + self.rootItem, dirname, fsInterface=self.__remotefsInterface + ) self.addItem(itm) self.toplevelDirs.append(itm.dirName()) @@ -607,39 +618,85 @@ """ self._addWatchedItem(parentItem) - qdir = QDir(parentItem.dirName()) + dirName = parentItem.dirName() + if FileSystemUtilities.isPlainFileName(dirName): + qdir = QDir(dirName) - dirFilter = ( - QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot | QDir.Filter.Hidden - ) - entryInfoList = qdir.entryInfoList(dirFilter) - if len(entryInfoList) > 0: - if repopulate: - self.beginInsertRows( - self.createIndex(parentItem.row(), 0, parentItem), - 0, - len(entryInfoList) - 1, - ) - for f in entryInfoList: - if f.isDir(): - node = BrowserDirectoryItem( - parentItem, - FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()), - False, + dirFilter = ( + QDir.Filter.AllEntries | QDir.Filter.NoDotAndDotDot | QDir.Filter.Hidden + ) + entryInfoList = qdir.entryInfoList(dirFilter) + if len(entryInfoList) > 0: + if repopulate: + self.beginInsertRows( + self.createIndex(parentItem.row(), 0, parentItem), + 0, + len(entryInfoList) - 1, ) - else: - fileFilters = Preferences.getUI("BrowsersFileFilters").split(";") - if fileFilters: - fn = f.fileName() - if any(fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters): - continue - node = BrowserFileItem( - parentItem, - FileSystemUtilities.toNativeSeparators(f.absoluteFilePath()), + for f in entryInfoList: + if f.isDir(): + node = BrowserDirectoryItem( + parentItem, + FileSystemUtilities.toNativeSeparators( + f.absoluteFilePath() + ), + False, + ) + else: + fileFilters = Preferences.getUI("BrowsersFileFilters").split( + ";" + ) + if fileFilters: + fn = f.fileName() + if any( + fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters + ): + continue + node = BrowserFileItem( + parentItem, + FileSystemUtilities.toNativeSeparators( + f.absoluteFilePath() + ), + ) + 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, ) - self._addItem(node, parentItem) - if repopulate: - self.endInsertRows() + for entry in entriesList: + if entry["is_dir"]: + node = BrowserDirectoryItem( + parentItem, + FileSystemUtilities.remoteFileName(entry["path"]), + False, + fsInterface=self.__remotefsInterface, + ) + else: + fileFilters = Preferences.getUI("BrowsersFileFilters").split( + ";" + ) + if fileFilters: + fn = entry["name"] + if any( + fnmatch.fnmatch(fn, ff.strip()) for ff in fileFilters + ): + continue + node = BrowserFileItem( + parentItem, + FileSystemUtilities.remoteFileName(entry["path"]), + fsInterface=self.__remotefsInterface, + ) + self._addItem(node, parentItem) + if repopulate: + self.endInsertRows() def populateSysPathItem(self, parentItem, repopulate=False): """ @@ -692,6 +749,7 @@ """ from eric7.Utilities import ClassBrowsers + # TODO: add support for 'remote:' directories moduleName = parentItem.moduleName() fileName = parentItem.fileName() try: @@ -1152,6 +1210,7 @@ Class implementing the data structure for browser simple directory items. """ + # TODO: add support for 'remote:' directories def __init__(self, parent, text, path=""): """ Constructor @@ -1186,6 +1245,7 @@ @param full flag indicating full path name should be displayed @type bool """ + # TODO: add support for 'remote:' directories self._dirName = os.path.abspath(dinfo) self.itemData[0] = os.path.basename(self._dirName) @@ -1233,7 +1293,7 @@ Class implementing the data structure for browser directory items. """ - def __init__(self, parent, dinfo, full=True): + def __init__(self, parent, dinfo, full=True, fsInterface=None): """ Constructor @@ -1241,21 +1301,33 @@ @type BrowserItem @param dinfo dinfo is the string for the directory @type str - @param full flag indicating full pathname should be displayed - @type bool + @param full flag indicating full pathname should be displayed (defaults to True) + @type bool (optional) + @param fsInterface reference to the 'eric-ide' server file system interface + (defaults to None) + @type EricServerFileSystemInterface (optional) """ - self._dirName = os.path.abspath(dinfo) - dn = self._dirName if full else os.path.basename(self._dirName) + self.__fsInterface = fsInterface + + if FileSystemUtilities.isRemoteFileName(dinfo): + self._dirName = dinfo + dn = self._dirName if full else self.__fsInterface.basename(self._dirName) + else: + self._dirName = os.path.abspath(dinfo) + dn = self._dirName if full else os.path.basename(self._dirName) BrowserItem.__init__(self, parent, dn) self.type_ = BrowserItemType.Directory if ( - not FileSystemUtilities.isDrive(self._dirName) + FileSystemUtilities.isPlainFileName(self._dirName) + and not FileSystemUtilities.isDrive(self._dirName) and os.path.lexists(self._dirName) and os.path.islink(self._dirName) ): self.symlink = True self.icon = EricPixmapCache.getSymlinkIcon("dirClosed") + elif FileSystemUtilities.isRemoteFileName(self._dirName): + self.icon = EricPixmapCache.getIcon("open-remote") else: self.icon = EricPixmapCache.getIcon("dirClosed") self._populated = False @@ -1270,8 +1342,12 @@ @param full flag indicating full pathname should be displayed @type bool """ - self._dirName = os.path.abspath(dinfo) - dn = self._dirName if full else os.path.basename(self._dirName) + if FileSystemUtilities.isRemoteFileName(dinfo): + self._dirName = dinfo + dn = self._dirName if full else self.__fsInterface.basename(self._dirName) + else: + self._dirName = os.path.abspath(dinfo) + dn = self._dirName if full else os.path.basename(self._dirName) self.itemData[0] = dn def dirName(self): @@ -1342,12 +1418,13 @@ return "sys.path" +# TODO: add support for 'remote:' directories class BrowserFileItem(BrowserItem): """ Class implementing the data structure for browser file items. """ - def __init__(self, parent, finfo, full=True, sourceLanguage=""): + def __init__(self, parent, finfo, full=True, sourceLanguage="", fsInterface=None): """ Constructor @@ -1355,17 +1432,29 @@ @type BrowserItem @param finfo the string for the file @type str - @param full flag indicating full pathname should be displayed - @type bool - @param sourceLanguage source code language of the project - @type str + @param full flag indicating full pathname should be displayed (defaults to True) + @type bool (optional) + @param sourceLanguage source code language of the project (defaults to "") + @type str (optional) + @param fsInterface reference to the 'eric-ide' server file system interface + (defaults to None) + @type EricServerFileSystemInterface (optional) """ - BrowserItem.__init__(self, parent, os.path.basename(finfo)) + self.__fsInterface = fsInterface + if FileSystemUtilities.isRemoteFileName(finfo): + dirname, basename = self.__fsInterface.split(finfo) + self.fileext = self.__fsInterface.splitext(finfo)[1].lower() + self._filename = finfo + else: + dirname, basename = os.path.split(finfo) + self.fileext = os.path.splitext(finfo)[1].lower() + self._filename = os.path.abspath(finfo) + + BrowserItem.__init__(self, parent, basename) + + self._dirName = dirname self.type_ = BrowserItemType.File - self.fileext = os.path.splitext(finfo)[1].lower() - self._filename = os.path.abspath(finfo) - self._dirName = os.path.dirname(finfo) self.sourceLanguage = sourceLanguage self._moduleName = "" @@ -1375,17 +1464,17 @@ pixName = "filePython" self._populated = False self._lazyPopulation = True - self._moduleName = os.path.basename(finfo) + self._moduleName = basename elif self.isCythonFile(): pixName = "lexerCython" self._populated = False self._lazyPopulation = True - self._moduleName = os.path.basename(finfo) + self._moduleName = basename elif self.isRubyFile(): pixName = "fileRuby" self._populated = False self._lazyPopulation = True - self._moduleName = os.path.basename(finfo) + self._moduleName = basename elif self.isDesignerFile(): pixName = "fileDesigner" elif self.isLinguistFile(): @@ -1409,18 +1498,22 @@ pixName = "fileJavascript" self._populated = False self._lazyPopulation = True - self._moduleName = os.path.basename(finfo) + self._moduleName = basename elif self.isEricGraphicsFile(): pixName = "fileUML" elif self.isParsableFile(): pixName = ClassBrowsers.getIcon(self._filename) self._populated = False self._lazyPopulation = True - self._moduleName = os.path.basename(finfo) + self._moduleName = basename else: pixName = "fileMisc" - if os.path.lexists(self._filename) and os.path.islink(self._filename): + if ( + FileSystemUtilities.isPlainFileName(self._filename) + and os.path.lexists(self._filename) + and os.path.islink(self._filename) + ): self.symlink = True self.icon = EricPixmapCache.getSymlinkIcon(pixName) else: @@ -1435,12 +1528,25 @@ @param full flag indicating full pathname should be displayed @type bool """ - self._filename = os.path.abspath(finfo) - self.itemData[0] = os.path.basename(finfo) - self.fileext = os.path.splitext(finfo)[1].lower() - if self.isPython3File() or self.isRubyFile() or self.isParsableFile(): - self._dirName = os.path.dirname(finfo) - self._moduleName = os.path.basename(finfo) + if FileSystemUtilities.isRemoteFileName(finfo): + dirname, basename = self.__fsInterface.split(finfo) + self.fileext = self.__fsInterface.splitext(finfo)[1].lower() + self._filename = finfo + else: + dirname, basename = os.path.split(finfo) + self.fileext = os.path.splitext(finfo)[1].lower() + self._filename = os.path.abspath(finfo) + + self.itemData[0] = basename + if ( + self.isPython3File() + or self.isCythonFile() + or self.isRubyFile() + or self.isJavaScriptFile() + or self.isParsableFile() + ): + self._dirName = dirname + self._moduleName = basename def fileName(self): """ @@ -1654,8 +1760,18 @@ return order == Qt.SortOrder.DescendingOrder if issubclass(other.__class__, BrowserFileItem): - sinit = os.path.basename(self._filename).startswith("__init__.py") - oinit = os.path.basename(other.fileName()).startswith("__init__.py") + if FileSystemUtilities.isRemoteFileName(self._filename): + basename = self.__fsInterface.basename(self._filename) + else: + basename = os.path.basename(self._filename) + sinit = basename.startswith("__init__.py") + + if FileSystemUtilities.isRemoteFileName(other.fileName()): + basename = self.__fsInterface.basename(other.fileName()) + else: + basename = os.path.basename(other.fileName()) + oinit = basename.startswith("__init__.py") + if sinit and not oinit: return order == Qt.SortOrder.AscendingOrder if not sinit and oinit:
--- a/src/eric7/UI/UserInterface.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/UI/UserInterface.py Fri Feb 23 10:46:46 2024 +0100 @@ -79,6 +79,7 @@ from eric7.Preferences import Shortcuts from eric7.Project.Project import Project from eric7.QScintilla.SpellChecker import SpellChecker +from eric7.RemoteServerInterface.EricServerInterface import EricServerInterface from eric7.Sessions.SessionFile import SessionFile from eric7.SystemUtilities import ( DesktopUtilities, @@ -306,6 +307,10 @@ splash.showMessage(self.tr("Initializing Basic Services...")) + # create the remote server interface + logging.debug("Creating 'eric-ide' Server Interface...") + self.__ericServerInterface = EricServerInterface(self) + # Generate the conda interface logging.debug("Creating Conda Interface...") self.condaInterface = Conda(self) @@ -324,7 +329,7 @@ # Generate an empty project object and multi project object logging.debug("Creating Project Manager...") - self.project = Project(self) + self.project = Project(self, remoteServer=self.__ericServerInterface) ericApp().registerObject("Project", self.project) logging.debug("Creating Multi-Project Manager...") @@ -586,6 +591,19 @@ self.viewmanager.closeDeviceEditors ) + self.__ericServerInterface.connectionStateChanged.connect( + self.viewmanager.remoteConnectionChanged + ) + self.__ericServerInterface.connectionStateChanged.connect( + self.shell.remoteConnectionChanged + ) + self.__ericServerInterface.connectionStateChanged.connect( + self.project.remoteConnectionChanged + ) + self.__ericServerInterface.aboutToDisconnect.connect( + self.viewmanager.closeRemoteEditors + ) + # create the toolbar manager object self.toolbarManager = EricToolBarManager(self, self) self.toolbarManager.setMainWindow(self) @@ -631,6 +649,7 @@ ericApp().registerObject("MicroPython", self.microPythonWidget) ericApp().registerObject("JediAssistant", self.jediAssistant) ericApp().registerObject("PluginRepositoryViewer", self.pluginRepositoryViewer) + ericApp().registerObject("EricServer", self.__ericServerInterface) # create the various JSON file interfaces self.__sessionFile = SessionFile(True) @@ -791,7 +810,7 @@ # Create the view manager depending on the configuration setting logging.debug("Creating Viewmanager...") self.viewmanager = ViewManager.factory( - self, self.__debugServer, self.pluginManager + self, self.__debugServer, self.__ericServerInterface, self.pluginManager ) # Create previewer @@ -848,7 +867,7 @@ logging.debug("Creating File Browser...") from .Browser import Browser # noqa: I101 - self.browser = Browser() + self.browser = Browser(self.__ericServerInterface) else: logging.debug("File Browser disabled") self.browser = None @@ -3568,6 +3587,9 @@ # initialize multi project actions self.multiProject.initActions() + # initialize eric-ide server actions + self.__ericServerInterface.initActions() + def __initQtDocActions(self): """ Private slot to initialize the action to show the Qt documentation. @@ -3777,6 +3799,12 @@ mb.setNativeMenuBar(False) ############################################################## + ## Remote Server menu + ############################################################## + + self.__menus["server"] = self.__ericServerInterface.initMenu() + + ############################################################## ## File menu ############################################################## @@ -3791,6 +3819,9 @@ act = self.__menus["file"].actions()[0] sep = self.__menus["file"].insertSeparator(act) self.__menus["file"].insertAction(sep, self.newWindowAct) + self.__menus["file"].insertSeparator(sep) + self.__menus["file"].insertMenu(sep, self.__menus["server"]) + self.__menus["file"].insertSeparator(sep) self.__menus["file"].aboutToShow.connect(self.__showFileMenu) ############################################################## @@ -4088,6 +4119,7 @@ helptb = QToolBar(self.tr("Help"), self) profilestb = QToolBar(self.tr("Profiles"), self) pluginstb = QToolBar(self.tr("Plugins"), self) + servertb = self.__ericServerInterface.initToolbar(self.toolbarManager) toolstb.setObjectName("ToolsToolbar") testingtb.setObjectName("UnittestToolbar") @@ -4190,6 +4222,7 @@ # add the various toolbars self.addToolBar(filetb) + self.addToolBar(servertb) self.addToolBar(edittb) self.addToolBar(searchtb) self.addToolBar(viewtb) @@ -4240,6 +4273,7 @@ ] self.__toolbars["spelling"] = [spellingtb.windowTitle(), spellingtb, ""] self.__toolbars["vcs"] = [vcstb.windowTitle(), vcstb, "vcs"] + self.__toolbars["server"] = [servertb.windowTitle(), servertb, ""] def __initDebugToolbarsLayout(self): """ @@ -8587,3 +8621,18 @@ elif self.__layoutType == "Sidebars": self.__activateLeftRightSidebarWidget(self.__virtualenvManagerWidget) self.__virtualenvManagerWidget.setFocus(Qt.FocusReason.ActiveWindowFocusReason) + + ############################################################ + ## Interface to the eric-ide server interface + ############################################################ + + def isEricServerConnected(self): + """ + Public method to check, if a connection to an eric-ide server has been + established. + + @return flag indicating the interface connection state + @rtype bool + """ + # simply delegated to the eric-ide server interface object + return self.__ericServerInterface.isServerConnected()
--- a/src/eric7/Utilities/ClassBrowsers/__init__.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Utilities/ClassBrowsers/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -108,7 +108,7 @@ return None -def readmodule(module, path=None, isPyFile=False): +def readmodule(module, searchPath=None, isPyFile=False): """ Function to read a source file and return a dictionary of classes, functions, modules, etc. . @@ -118,7 +118,7 @@ @param module name of the source file @type str - @param path list of paths the file should be searched in + @param searchPath list of paths the file should be searched in @type list of str @param isPyFile flag indicating a Python file @type bool @@ -126,13 +126,13 @@ @rtype dict """ ext = os.path.splitext(module)[1].lower() - path = [] if path is None else path[:] + searchPath = [] if searchPath is None else searchPath[:] if not isPyFile: for classBrowserName in ClassBrowserRegistry: if ext in ClassBrowserRegistry[classBrowserName]["Extensions"]: return ClassBrowserRegistry[classBrowserName]["ReadModule"]( - module, path + module, searchPath ) if ext in __extensions["Ruby"]: @@ -145,7 +145,7 @@ classBrowserModule = getClassBrowserModule(moduleType) dictionary = ( - classBrowserModule.readmodule_ex(module, path, isTypeFile=isPyFile) + classBrowserModule.readmodule_ex(module, searchPath, isTypeFile=isPyFile) if classBrowserModule else {} ) @@ -244,6 +244,34 @@ raise ImportError +def determineSourceType(name, isPyFile=False): + """ + Function to determine the type of a source file given its name. + + @param name file name or module name + @type str + @param isPyFile flag indicating a Python file (defaults to False) + @type bool (optional) + @return source file type + @rtype int + """ + ext = os.path.splitext(name)[1].lower() + + if ext in __extensions["Ruby"]: + sourceType = RB_SOURCE + elif ext == ".ptl": + sourceType = PTL_SOURCE + elif ( + name.lower().endswith(tuple(Preferences.getPython("Python3Extensions"))) + or isPyFile + ): + sourceType = PY_SOURCE + else: + sourceType = UNKNOWN_SOURCE + + return sourceType + + def getIcon(filename): """ Function to get an icon name for the given file (only for class browsers provided
--- a/src/eric7/Utilities/ClassBrowsers/pyclbr.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Utilities/ClassBrowsers/pyclbr.py Fri Feb 23 10:46:46 2024 +0100 @@ -20,6 +20,8 @@ from PyQt6.QtCore import QRegularExpression from eric7 import Utilities +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities from eric7.Utilities import ClassBrowsers from . import ClbrBaseClasses @@ -395,7 +397,7 @@ self.importedNames[name].append(lineno) -def readmodule_ex(module, path=None, isTypeFile=False): +def readmodule_ex(module, searchPath=None, isTypeFile=False): """ Read a module file and return a dictionary of classes. @@ -405,29 +407,38 @@ @param module name of the module file @type str - @param path path the module should be searched in + @param searchPath path the module should be searched in @type list of str @param isTypeFile flag indicating a file of this type @type bool @return the resulting dictionary @rtype dict """ - # search the path for the module - path = [] if path is None else path[:] - f = None - if f is None: - fullpath = path[:] + sys.path[:] - f, file, (suff, mode, type) = ClassBrowsers.find_module( + fsInterface = ericApp().getObject("EricServer").getServiceInterface("FileSystem") + + if searchPath and FileSystemUtilities.isRemoteFileName(searchPath[0]): + sourceType = ClassBrowsers.determineSourceType(module, isTypeFile) + file = fsInterface.join(searchPath[0], module) + else: + # search the path for the module + searchPath = [] if searchPath is None else searchPath[:] + fullpath = searchPath[:] + sys.path[:] + f, file, (suff, mode, sourceType) = ClassBrowsers.find_module( module, fullpath, isTypeFile ) - if f: - f.close() - if type not in SUPPORTED_TYPES: + if f: + f.close() + + if sourceType not in SUPPORTED_TYPES: # not Python source, can't do anything with this module return {} try: - src = Utilities.readEncodedFile(file)[0] + src = ( + fsInterface.readEncodedFile(file)[0] + if FileSystemUtilities.isRemoteFileName(file) + else Utilities.readEncodedFile(file)[0] + ) except (OSError, UnicodeError): # can't do anything with this module return {}
--- a/src/eric7/Utilities/ClassBrowsers/rbclbr.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Utilities/ClassBrowsers/rbclbr.py Fri Feb 23 10:46:46 2024 +0100 @@ -17,6 +17,8 @@ from PyQt6.QtCore import QRegularExpression from eric7 import Utilities +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities from eric7.Utilities import ClassBrowsers from . import ClbrBaseClasses @@ -277,31 +279,41 @@ self.setPrivate() -def readmodule_ex(module, path=None, isTypeFile=False): # noqa: U100 +def readmodule_ex(module, searchPath=None, isTypeFile=False): # noqa: U100 """ Read a Ruby file and return a dictionary of classes, functions and modules. @param module name of the Ruby file @type str - @param path path the file should be searched in + @param searchPath path the file should be searched in @type list of str @param isTypeFile flag indicating a file of this type @type bool @return the resulting dictionary @rtype dict """ - # search the path for the file - f = None - fullpath = [] if path is None else path[:] - f, file, (suff, mode, type) = ClassBrowsers.find_module(module, fullpath) - if f: - f.close() - if type not in SUPPORTED_TYPES: + fsInterface = ericApp().getObject("EricServer").getServiceInterface("FileSystem") + + if searchPath and FileSystemUtilities.isRemoteFileName(searchPath[0]): + sourceType = ClassBrowsers.determineSourceType(module) + file = fsInterface.join(searchPath[0], module) + else: + # search the path for the module + fullpath = [] if searchPath is None else searchPath[:] + f, file, (suff, mode, sourceType) = ClassBrowsers.find_module(module, fullpath) + if f: + f.close() + + if sourceType not in SUPPORTED_TYPES: # not Ruby source, can't do anything with this module return {} try: - src = Utilities.readEncodedFile(file)[0] + src = ( + fsInterface.readEncodedFile(file)[0] + if FileSystemUtilities.isRemoteFileName(file) + else Utilities.readEncodedFile(file)[0] + ) except (OSError, UnicodeError): # can't do anything with this module return {}
--- a/src/eric7/Utilities/ModuleParser.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Utilities/ModuleParser.py Fri Feb 23 10:46:46 2024 +0100 @@ -26,6 +26,8 @@ from PyQt6.QtCore import QRegularExpression from eric7 import Utilities +from eric7.EricWidgets.EricApplication import ericApp +from eric7.SystemUtilities import FileSystemUtilities __all__ = [ "Attribute", @@ -1677,42 +1679,50 @@ @return reference to a Module object containing the parsed module information @rtype Module + @exception ImportError raised to indicate an unsupported source code type """ global _modules _extensions = ( [".py", ".pyw", ".pyi", ".ptl", ".rb"] if extensions is None else extensions[:] ) - with contextlib.suppress(ValueError): - _extensions.remove(".py") + modname = module + isRemoteFileName = FileSystemUtilities.isRemoteFileName(module) - modname = module + if isRemoteFileName: + fsInterface = ( + ericApp().getObject("EricServer").getServiceInterface("FileSystem") + ) + module, extension = fsInterface.splitext(fsInterface.basename(module)) + else: + with contextlib.suppress(ValueError): + _extensions.remove(".py") - if os.path.exists(module): - path = [os.path.dirname(module)] - if module.lower().endswith(".py"): - module = module[:-3] - if ( - os.path.exists(os.path.join(path[0], "__init__.py")) - or os.path.exists(os.path.join(path[0], "__init__.pyi")) - or os.path.exists(os.path.join(path[0], "__init__.rb")) - or inpackage - ): - if basename: - module = module.replace(basename, "") - if os.path.isabs(module): - modname = os.path.splitdrive(module)[1][len(os.sep) :] + if os.path.exists(module): + path = [os.path.dirname(module)] + if module.lower().endswith(".py"): + module = module[:-3] + if ( + os.path.exists(os.path.join(path[0], "__init__.py")) + or os.path.exists(os.path.join(path[0], "__init__.pyi")) + or os.path.exists(os.path.join(path[0], "__init__.rb")) + or inpackage + ): + if basename: + module = module.replace(basename, "") + if os.path.isabs(module): + modname = os.path.splitdrive(module)[1][len(os.sep) :] + else: + modname = module + modname = modname.replace(os.sep, ".") + inpackage = True else: - modname = module - modname = modname.replace(os.sep, ".") - inpackage = True - else: - modname = os.path.basename(module) - for ext in _extensions: - if modname.lower().endswith(ext): - modname = modname[: -len(ext)] - break - module = os.path.basename(module) + modname = os.path.basename(module) + for ext in _extensions: + if modname.lower().endswith(ext): + modname = modname[: -len(ext)] + break + module = os.path.basename(module) if caching and modname in _modules: # we've seen this module before... @@ -1725,19 +1735,44 @@ _modules[modname] = mod return mod - # search the path for the module - path = [] if path is None else path[:] - f = None - if inpackage: - try: - f, file, (suff, mode, moduleType) = find_module(module, path, _extensions) - except ImportError: - f = None - if f is None: - fullpath = path[:] + sys.path[:] - f, file, (suff, mode, moduleType) = find_module(module, fullpath, _extensions) - if f: - f.close() + if isRemoteFileName: + if not fsInterface.exists(modname): + raise ImportError + + if extension == ".ptl": + moduleType = PTL_SOURCE + elif extension == ".rb": + moduleType = RB_SOURCE + elif extension in _extensions: # noqa: Y106 + moduleType = PY_SOURCE + else: + raise ImportError + + file = modname + + modname = FileSystemUtilities.plainFileName(modname) + if modname.startswith(("/", "\\")): + modname = modname[1:] + modname = os.path.splitext(modname)[0].replace("/", ".").replace("\\", ".") + else: + # search the path for the module + path = [] if path is None else path[:] + f = None + if inpackage: + try: + f, file, (suff, mode, moduleType) = find_module( + module, path, _extensions + ) + except ImportError: + f = None + if f is None: + fullpath = path[:] + sys.path[:] + f, file, (suff, mode, moduleType) = find_module( + module, fullpath, _extensions + ) + if f: + f.close() + if moduleType not in SUPPORTED_TYPES: # not supported source, can't do anything with this module _modules[modname] = Module(modname, None, None) @@ -1745,7 +1780,14 @@ mod = Module(modname, file, moduleType) with contextlib.suppress(UnicodeError, OSError): - src = Utilities.readEncodedFile(file)[0] + src = ( + ericApp() + .getObject("EricServer") + .getServiceInterface("FileSystem") + .readEncodedFile(file)[0] + if isRemoteFileName + else Utilities.readEncodedFile(file)[0] + ) mod.scan(src) if caching: _modules[modname] = mod
--- a/src/eric7/Utilities/__init__.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/Utilities/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -35,7 +35,7 @@ from eric7 import Preferences from eric7.EricWidgets.EricApplication import ericApp -from eric7.SystemUtilities import DesktopUtilities, OSUtilities +from eric7.SystemUtilities import DesktopUtilities, FileSystemUtilities, OSUtilities from eric7.UI.Info import Program, Version @@ -338,6 +338,32 @@ return str(text, "utf-8", "ignore"), "utf-8-ignore" +def decodeWithEncoding(text, encoding): + """ + Function to decode some byte text into a string. + + @param text byte text to decode + @type bytes + @param encoding encoding to be used to read the file + @type str + @return tuple of decoded text and encoding + @rtype tuple of (str, str) + """ + if encoding: + with contextlib.suppress(UnicodeError, LookupError): + return str(text, encoding), "{0}-selected".format(encoding) + + # Try default encoding + with contextlib.suppress(UnicodeError, LookupError): + codec = Preferences.getEditor("DefaultEncoding") + return str(text, codec), "{0}-default".format(codec) + + # Assume UTF-8 loosing information + return str(text, "utf-8", "ignore"), "utf-8-ignore" + else: + return decode(text) + + def readEncodedFileWithEncoding(filename, encoding): """ Function to read a file and decode its contents into proper text. @@ -351,19 +377,7 @@ """ with open(filename, "rb") as f: text = f.read() - if encoding: - with contextlib.suppress(UnicodeError, LookupError): - return str(text, encoding), "{0}-selected".format(encoding) - - # Try default encoding - with contextlib.suppress(UnicodeError, LookupError): - codec = Preferences.getEditor("DefaultEncoding") - return str(text, codec), "{0}-default".format(codec) - - # Assume UTF-8 loosing information - return str(text, "utf-8", "ignore"), "utf-8-ignore" - else: - return decode(text) + return decodeWithEncoding(text, encoding) def writeEncodedFile(filename, text, origEncoding, forcedEncoding=""): @@ -889,6 +903,16 @@ basename = os.path.splitext(fn)[0] filename = "{0}.coverage".format(basename) if mustExist: + if FileSystemUtilities.isRemoteFileName(fn): + ericServer = ericApp().getObject("EricServer") + if ericServer.isServerConnected() and ericServer.getServiceInterface( + "FileSystem" + ).exists(filename): + return filename + else: + return "" + + # It is a local file. if os.path.isfile(filename): return filename else: @@ -929,12 +953,22 @@ basename = os.path.splitext(fn)[0] filename = "{0}.profile".format(basename) if mustExist: + if FileSystemUtilities.isRemoteFileName(fn): + ericServer = ericApp().getObject("EricServer") + if ericServer.isServerConnected() and ericServer.getServiceInterface( + "FileSystem" + ).exists(filename): + return filename + else: + return "" + + # It is a local file. if os.path.isfile(filename): return filename else: return "" - else: - return filename + + return filename def parseOptionString(s):
--- a/src/eric7/ViewManager/ViewManager.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/ViewManager/ViewManager.py Fri Feb 23 10:46:46 2024 +0100 @@ -39,6 +39,7 @@ from eric7.QScintilla.SpellChecker import SpellChecker from eric7.QScintilla.SpellingDictionaryEditDialog import SpellingDictionaryEditDialog from eric7.QScintilla.ZoomDialog import ZoomDialog +from eric7.RemoteServerInterface import EricServerFileDialog from eric7.SystemUtilities import FileSystemUtilities, OSUtilities @@ -148,7 +149,7 @@ self.__watcher = QFileSystemWatcher(self) self.__watcher.fileChanged.connect(self.__watchedFileChanged) - def setReferences(self, ui, dbs): + def setReferences(self, ui, dbs, remoteServerInterface): """ Public method to set some references needed later on. @@ -156,11 +157,16 @@ @type UserInterface @param dbs reference to the debug server object @type DebugServer + @param remoteServerInterface reference to the 'eric-ide' server interface + @type EricServerInterface """ from eric7.QScintilla.SearchReplaceWidget import SearchReplaceSlidingWidget self.ui = ui self.dbs = dbs + self.__remotefsInterface = remoteServerInterface.getServiceInterface( + "FileSystem" + ) self.__searchReplaceWidget = SearchReplaceSlidingWidget(self, ui) @@ -650,6 +656,30 @@ self.openAct.triggered.connect(self.__openFiles) self.fileActions.append(self.openAct) + self.openRemoteAct = EricAction( + QCoreApplication.translate("ViewManager", "Open (Remote)"), + EricPixmapCache.getIcon("open-remote"), + QCoreApplication.translate("ViewManager", "Open (Remote)..."), + 0, + 0, + self, + "vm_file_open_remote", + ) + self.openRemoteAct.setStatusTip( + QCoreApplication.translate("ViewManager", "Open a remote file") + ) + self.openRemoteAct.setWhatsThis( + QCoreApplication.translate( + "ViewManager", + """<b>Open a remote file</b>""" + """<p>You will be asked for the name of a remote file to be opened""" + """ in an editor window.</p>""", + ) + ) + self.openRemoteAct.triggered.connect(self.__openRemoteFiles) + self.openRemoteAct.setEnabled(False) + self.fileActions.append(self.openRemoteAct) + self.closeActGrp = createActionGroup(self) self.closeAct = EricAction( @@ -745,13 +775,40 @@ QCoreApplication.translate( "ViewManager", """<b>Save File as</b>""" - """<p>Save the contents of current editor window to a new file.""" + """<p>Save the contents of the current editor window to a new file.""" """ The file can be entered in a file selection dialog.</p>""", ) ) self.saveAsAct.triggered.connect(self.saveAsCurrentEditor) self.fileActions.append(self.saveAsAct) + self.saveAsRemoteAct = EricAction( + QCoreApplication.translate("ViewManager", "Save as (Remote)"), + EricPixmapCache.getIcon("fileSaveAsRemote"), + QCoreApplication.translate("ViewManager", "Save as (Remote)..."), + 0, + 0, + self.saveActGrp, + "vm_file_save_as_remote", + ) + self.saveAsRemoteAct.setStatusTip( + QCoreApplication.translate( + "ViewManager", + "Save the current file to a new one on an eric-ide server", + ) + ) + self.saveAsRemoteAct.setWhatsThis( + QCoreApplication.translate( + "ViewManager", + """<b>Save File as (Remote)</b>""" + """<p>Save the contents of the current editor window to a new file""" + """ on the connected eric-ide server. The file can be entered in a""" + """ file selection dialog.</p>""", + ) + ) + self.saveAsRemoteAct.triggered.connect(self.saveAsRemoteCurrentEditor) + self.fileActions.append(self.saveAsRemoteAct) + self.saveCopyAct = EricAction( QCoreApplication.translate("ViewManager", "Save Copy"), EricPixmapCache.getIcon("fileSaveCopy"), @@ -896,6 +953,7 @@ menu.addAction(self.newAct) menu.addAction(self.openAct) + menu.addAction(self.openRemoteAct) self.menuRecentAct = menu.addMenu(self.recentMenu) menu.addMenu(self.bookmarkedMenu) menu.addSeparator() @@ -906,6 +964,7 @@ menu.addSeparator() menu.addAction(self.saveAct) menu.addAction(self.saveAsAct) + menu.addAction(self.saveAsRemoteAct) menu.addAction(self.saveCopyAct) menu.addAction(self.saveAllAct) self.exportersMenuAct = menu.addMenu(self.exportersMenu) @@ -938,10 +997,12 @@ tb.addAction(self.newAct) tb.addAction(self.openAct) + tb.addAction(self.openRemoteAct) tb.addAction(self.closeAct) tb.addSeparator() tb.addAction(self.saveAct) tb.addAction(self.saveAsAct) + tb.addAction(self.saveAsRemoteAct) tb.addAction(self.saveCopyAct) tb.addAction(self.saveAllAct) @@ -5389,20 +5450,17 @@ ## Methods and slots that deal with file and window handling ################################################################## + @pyqtSlot() def __openFiles(self): """ Private slot to open some files. """ - # set the cwd of the dialog based on the following search criteria: - # 1: Directory of currently active editor - # 2: Directory of currently active project - # 3: CWD from eric7.QScintilla import Lexers fileFilter = self._getOpenFileFilter() progs = EricFileDialog.getOpenFileNamesAndFilter( self.ui, - QCoreApplication.translate("ViewManager", "Open files"), + QCoreApplication.translate("ViewManager", "Open Files"), self._getOpenStartDir(), Lexers.getOpenFileFiltersList(True, True), fileFilter, @@ -5410,6 +5468,33 @@ for prog in progs: self.openFiles(prog) + @pyqtSlot() + def __openRemoteFiles(self): + """ + Private slot to open some files. + """ + from eric7.QScintilla import Lexers + + if self.ui.isEricServerConnected(): + fileFilter = self._getOpenFileFilter() + progs = EricServerFileDialog.getOpenFileNames( + self.ui, + QCoreApplication.translate("ViewManager", "Open Remote Files"), + self._getOpenStartDir(forRemote=True), + Lexers.getOpenFileFiltersList(True, True), + fileFilter, + ) + for prog in progs: + self.openFiles(prog) + else: + EricMessageBox.critical( + self.ui, + self.tr("Open Remote Files"), + self.tr( + "You must be connected to a remote eric-ide server. Aborting..." + ), + ) + def openFiles(self, prog): """ Public slot to open some files. @@ -5417,7 +5502,8 @@ @param prog name of file to be opened @type str """ - prog = os.path.abspath(prog) + if FileSystemUtilities.isPlainFileName(prog): + prog = os.path.abspath(prog) # Open up the new files. self.openSourceFile(prog) @@ -5618,6 +5704,26 @@ if FileSystemUtilities.isDeviceFileName(editor.getFileName()): self.closeEditor(editor, ignoreDirty=True) + @pyqtSlot() + def closeRemoteEditors(self): + """ + Public slot to close all editors related to a connected eric-ide server. + """ + for editor in self.editors[:]: + if FileSystemUtilities.isRemoteFileName(editor.getFileName()): + self.closeEditor(editor, ignoreDirty=True) + + @pyqtSlot(bool) + def remoteConnectionChanged(self, connected): + """ + Public slot handling a change of the connection state to an eric-ide server. + + @param connected flag indicating the connection state + @type bool + """ + self.openRemoteAct.setEnabled(connected) + self.saveAsRemoteAct.setEnabled(self.saveActGrp.isEnabled() and connected) + def exit(self): """ Public method to handle the debugged program terminating. @@ -6013,9 +6119,15 @@ filenames = [] for editor in self.editors: fn = editor.getFileName() - if fn is not None and fn not in filenames and os.path.exists(fn): + if fn is not None and fn not in filenames: # only return names of existing files - filenames.append(fn) + exists = ( + self.__remotefsInterface.exists(fn) + if FileSystemUtilities.isRemoteFileName(fn) + else os.path.exists(fn) + ) + if exists: + filenames.append(fn) return filenames @@ -6227,6 +6339,29 @@ aw = self.activeWindow() self.saveAsEditorEd(aw) + @pyqtSlot(Editor) + def saveAsRemoteEditorEd(self, ed): + """ + Public slot to save the contents of an editor to a new file on a + connected eric-ide server. + + @param ed editor to be saved + @type Editor + """ + if ed: + ok = ed.saveFileAs(remote=True) + if ok: + self.setEditorName(ed, ed.getFileName()) + + @pyqtSlot() + def saveAsRemoteCurrentEditor(self): + """ + Public slot to save the contents of the current editor to a new file on a + connected eric-ide server. + """ + aw = self.activeWindow() + self.saveAsRemoteEditorEd(aw) + def saveCopyEditorEd(self, ed): """ Public slot to save the contents of an editor to a new copy of @@ -7610,6 +7745,7 @@ """ self.closeActGrp.setEnabled(True) self.saveActGrp.setEnabled(True) + self.saveAsRemoteAct.setEnabled(self.ui.isEricServerConnected()) self.exportersMenuAct.setEnabled(True) self.printAct.setEnabled(True) if self.printPreviewAct: @@ -7642,10 +7778,7 @@ @type bool """ if editor is not None: - self.saveAct.setEnabled( - editor.isModified() - and not FileSystemUtilities.isRemoteFileName(editor.getFileName()) - ) + self.saveAct.setEnabled(editor.isModified()) self.revertAct.setEnabled(editor.isModified()) self.undoAct.setEnabled(editor.isUndoAvailable()) @@ -8034,7 +8167,7 @@ ## Below are protected utility methods ################################################################## - def _getOpenStartDir(self): + def _getOpenStartDir(self, forRemote=False): """ Protected method to return the starting directory for a file open dialog. @@ -8043,22 +8176,42 @@ using the following search order, until a match is found:<br /> 1: Directory of currently active editor<br /> 2: Directory of currently active Project<br /> - 3: CWD - + 3: Directory defined as the workspace (only for local access)<br /> + 4: CWD + + @param forRemote flag indicating to get the start directory for a remote + operation (defaults to False) + @type bool (optional) @return name of directory to start @rtype str """ # if we have an active source, return its path - if self.activeWindow() is not None and self.activeWindow().getFileName(): - return os.path.dirname(self.activeWindow().getFileName()) + if self.activeWindow() is not None: + fn = self.activeWindow().getFileName() + if forRemote and FileSystemUtilities.isRemoteFileName(fn): + return ( + ericApp() + .getObject("EricServer") + .getServiceInterface("FileSystem") + .dirname(fn) + ) + if not forRemote and FileSystemUtilities.isPlainFileName(fn): + return os.path.dirname(fn) # check, if there is an active project and return its path - elif ericApp().getObject("Project").isOpen(): - return ericApp().getObject("Project").ppath - - else: + if ericApp().getObject("Project").isOpen(): + ppath = ericApp().getObject("Project").ppath + if (forRemote and FileSystemUtilities.isRemoteFileName(ppath)) or ( + not forRemote and FileSystemUtilities.isPlainFileName(ppath) + ): + return ppath + + if not forRemote: return Preferences.getMultiProject("Workspace") or OSUtilities.getHomeDir() + # return empty string + return "" + def _getOpenFileFilter(self): """ Protected method to return the active filename filter for a file open
--- a/src/eric7/ViewManager/__init__.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/ViewManager/__init__.py Fri Feb 23 10:46:46 2024 +0100 @@ -23,7 +23,7 @@ ###################################################################### -def factory(ui, dbs, pluginManager): +def factory(ui, dbs, remoteServerInterface, pluginManager): """ Modul factory function to generate the right viewmanager type. @@ -34,6 +34,8 @@ @type UserInterface @param dbs reference to the debug server object @type DebugServer + @param remoteServerInterface reference to the 'eric-ide' server interface + @type EricServerInterface @param pluginManager reference to the plugin manager object @type PluginManager @return the instantiated viewmanager @@ -48,5 +50,5 @@ if vm is None: raise RuntimeError(f"Could not create a viemanager object.\nError: {err}") Preferences.setViewManager("tabview") - vm.setReferences(ui, dbs) + vm.setReferences(ui, dbs, remoteServerInterface) return vm
--- a/src/eric7/VirtualEnv/VirtualenvManager.py Fri Feb 23 10:43:39 2024 +0100 +++ b/src/eric7/VirtualEnv/VirtualenvManager.py Fri Feb 23 10:46:46 2024 +0100 @@ -26,6 +26,7 @@ from .VirtualenvMeta import VirtualenvMetaData +# TODO: introduce 'eric-ide Server' environment definitions class VirtualenvManager(QObject): """ Class implementing an object to manage Python virtual environments.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/eric7_server.py Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +eric-ide Server. + +This is the main Python script of the eric-ide server. This is a server to perform +remote development (e.g. code hosted on another computer or through a docker +container). +""" + +import argparse +import socket +import sys + +from eric7.RemoteServer.EricServer import EricServer +from eric7.UI.Info import Version + + +def createArgumentParser(): + """ + Function to create an argument parser. + + @return created argument parser object + @rtype argparse.ArgumentParser + """ + parser = argparse.ArgumentParser( + description=( + "Start the eric-ide server component. This will listen for connections" + " from the eric IDE in order to perform remote development." + ), + epilog="Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>.", + ) + + parser.add_argument( + "-p", + "--port", + type=int, + default=42024, + help="Listen on the given port for connections from an eric IDE.", + ) + parser.add_argument( + "-6", + "--with-ipv6", + action="store_true", + help="Listen on IPv6 interfaces as well if the system supports the creation" + "of TCP sockets which can handle both IPv4 and IPv6. {0}".format( + "This system supports this feature." + if socket.has_dualstack_ipv6() + else "This system does not support this feature. Option will be ignored." + ), + ) + parser.add_argument( + "-V", + "--version", + action="version", + version="%(prog)s {0}".format(Version), + help="Show version information and exit.", + ) + + return parser + + +def main(): + """ + Main entry point into the application. + """ + global supportedExtensions + + parser = createArgumentParser() + args = parser.parse_args() + + server = EricServer(port=args.port, useIPv6=args.with_ipv6) + ok = server.run() + + sys.exit(0 if ok else 1) + + +if __name__ == "__main__": + main() + +# +# eflag: noqa = M801
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/dialog-cancel.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dialog-cancel.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.471276" + inkscape:cy="11.160662" + inkscape:window-width="2580" + inkscape:window-height="1255" + inkscape:window-x="692" + inkscape:window-y="13" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 11,1 C 8.4655025,1 6.16085,1.9458675 4.3984375,3.5 4.2406387,3.63915 4.0784,3.780975 3.9296875,3.9296875 L 3.5,4.3984375 C 1.9458675,6.16085 1,8.4655025 1,11 c 0,5.52285 4.47715,10 10,10 2.534497,0 4.83915,-0.945867 6.601563,-2.5 l 0.46875,-0.429687 C 18.219025,17.9216 18.36085,17.759361 18.5,17.601563 20.054133,15.83915 21,13.534497 21,11 21,5.47715 16.52285,1 11,1 m 0,1.25 c 4.832487,0 8.75,3.9175125 8.75,8.75 0,2.192075 -0.816519,4.167835 -2.148437,5.703125 L 5.296875,4.3984375 C 6.8321637,3.0665187 8.807925,2.25 11,2.25 M 4.3984375,5.296875 16.703125,17.601563 C 15.167835,18.933481 13.192075,19.75 11,19.75 6.1675125,19.75 2.25,15.832487 2.25,11 2.25,8.807925 3.0665187,6.8321637 4.3984375,5.296875" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/dialog-ok.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dialog-ok.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.449854" + inkscape:cy="11.26777" + inkscape:window-width="2580" + inkscape:window-height="1271" + inkscape:window-x="861" + inkscape:window-y="110" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.291" + d="M 20.116087,3.0000134 7.0676375,17.088646 1.8839,11.491668 1,12.446045 l 5.1837375,5.596951 -0.0025,0.0026 0.8839125,0.954364 0.0025,-0.0026 0.0025,0.0026 0.8838875,-0.954364 -0.0025,-0.0026 L 21,3.9543498 20.1161,3 Z" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/dirNew.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dirNew.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.492697" + inkscape:cy="11.278481" + inkscape:window-width="2580" + inkscape:window-height="1326" + inkscape:window-x="0" + inkscape:window-y="55" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 1,1 V 2.2499998 20.999996 H 2.2499998 13.499998 v -1.25 H 2.2499998 V 9.7499984 h 3.7499993 v -0.012208 l 0.00977,0.012208 2.4999995,-2.4999996 H 19.749998 v 6.2499992 h 1.25 V 3.4999995 H 12.259764 L 9.7597646,1 9.7500021,1.0122125 V 1 H 2.2500035 1.0000037 M 17.250001,14.749997 v 2.5 h -2.5 v 1.25 h 2.5 v 2.499999 H 18.5 V 18.499997 H 21 v -1.25 h -2.5 v -2.5 h -1.249999" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/fileSaveAsRemote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg8" + sodipodi:docname="fileSaveAsRemote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview10" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="12.028238" + inkscape:cy="11.546251" + inkscape:window-width="2580" + inkscape:window-height="1321" + inkscape:window-x="860" + inkscape:window-y="59" + inkscape:window-maximized="0" + inkscape:current-layer="svg8" /> + <defs + id="defs4"> + <style + type="text/css" + id="style2">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <path + class="ColorScheme-Text" + d="M 2,1.0001509 V 15.994085 h 9.000109 v -0.93707 H 6.0000082 V 9.4348532 H 11.000109 V 8.4977845 H 5.0000061 V 15.057015 H 3.000002 V 1.9388863 H 6.0000082 V 6.6240635 H 14.000114 V 1.9388863 h 0.292971 l 2.707054,2.5365439 V 7.561049 h 1.000002 V 4.0947694 h -0.0077 l 0.0077,-0.00915 L 14.707092,1 14.699256,1.00915 V 1 H 14.000034 Z M 7.0001004,1.9389695 H 10.900567 V 5.6870781 H 7.0001004 Z M 18.000033,8.4982011 17.003901,9.4297698 h -0.0038 L 12,14.113115 l 0.0077,0.0092 -0.0039,0.940653 H 11.9999 V 16 h 2.000071 l 0.0059,-0.0092 h 0.01364 l -0.0059,-0.0092 0.98632,-0.924234 4.00001,-3.744443 -0.705086,-0.664339 -4.990291,4.672345 -0.593755,-0.554534 4.99227,-4.674178 0.591802,0.556359 0.705087,0.658847 L 20,10.37034 Z" + color="#eff0f1" + fill="currentColor" + id="path6" + style="stroke-width:0.866025" /> + <path + style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2499999,-0.594666 -1.2499999,-1.333333 0,-0.738667 0.5574999,-1.333333 1.2499999,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/open-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="open-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.621227" + inkscape:cy="11.332035" + inkscape:window-width="2580" + inkscape:window-height="1337" + inkscape:window-x="861" + inkscape:window-y="44" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#eff0f1; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 1,1 V 2.25 16 H 2.2499998 V 9.75 H 5.9999991 V 9.74023 L 6.0097653,9.75 8.5097648,7.25 H 19.749998 V 16 h 1.25 V 3.5 H 12.259764 L 9.7597646,1 9.7500046,1.00976 V 1 H 2.2500035 1.0000037 M 8.5000023,17.25 V 18.5 H 1.0000037 v 1.25 H 8.5000023 V 21 H 13.500001 V 19.75 H 21 V 18.5 H 13.500001 V 17.25 H 8.5000023 m 2.4999997,0.625 c 0.6925,0 1.25,0.5575 1.25,1.25 0,0.6925 -0.5575,1.25 -1.25,1.25 -0.6925,0 -1.2499999,-0.5575 -1.2499999,-1.25 0,-0.6925 0.5574999,-1.25 1.2499999,-1.25" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/preferences-eric-server.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + height="22" + width="22" + version="1.1" + id="svg1718" + sodipodi:docname="preferences-eric-server.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1722" /> + <sodipodi:namedview + id="namedview1720" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="32.09375" + inkscape:cx="16.623174" + inkscape:cy="16.327167" + inkscape:window-width="2580" + inkscape:window-height="1381" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg1718" /> + <linearGradient + id="b" + gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)" + gradientUnits="userSpaceOnUse" + x2="0" + y1="44" + y2="4"> + <stop + offset="0" + stop-color="#1d1e1e" + id="stop1680" /> + <stop + offset="1" + stop-color="#44484c" + id="stop1682" /> + </linearGradient> + <linearGradient + id="c" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="a" + gradientUnits="userSpaceOnUse" + x2="0" + y1="507.79999" + y2="506.79999"> + <stop + offset="0" + stop-color="#3da103" + id="stop1686" /> + <stop + offset="1" + stop-color="#7ddf07" + id="stop1688" /> + </linearGradient> + <linearGradient + id="d" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="e" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="f" + gradientUnits="userSpaceOnUse" + x1="5" + x2="18" + y1="12" + y2="25"> + <stop + offset="0" + stop-color="#292c2f" + id="stop1693" + style="stop-color:#c0c0c0;stop-opacity:1;" /> + <stop + offset="1" + stop-opacity="0" + id="stop1695" /> + </linearGradient> + <g + id="g2294" + transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)"> + <path + d="M 3,7 H 29 V 24 H 3 Z" + fill="#111213" + id="path1698" /> + <path + d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z" + fill="url(#b)" + id="path1700" + style="fill:#ececec;stroke-width:0.7" /> + <path + d="M 5,4 H 7 V 5 H 5 Z" + fill="url(#c)" + id="path1702" + style="fill:url(#c);stroke-width:0.7" /> + <path + d="m 5,15 h 2 v 1 H 5 Z" + fill="url(#d)" + id="path1704" + style="fill:url(#d);stroke-width:0.7" /> + <path + d="m 5,26 h 2 v 1 H 5 Z" + fill="url(#e)" + id="path1706" + style="fill:url(#e);stroke-width:0.7" /> + <path + d="m 3,29 h 26 v 1 H 3 Z" + opacity="0.2" + id="path1710" /> + <path + d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z" + fill="url(#f)" + fill-rule="evenodd" + opacity="0.4" + id="path1712" + style="fill:url(#f)" /> + <rect + fill="#eff0f1" + height="16" + rx="2" + width="16" + x="16" + y="14" + id="rect1714" /> + <path + d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z" + fill="#232629" + id="path1716" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/projectOpen-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg10" + sodipodi:docname="projectOpen-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview12" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="12.028238" + inkscape:cy="11.546251" + inkscape:window-width="2580" + inkscape:window-height="1285" + inkscape:window-x="861" + inkscape:window-y="96" + inkscape:window-maximized="0" + inkscape:current-layer="svg10" /> + <defs + id="defs4"> + <style + type="text/css" + id="style2">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <g + id="g4755" + transform="matrix(0.75,0,0,0.75,2.75,0.25)"> + <path + d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z" + color="#eff0f1" + fill="currentColor" + id="path6" /> + <path + class="ColorScheme-Text" + d="m 12,12 v 9 h 9 V 13.5 H 17.2559 L 15.7559,12 15.75,12.0059 V 12 h -3 z m 0.75,0.75 h 2.6938 l 0.75146,0.75 h -0.44531 v 0.0059 l -0.0058,-0.0059 -1.5,1.5 h -1.4941 z m 0,3 h 7.5 v 4.5 h -7.5 z" + color="#eff0f1" + fill="currentColor" + id="path8" /> + </g> + <path + style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.4999998,17 v 1.333333 H 1 v 1.333334 H 8.4999998 V 21 H 13.499999 V 19.666667 H 21 V 18.333333 H 13.499999 V 17 H 8.4999998 M 11,17.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2500002,-0.594666 -1.2500002,-1.333333 0,-0.738667 0.5575002,-1.333333 1.2500002,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-dark/projectSaveAs-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg10" + sodipodi:docname="projectSaveAs-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview12" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="12.135346" + inkscape:cy="11.567673" + inkscape:window-width="2580" + inkscape:window-height="1281" + inkscape:window-x="861" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg10" /> + <defs + id="defs4"> + <style + type="text/css" + id="style2">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <g + id="g2432" + transform="matrix(0.74999663,0,0,0.74999663,2.7500034,0.25000337)"> + <g + id="g2427"> + <path + d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z" + color="#eff0f1" + fill="currentColor" + id="path6" /> + </g> + <path + class="ColorScheme-Text" + d="m 12,12.5 v 8.4969 h 4.5 V 20.46594 H 14 v -3.186 h 2.5 v -0.531 h -3 v 3.717 h -1 v -7.4339 H 14 v 2.655 h 4 v -2.655 h 0.14648 l 1.3535,1.4374 v 1.7486 h 0.5 v -1.9643 h -0.0039 l 0.0039,-0.0051 -1.6465,-1.7486 -0.0039,0.0051 v -0.0051 h -0.34961 z m 2.5,0.53203 h 1.9502 v 2.124 H 14.5 Z m 5.5,3.717 -0.49805,0.52788 h -0.0019 l -2.5,2.6539 0.0039,0.0051 -0.0019,0.53307 h -0.0019 v 0.531 h 1 l 0.0029,-0.0051 h 0.0068 l -0.0029,-0.0051 0.49316,-0.52374 2,-2.1219 -0.35254,-0.37647 -2.4951,2.6477 -0.29688,-0.31424 2.4961,-2.6488 0.2959,0.31528 0.35254,0.37336 0.49996,-0.531 z" + color="#eff0f1" + fill="currentColor" + id="path8" /> + </g> + <path + style="color:#eff0f1;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.4999998,17 v 1.333333 H 1 v 1.333334 H 8.4999998 V 21 H 13.499999 V 19.666667 H 21 V 18.333333 H 13.499999 V 17 H 8.4999998 M 11,17.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2500002,-0.594666 -1.2500002,-1.333333 0,-0.738667 0.5575002,-1.333333 1.2500002,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/dialog-cancel.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dialog-cancel.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.53554" + inkscape:cy="11.332035" + inkscape:window-width="2580" + inkscape:window-height="1327" + inkscape:window-x="861" + inkscape:window-y="54" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 11,1 C 8.4655025,1 6.16085,1.9458675 4.3984375,3.5 4.2406388,3.63915 4.0784,3.780975 3.9296875,3.9296875 L 3.5,4.3984375 C 1.9458675,6.16085 1,8.4655025 1,11 c 0,5.52285 4.47715,10 10,10 2.534497,0 4.83915,-0.945867 6.601562,-2.5 l 0.46875,-0.429688 C 18.219025,17.9216 18.36085,17.759361 18.5,17.601562 20.054133,15.83915 21,13.534497 21,11 21,5.47715 16.52285,1 11,1 m 0,1.25 c 4.832487,0 8.75,3.9175125 8.75,8.75 0,2.192075 -0.816519,4.167835 -2.148438,5.703125 L 5.296875,4.3984375 C 6.8321638,3.0665187 8.807925,2.25 11,2.25 M 4.3984375,5.296875 16.703125,17.601562 C 15.167835,18.933481 13.192075,19.75 11,19.75 6.1675125,19.75 2.25,15.832487 2.25,11 2.25,8.807925 3.0665187,6.8321638 4.3984375,5.296875" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/dialog-ok.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dialog-ok.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.514119" + inkscape:cy="11.26777" + inkscape:window-width="2580" + inkscape:window-height="1335" + inkscape:window-x="861" + inkscape:window-y="46" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="M 20.116087,3.0000133 7.0676375,17.088623 1.8839,11.491654 1,12.446029 l 5.1837375,5.596943 -0.0025,0.0027 L 7.06515,19 7.06765,18.997333 7.07015,19 7.9540375,18.045638 7.9515375,18.042972 21,3.9543483 20.1161,3 Z" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/dirNew.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="dirNew.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.514119" + inkscape:cy="11.246349" + inkscape:window-width="2580" + inkscape:window-height="1351" + inkscape:window-x="0" + inkscape:window-y="30" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 1,1 V 2.2499998 20.999996 H 2.2499998 13.499998 v -1.25 H 2.2499998 V 9.7499984 h 3.7499993 v -0.012208 l 0.00977,0.012208 2.4999995,-2.4999996 H 19.749998 v 6.2499992 h 1.25 V 3.4999995 H 12.259764 L 9.7597646,1 9.7500021,1.0122125 V 1 H 2.2500035 1.0000037 M 17.250001,14.749997 v 2.5 h -2.5 v 1.25 h 2.5 v 2.499999 H 18.5 V 18.499997 H 21 v -1.25 h -2.5 v -2.5 h -1.249999" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/fileSaveAsRemote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg8" + sodipodi:docname="fileSaveAsRemote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview10" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.53554" + inkscape:cy="11.246349" + inkscape:window-width="2580" + inkscape:window-height="1381" + inkscape:window-x="861" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg8" /> + <defs + id="defs4"> + <style + type="text/css" + id="style2">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <path + class="ColorScheme-Text" + d="M 2,1.0001508 V 15.994083 h 9.000144 V 15.057015 H 6.000024 V 9.4348525 h 5.00012 V 8.4977834 H 5.000018 V 15.057015 H 3.000006 V 1.9388862 h 3.000018 v 4.6851769 h 8.000138 V 1.9388862 h 0.292973 L 17.000198,4.47543 v 3.0856185 h 1.000006 V 4.0947693 h -0.0078 l 0.0078,-0.00915 L 14.707141,1 14.699341,1.00915 V 1 h -0.69923 z m 5.00012,0.9388187 h 3.900482 V 5.6870777 H 7.00012 Z M 18.000096,8.4982001 17.00396,9.4297694 h -0.0039 l -5.00012,4.6833436 0.0078,0.0092 -0.0039,0.940652 h -0.0039 V 16 h 2.000012 l 0.0059,-0.0092 h 0.01364 l -0.0059,-0.0092 0.986326,-0.924235 4.000024,-3.744443 -0.705089,-0.664339 -4.99031,4.672344 -0.593758,-0.554533 4.99229,-4.674177 0.591805,0.556358 0.705089,0.658848 L 20,10.370438 Z" + color="#eff0f1" + fill="#232629" + id="path6" + style="stroke-width:0.866025" /> + <path + style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.2499999,-0.594666 -1.2499999,-1.333333 0,-0.738667 0.5574999,-1.333333 1.2499999,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/open-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 22 22" + version="1.1" + id="svg6" + sodipodi:docname="open-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview8" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.514119" + inkscape:cy="11.26777" + inkscape:window-width="2580" + inkscape:window-height="1333" + inkscape:window-x="861" + inkscape:window-y="48" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <defs + id="defs3051"> + <style + type="text/css" + id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path + style="fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.25" + d="M 1,1 V 2.25 16 H 2.2499998 V 9.75 H 5.9999991 V 9.74023 L 6.0097653,9.75 8.5097648,7.25 H 19.749998 V 16 h 1.25 V 3.5 H 12.259764 L 9.7597646,1 9.7500046,1.00976 V 1 H 2.2500035 1.0000037 M 8.5000023,17.25 V 18.5 H 1.0000037 v 1.25 H 8.5000023 V 21 H 13.500001 V 19.75 H 21 V 18.5 H 13.500001 V 17.25 H 8.5000023 m 2.4999997,0.625 c 0.6925,0 1.25,0.5575 1.25,1.25 0,0.6925 -0.5575,1.25 -1.25,1.25 -0.6925,0 -1.2499999,-0.5575 -1.2499999,-1.25 0,-0.6925 0.5574999,-1.25 1.2499999,-1.25" + class="ColorScheme-Text" + id="path4" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/preferences-eric-server.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + height="22" + width="22" + version="1.1" + id="svg3827" + sodipodi:docname="preferences-eric-server.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs3831" /> + <sodipodi:namedview + id="namedview3829" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="32.09375" + inkscape:cx="16.747809" + inkscape:cy="16.389484" + inkscape:window-width="2580" + inkscape:window-height="1321" + inkscape:window-x="0" + inkscape:window-y="60" + inkscape:window-maximized="0" + inkscape:current-layer="svg3827" /> + <linearGradient + id="b" + gradientTransform="matrix(0.7,0,0,0.7,-0.7,-1.1)" + gradientUnits="userSpaceOnUse" + x2="0" + y1="44" + y2="4"> + <stop + offset="0" + stop-color="#1d1e1e" + id="stop3789" /> + <stop + offset="1" + stop-color="#44484c" + id="stop3791" /> + </linearGradient> + <linearGradient + id="c" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-350.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="a" + gradientUnits="userSpaceOnUse" + x2="0" + y1="507.79999" + y2="506.79999"> + <stop + offset="0" + stop-color="#3da103" + id="stop3795" /> + <stop + offset="1" + stop-color="#7ddf07" + id="stop3797" /> + </linearGradient> + <linearGradient + id="d" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-339.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="e" + gradientTransform="matrix(0.7,0,0,0.7,-270.499,-328.76)" + x2="0" + xlink:href="#a" + y1="507.79999" + y2="506.79999" /> + <linearGradient + id="f" + gradientUnits="userSpaceOnUse" + x1="5" + x2="18" + y1="12" + y2="25"> + <stop + offset="0" + stop-color="#292c2f" + id="stop3802" /> + <stop + offset="1" + stop-opacity="0" + id="stop3804" /> + </linearGradient> + <g + id="g5049" + transform="matrix(0.72413793,0,0,0.71428571,-1.1724138,-0.42857143)"> + <path + d="M 3,7 H 29 V 24 H 3 Z" + fill="#111213" + id="path3807" /> + <g + stroke-width="0.7" + id="g3817"> + <path + d="M 3,2 V 30 H 29 V 2 Z m 1,6 h 24 v 4 H 4 Z m 0,11 h 24 v 4 H 4 Z" + fill="url(#b)" + id="path3809" + style="fill:url(#b)" /> + <path + d="M 5,4 H 7 V 5 H 5 Z" + fill="url(#c)" + id="path3811" + style="fill:url(#c)" /> + <path + d="m 5,15 h 2 v 1 H 5 Z" + fill="url(#d)" + id="path3813" + style="fill:url(#d)" /> + <path + d="m 5,26 h 2 v 1 H 5 Z" + fill="url(#e)" + id="path3815" + style="fill:url(#e)" /> + </g> + <path + d="m 3,29 h 26 v 1 H 3 Z" + opacity="0.2" + id="path3819" /> + <path + d="m 4,12 7,7 h 17 v 4 H 15 l 7,7 h 7 V 13 l -1,-1 z" + fill="url(#f)" + fill-rule="evenodd" + opacity="0.4" + id="path3821" + style="fill:url(#f)" /> + <rect + fill="#eff0f1" + height="16" + rx="2" + width="16" + x="16" + y="14" + id="rect3823" /> + <path + d="m 19,16 v 2 h -1 v 2 h 1 v 4 h -1 v 2 h 1 v 2 H 30 V 16 Z m 1,1 h 1 v 10 h -1 z m 2,0 h 7 v 10 h -7 z" + fill="#232629" + id="path3825" /> + </g> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/projectOpen-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg1062" + sodipodi:docname="projectOpen-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1064" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.289192" + inkscape:cy="11.803311" + inkscape:window-width="1857" + inkscape:window-height="1337" + inkscape:window-x="62" + inkscape:window-y="17" + inkscape:window-maximized="0" + inkscape:current-layer="svg1062" /> + <defs + id="defs1056"> + <style + type="text/css" + id="style1054">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <g + id="g9300" + transform="matrix(0.75,0,0,0.75,2.75,0.25)"> + <path + d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z" + color="#eff0f1" + fill="#232629" + id="path1058" /> + <path + class="ColorScheme-Text" + d="m 12,12 v 9 h 9 V 13.5 H 17.2559 L 15.7559,12 15.75,12.0059 V 12 h -3 z m 0.75,0.75 h 2.6938 l 0.75146,0.75 h -0.44531 v 0.0059 l -0.0058,-0.0059 -1.5,1.5 h -1.4941 z m 0,3 h 7.5 v 4.5 h -7.5 z" + color="#eff0f1" + fill="#232629" + id="path1060" /> + </g> + <path + style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.7679983,17 v 1.333333 H 1.268 v 1.333334 H 8.7679983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.767997 V 17 H 8.7679983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.25,-0.594666 -1.25,-1.333333 0,-0.738667 0.5575,-1.333333 1.25,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/eric7/icons/breeze-light/projectSaveAs-remote.svg Fri Feb 23 10:46:46 2024 +0100 @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + version="1.1" + viewBox="0 0 22 22" + id="svg10" + sodipodi:docname="projectSaveAs-remote.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview12" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="46.681818" + inkscape:cx="11.407011" + inkscape:cy="11.214216" + inkscape:window-width="2580" + inkscape:window-height="1274" + inkscape:window-x="861" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg10" /> + <defs + id="defs4"> + <style + type="text/css" + id="style2">.ColorScheme-Text { + color:#eff0f1; + }</style> + </defs> + <g + id="g2428" + transform="matrix(0.74999663,0,0,0.74999663,2.7500034,0.25000337)"> + <path + d="M 3.5,1 V 3.5 H 1 v 3.75 h 2.5 v 7.5 H 1 V 18.5 H 3.5 V 21 H 11 V 19.75 H 4.75 V 6 h 15 v 5 H 21 V 1 Z m 1.25,1.25 h 15 v 2.5 h -15 z" + color="#eff0f1" + fill="#232629" + id="path6" /> + <path + class="ColorScheme-Text" + d="m 12,12.5 v 8.4969 h 4.5 V 20.46594 H 14 v -3.186 h 2.5 v -0.531 h -3 v 3.717 h -1 v -7.4339 H 14 v 2.655 h 4 v -2.655 h 0.14648 l 1.3535,1.4374 v 1.7486 h 0.5 v -1.9643 h -0.0039 l 0.0039,-0.0051 -1.6465,-1.7486 -0.0039,0.0051 v -0.0051 h -0.34961 z m 2.5,0.53203 h 1.9502 v 2.124 H 14.5 Z m 5.5,3.717 -0.49805,0.52788 h -0.0019 l -2.5,2.6539 0.0039,0.0051 -0.0019,0.53307 h -0.0019 v 0.531 h 1 l 0.0029,-0.0051 h 0.0068 l -0.0029,-0.0051 0.49316,-0.52374 2,-2.1219 -0.35254,-0.37647 -2.4951,2.6477 -0.29688,-0.31424 2.4961,-2.6488 0.2959,0.31528 0.35254,0.37336 0.49996,-0.531 z" + color="#eff0f1" + fill="#232629" + id="path8" /> + </g> + <path + style="color:#232629;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:1.29099" + d="m 8.4999983,17 v 1.333333 H 1 v 1.333334 H 8.4999983 V 21 h 4.9999987 v -1.333333 h 7.499999 V 18.333333 H 13.499997 V 17 H 8.4999983 m 2.4999997,0.666667 c 0.6925,0 1.25,0.594666 1.25,1.333333 0,0.738667 -0.5575,1.333333 -1.25,1.333333 -0.6925,0 -1.25,-0.594666 -1.25,-1.333333 0,-0.738667 0.5575,-1.333333 1.25,-1.333333" + class="ColorScheme-Text" + id="path4" + sodipodi:nodetypes="ccccccccccccccsssc" /> +</svg>