diff --git a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
index 37174ca39cb9..03aee51b2777 100644
--- a/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
+++ b/python/PyQt6/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
@@ -90,6 +90,15 @@ This method calls :py:func:`QgsCodeEditor.clearWarnings()`, but also removes
highlights from the widget scrollbars at the warning locations.
.. seealso:: :py:func:`addWarning`
+%End
+
+ QString filePath() const;
+%Docstring
+Returns the widget's associated file path.
+
+.. seealso:: :py:func:`setFilePath`
+
+.. seealso:: :py:func:`filePathChanged`
%End
public slots:
@@ -136,6 +145,24 @@ Triggers a find operation, using the default behavior.
This will automatically open the search bar and start a find operation using
the default behavior, e.g. searching for any selected text in the code editor.
+%End
+
+ void setFilePath( const QString &path );
+%Docstring
+Sets the widget's associated file ``path``.
+
+.. seealso:: :py:func:`filePathChanged`
+
+.. seealso:: :py:func:`filePath`
+%End
+
+ bool openInExternalEditor();
+%Docstring
+Attempts to opens the script from the editor in an external text editor.
+
+This requires that the widget has an associated :py:func:`~QgsCodeEditorWidget.filePath` set.
+
+:return: ``True`` if the file was opened successfully.
%End
signals:
@@ -143,6 +170,15 @@ the default behavior, e.g. searching for any selected text in the code editor.
void searchBarToggled( bool visible );
%Docstring
Emitted when the visibility of the search bar is changed.
+%End
+
+ void filePathChanged( const QString &path );
+%Docstring
+Emitted when the widget's associated file path is changed.
+
+.. seealso:: :py:func:`setFilePath`
+
+.. seealso:: :py:func:`filePath`
%End
};
diff --git a/python/console/console.py b/python/console/console.py
index adc5ff55873f..612de970a146 100644
--- a/python/console/console.py
+++ b/python/console/console.py
@@ -18,6 +18,7 @@
Some portions of code were taken from https://code.google.com/p/pydee/
"""
import os
+import subprocess
from qgis.PyQt.QtCore import Qt, QTimer, QCoreApplication, QSize, QByteArray, QFileInfo, QUrl, QDir
from qgis.PyQt.QtWidgets import QToolBar, QToolButton, QWidget, QSplitter, QTreeWidget, QAction, QFileDialog, QCheckBox, QSizePolicy, QMenu, QGridLayout, QApplication, QShortcut
@@ -587,12 +588,7 @@ def reformatCode(self):
def openScriptFileExtEditor(self):
tabWidget = self.tabEditorWidget.currentWidget()
- path = tabWidget.path
- import subprocess
- try:
- subprocess.Popen([os.environ['EDITOR'], path])
- except KeyError:
- QDesktopServices.openUrl(QUrl.fromLocalFile(path))
+ tabWidget.open_in_external_editor()
def openScriptFile(self):
settings = QgsSettings()
@@ -604,7 +600,7 @@ def openScriptFile(self):
for pyFile in fileList:
for i in range(self.tabEditorWidget.count()):
tabWidget = self.tabEditorWidget.widget(i)
- if tabWidget.path == pyFile:
+ if tabWidget.file_path() == pyFile:
self.tabEditorWidget.setCurrentWidget(tabWidget)
break
else:
@@ -621,7 +617,7 @@ def saveScriptFile(self):
tabWidget.save()
except (IOError, OSError) as error:
msgText = QCoreApplication.translate('PythonConsole',
- 'The file {0} could not be saved. Error: {1}').format(tabWidget.path,
+ 'The file {0} could not be saved. Error: {1}').format(tabWidget.file_path(),
error.strerror)
self.callWidgetMessageBarEditor(msgText, Qgis.MessageLevel.Critical)
@@ -629,13 +625,13 @@ def saveAsScriptFile(self, index=None):
tabWidget = self.tabEditorWidget.currentWidget()
if not index:
index = self.tabEditorWidget.currentIndex()
- if not tabWidget.path:
+ if not tabWidget.file_path():
fileName = self.tabEditorWidget.tabText(index).replace('*', '') + '.py'
folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath())
pathFileName = os.path.join(folder, fileName)
fileNone = True
else:
- pathFileName = tabWidget.path
+ pathFileName = tabWidget.file_path()
fileNone = False
saveAsFileTr = QCoreApplication.translate("PythonConsole", "Save File As")
filename, filter = QFileDialog.getSaveFileName(self,
@@ -648,13 +644,13 @@ def saveAsScriptFile(self, index=None):
tabWidget.save(filename)
except (IOError, OSError) as error:
msgText = QCoreApplication.translate('PythonConsole',
- 'The file {0} could not be saved. Error: {1}').format(tabWidget.path,
+ 'The file {0} could not be saved. Error: {1}').format(tabWidget.file_path(),
error.strerror)
self.callWidgetMessageBarEditor(msgText, Qgis.MessageLevel.Critical)
if fileNone:
- tabWidget.path = None
+ tabWidget.set_file_path(None)
else:
- tabWidget.path = pathFileName
+ tabWidget.set_file_path(pathFileName)
return
if not fileNone:
diff --git a/python/console/console_editor.py b/python/console/console_editor.py
index cc58926d97ff..d03afa459ac5 100644
--- a/python/console/console_editor.py
+++ b/python/console/console_editor.py
@@ -93,7 +93,6 @@ def __init__(self,
self.tab_widget: EditorTabWidget = tab_widget
self.code_editor_widget: Optional[QgsCodeEditorWidget] = None
- self.path: Optional[str] = None
# recent modification time
self.lastModified = 0
@@ -293,7 +292,7 @@ def shareOnGist(self, is_public):
URL = "https://api.github.com/gists"
- path = self.tab_widget.currentWidget().path
+ path = self.code_editor_widget.filePath()
filename = os.path.basename(path) if path else None
filename = filename if filename else "pyqgis_snippet.py"
@@ -334,8 +333,7 @@ def createTempFile(self):
def runScriptCode(self):
autoSave = QgsSettings().value("pythonConsole/autoSaveScript", False, type=bool)
- tabWidget = self.tab_widget.currentWidget()
- filename = tabWidget.path
+ filename = self.code_editor_widget.filePath()
filename_override = None
msgEditorBlank = QCoreApplication.translate('PythonConsole',
'Hey, type something to run!')
@@ -402,34 +400,33 @@ def syntaxCheck(self):
return True
def focusInEvent(self, e):
- if self.path:
- if not QFileInfo(self.path).exists():
+ if self.code_editor_widget.filePath():
+ if not QFileInfo(self.code_editor_widget.filePath()).exists():
msgText = QCoreApplication.translate('PythonConsole',
- 'The file "{0}" has been deleted or is not accessible').format(self.path)
+ 'The file "{0}" has been deleted or is not accessible').format(self.code_editor_widget.filePath())
self.showMessage(msgText,
level=Qgis.MessageLevel.Critical)
return
- if self.path and self.lastModified != QFileInfo(self.path).lastModified():
+ if self.code_editor_widget.filePath() and self.lastModified != QFileInfo(self.code_editor_widget.filePath()).lastModified():
self.beginUndoAction()
self.selectAll()
self.removeSelectedText()
- self.insert(Path(self.path).read_text(encoding='utf-8'))
+ self.insert(Path(self.code_editor_widget.filePath()).read_text(encoding='utf-8'))
self.setModified(False)
self.endUndoAction()
self.tab_widget.listObject(self.tab_widget.currentWidget())
- self.lastModified = QFileInfo(self.path).lastModified()
+ self.lastModified = QFileInfo(self.code_editor_widget.filePath()).lastModified()
super().focusInEvent(e)
def fileReadOnly(self):
- tabWidget = self.tab_widget.currentWidget()
msgText = QCoreApplication.translate('PythonConsole',
- 'The file "{0}" is read only, please save to different file first.').format(tabWidget.path)
+ 'The file "{0}" is read only, please save to different file first.').format(self.code_editor_widget.filePath())
self.showMessage(msgText)
def loadFile(self, filename: str, read_only: bool = False):
self.lastModified = QFileInfo(filename).lastModified()
- self.path = filename
+ self.code_editor_widget.setFilePath(filename)
self.setText(Path(filename).read_text(encoding='utf-8'))
self.setReadOnly(read_only)
self.setModified(False)
@@ -444,19 +441,20 @@ def save(self, filename: Optional[str] = None):
index = self.tab_widget.indexOf(self.editor_tab)
if filename:
- self.path = filename
- if not self.path:
+ self.code_editor_widget.setFilePath(filename)
+ if not self.code_editor_widget.filePath():
saveTr = QCoreApplication.translate('PythonConsole',
'Python Console: Save file')
folder = QgsSettings().value("pythonConsole/lastDirPath", QDir.homePath())
- self.path, filter = QFileDialog().getSaveFileName(self,
- saveTr,
- os.path.join(folder, self.tab_widget.tabText(index).replace('*', '') + '.py'),
- "Script file (*.py)")
+ path, filter = QFileDialog().getSaveFileName(self,
+ saveTr,
+ os.path.join(folder, self.tab_widget.tabText(index).replace('*', '') + '.py'),
+ "Script file (*.py)")
# If the user didn't select a file, abort the save operation
- if not self.path:
- self.path = None
+ if not path:
+ self.code_editor_widget.setFilePath(None)
return
+ self.code_editor_widget.setFilePath(path)
msgText = QCoreApplication.translate('PythonConsole',
'Script was correctly saved.')
@@ -464,17 +462,17 @@ def save(self, filename: Optional[str] = None):
# Save the new contents
# Need to use newline='' to avoid adding extra \r characters on Windows
- with open(self.path, 'w', encoding='utf-8', newline='') as f:
+ with open(self.code_editor_widget.filePath(), 'w', encoding='utf-8', newline='') as f:
f.write(self.text())
- self.tab_widget.setTabTitle(index, Path(self.path).name)
- self.tab_widget.setTabToolTip(index, self.path)
+ self.tab_widget.setTabTitle(index, Path(self.code_editor_widget.filePath()).name)
+ self.tab_widget.setTabToolTip(index, self.code_editor_widget.filePath())
self.setModified(False)
self.console_widget.saveFileButton.setEnabled(False)
- self.lastModified = QFileInfo(self.path).lastModified()
- self.console_widget.updateTabListScript(self.path, action='append')
+ self.lastModified = QFileInfo(self.code_editor_widget.filePath()).lastModified()
+ self.console_widget.updateTabListScript(self.code_editor_widget.filePath(), action='append')
self.tab_widget.listObject(self.editor_tab)
QgsSettings().setValue("pythonConsole/lastDirPath",
- Path(self.path).parent.as_posix())
+ Path(self.code_editor_widget.filePath()).parent.as_posix())
def event(self, e):
""" Used to override the Application shortcuts when the editor has focus """
@@ -564,6 +562,15 @@ def __init__(self,
self.tabLayout.setContentsMargins(0, 0, 0, 0)
self.tabLayout.addWidget(self._editor_code_widget)
+ def set_file_path(self, path: str):
+ self._editor_code_widget.setFilePath(path)
+
+ def file_path(self) -> Optional[str]:
+ return self._editor_code_widget.filePath()
+
+ def open_in_external_editor(self):
+ self._editor_code_widget.openInExternalEditor()
+
def modified(self, modified):
self.tab_widget.tabModified(self, modified)
@@ -850,14 +857,14 @@ def _removeTab(self, tab, tab2index=False):
return
if res == QMessageBox.StandardButton.Save:
editorTab.save()
- if editorTab.path:
- self.console_widget.updateTabListScript(editorTab.path, action='remove')
+ if editorTab.code_editor_widget.filePath():
+ self.console_widget.updateTabListScript(editorTab.code_editor_widget.filePath(), action='remove')
self.removeTab(tab)
if self.count() < 1:
self.newTabEditor()
else:
- if editorTab.path:
- self.console_widget.updateTabListScript(editorTab.path, action='remove')
+ if editorTab.code_editor_widget.filePath():
+ self.console_widget.updateTabListScript(editorTab.code_editor_widget.filePath(), action='remove')
if self.count() <= 1:
self.removeTab(tab)
self.newTabEditor()
@@ -923,8 +930,8 @@ def listObject(self, tab):
else:
tabWidget = self.widget(tab)
if tabWidget:
- if tabWidget.path:
- pathFile, file = os.path.split(tabWidget.path)
+ if tabWidget.file_path():
+ pathFile, file = os.path.split(tabWidget.file_path())
module, ext = os.path.splitext(file)
found = False
if pathFile not in sys.path:
@@ -936,7 +943,7 @@ def listObject(self, tab):
readModule = pyclbr.readmodule(module)
readModuleFunction = pyclbr.readmodule_ex(module)
for name, class_data in sorted(list(readModule.items()), key=lambda x: x[1].lineno):
- if os.path.normpath(class_data.file) == os.path.normpath(tabWidget.path):
+ if os.path.normpath(class_data.file) == os.path.normpath(tabWidget.file_path()):
superClassName = []
for superClass in class_data.super:
if superClass == 'object':
@@ -973,7 +980,7 @@ def listObject(self, tab):
self.console_widget.listClassMethod.addTopLevelItem(classItem)
for func_name, data in sorted(list(readModuleFunction.items()), key=lambda x: x[1].lineno):
if isinstance(data, pyclbr.Function) and \
- os.path.normpath(data.file) == os.path.normpath(tabWidget.path):
+ os.path.normpath(data.file) == os.path.normpath(tabWidget.file_path()):
funcItem = QTreeWidgetItem()
funcItem.setText(0, func_name + ' ')
funcItem.setText(1, str(data.lineno))
@@ -1009,9 +1016,9 @@ def refreshSettingsEditor(self):
def changeLastDirPath(self, tab):
tabWidget = self.widget(tab)
- if tabWidget and tabWidget.path:
+ if tabWidget and tabWidget.file_path():
QgsSettings().setValue("pythonConsole/lastDirPath",
- Path(tabWidget.path).parent.as_posix())
+ Path(tabWidget.file_path()).parent.as_posix())
def showMessage(self, text, level=Qgis.MessageLevel.Info, timeout=-1, title=""):
currWidget = self.currentWidget()
diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
index 37174ca39cb9..03aee51b2777 100644
--- a/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
+++ b/python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
@@ -90,6 +90,15 @@ This method calls :py:func:`QgsCodeEditor.clearWarnings()`, but also removes
highlights from the widget scrollbars at the warning locations.
.. seealso:: :py:func:`addWarning`
+%End
+
+ QString filePath() const;
+%Docstring
+Returns the widget's associated file path.
+
+.. seealso:: :py:func:`setFilePath`
+
+.. seealso:: :py:func:`filePathChanged`
%End
public slots:
@@ -136,6 +145,24 @@ Triggers a find operation, using the default behavior.
This will automatically open the search bar and start a find operation using
the default behavior, e.g. searching for any selected text in the code editor.
+%End
+
+ void setFilePath( const QString &path );
+%Docstring
+Sets the widget's associated file ``path``.
+
+.. seealso:: :py:func:`filePathChanged`
+
+.. seealso:: :py:func:`filePath`
+%End
+
+ bool openInExternalEditor();
+%Docstring
+Attempts to opens the script from the editor in an external text editor.
+
+This requires that the widget has an associated :py:func:`~QgsCodeEditorWidget.filePath` set.
+
+:return: ``True`` if the file was opened successfully.
%End
signals:
@@ -143,6 +170,15 @@ the default behavior, e.g. searching for any selected text in the code editor.
void searchBarToggled( bool visible );
%Docstring
Emitted when the visibility of the search bar is changed.
+%End
+
+ void filePathChanged( const QString &path );
+%Docstring
+Emitted when the widget's associated file path is changed.
+
+.. seealso:: :py:func:`setFilePath`
+
+.. seealso:: :py:func:`filePath`
%End
};
diff --git a/src/gui/codeeditors/qgscodeeditorwidget.cpp b/src/gui/codeeditors/qgscodeeditorwidget.cpp
index 442ff3a0f1ce..a8e555eb0b01 100644
--- a/src/gui/codeeditors/qgscodeeditorwidget.cpp
+++ b/src/gui/codeeditors/qgscodeeditorwidget.cpp
@@ -26,6 +26,10 @@
#include
#include
#include
+#include
+#include
+#include
+#include
QgsCodeEditorWidget::QgsCodeEditorWidget(
QgsCodeEditor *editor,
@@ -337,6 +341,48 @@ void QgsCodeEditorWidget::triggerFind()
showSearchBar();
}
+void QgsCodeEditorWidget::setFilePath( const QString &path )
+{
+ if ( mFilePath == path )
+ return;
+
+ mFilePath = path;
+ emit filePathChanged( mFilePath );
+}
+
+bool QgsCodeEditorWidget::openInExternalEditor()
+{
+ if ( mFilePath.isEmpty() )
+ return false;
+
+ const QDir dir = QFileInfo( mFilePath ).dir();
+
+ bool useFallback = true;
+
+ const QString editorCommand = qgetenv( "EDITOR" );
+ if ( !editorCommand.isEmpty() )
+ {
+ const QFileInfo fi( editorCommand );
+ if ( fi.exists( ) )
+ {
+ const QString command = fi.fileName();
+ const bool isTerminalEditor = command.compare( QLatin1String( "nano" ), Qt::CaseInsensitive ) == 0
+ || command.contains( QLatin1String( "vim" ), Qt::CaseInsensitive );
+
+ if ( !isTerminalEditor && QProcess::startDetached( editorCommand, {mFilePath}, dir.absolutePath() ) )
+ {
+ useFallback = false;
+ }
+ }
+ }
+
+ if ( useFallback )
+ {
+ QDesktopServices::openUrl( QUrl::fromLocalFile( mFilePath ) );
+ }
+ return true;
+}
+
bool QgsCodeEditorWidget::findNext()
{
return findText( true, false );
diff --git a/src/gui/codeeditors/qgscodeeditorwidget.h b/src/gui/codeeditors/qgscodeeditorwidget.h
index a38c22849740..27f344a1bdf9 100644
--- a/src/gui/codeeditors/qgscodeeditorwidget.h
+++ b/src/gui/codeeditors/qgscodeeditorwidget.h
@@ -106,6 +106,14 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget
*/
void clearWarnings();
+ /**
+ * Returns the widget's associated file path.
+ *
+ * \see setFilePath()
+ * \see filePathChanged()
+ */
+ QString filePath() const { return mFilePath; }
+
public slots:
/**
@@ -148,6 +156,23 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget
*/
void triggerFind();
+ /**
+ * Sets the widget's associated file \a path.
+ *
+ * \see filePathChanged()
+ * \see filePath()
+ */
+ void setFilePath( const QString &path );
+
+ /**
+ * Attempts to opens the script from the editor in an external text editor.
+ *
+ * This requires that the widget has an associated filePath() set.
+ *
+ * \returns TRUE if the file was opened successfully.
+ */
+ bool openInExternalEditor();
+
signals:
/**
@@ -155,6 +180,14 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget
*/
void searchBarToggled( bool visible );
+ /**
+ * Emitted when the widget's associated file path is changed.
+ *
+ * \see setFilePath()
+ * \see filePath()
+ */
+ void filePathChanged( const QString &path );
+
private slots:
bool findNext();
@@ -196,6 +229,7 @@ class GUI_EXPORT QgsCodeEditorWidget : public QgsPanelWidget
int mBlockSearching = 0;
QgsMessageBar *mMessageBar = nullptr;
std::unique_ptr< QgsScrollBarHighlightController > mHighlightController;
+ QString mFilePath;
};
#endif // QGSCODEEDITORWIDGET_H