Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix python script editor "open in external editor" action #57682

Merged
merged 4 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -136,13 +145,40 @@ 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:

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

};
Expand Down
22 changes: 9 additions & 13 deletions python/console/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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:
Expand All @@ -621,21 +617,21 @@ def saveScriptFile(self):
tabWidget.save()
except (IOError, OSError) as error:
msgText = QCoreApplication.translate('PythonConsole',
'The file <b>{0}</b> could not be saved. Error: {1}').format(tabWidget.path,
'The file <b>{0}</b> could not be saved. Error: {1}').format(tabWidget.file_path(),
error.strerror)
self.callWidgetMessageBarEditor(msgText, Qgis.MessageLevel.Critical)

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,
Expand All @@ -648,13 +644,13 @@ def saveAsScriptFile(self, index=None):
tabWidget.save(filename)
except (IOError, OSError) as error:
msgText = QCoreApplication.translate('PythonConsole',
'The file <b>{0}</b> could not be saved. Error: {1}').format(tabWidget.path,
'The file <b>{0}</b> 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:
Expand Down
81 changes: 44 additions & 37 deletions python/console/console_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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!')
Expand Down Expand Up @@ -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 <b>"{0}"</b> has been deleted or is not accessible').format(self.path)
'The file <b>"{0}"</b> 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 <b>"{0}"</b> is read only, please save to different file first.').format(tabWidget.path)
'The file <b>"{0}"</b> 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)
Expand All @@ -444,37 +441,38 @@ 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.')
self.showMessage(msgText)

# 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 """
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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':
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions python/gui/auto_generated/codeeditors/qgscodeeditorwidget.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -136,13 +145,40 @@ 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:

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

};
Expand Down
Loading
Loading