mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-11 00:04:09 -04:00
323 lines
11 KiB
Python
323 lines
11 KiB
Python
"""
|
|
***************************************************************************
|
|
EditScriptDialog.py
|
|
---------------------
|
|
Date : December 2012
|
|
Copyright : (C) 2012 by Alexander Bruy
|
|
Email : alexander dot bruy at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = "Alexander Bruy"
|
|
__date__ = "December 2012"
|
|
__copyright__ = "(C) 2012, Alexander Bruy"
|
|
|
|
import os
|
|
import codecs
|
|
import inspect
|
|
import traceback
|
|
import warnings
|
|
|
|
from qgis.PyQt import uic, sip
|
|
from qgis.PyQt.QtCore import Qt
|
|
from qgis.PyQt.QtGui import QPalette
|
|
from qgis.PyQt.QtWidgets import QMessageBox, QFileDialog, QVBoxLayout
|
|
|
|
from qgis.gui import QgsGui, QgsErrorDialog, QgsCodeEditorWidget
|
|
from qgis.core import (
|
|
QgsApplication,
|
|
QgsFileUtils,
|
|
QgsSettings,
|
|
QgsError,
|
|
QgsProcessingAlgorithm,
|
|
QgsProcessingFeatureBasedAlgorithm,
|
|
)
|
|
from qgis.utils import iface, OverrideCursor
|
|
from qgis.processing import alg as algfactory
|
|
|
|
from processing.gui.AlgorithmDialog import AlgorithmDialog
|
|
from processing.script import ScriptUtils
|
|
|
|
from .ScriptEdit import ScriptEdit
|
|
|
|
pluginPath = os.path.split(os.path.dirname(__file__))[0]
|
|
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
WIDGET, BASE = uic.loadUiType(os.path.join(pluginPath, "ui", "DlgScriptEditor.ui"))
|
|
|
|
|
|
class ScriptEditorDialog(BASE, WIDGET):
|
|
hasChanged = False
|
|
|
|
DIALOG_STORE = []
|
|
|
|
def __init__(self, filePath=None, parent=None):
|
|
super().__init__(parent)
|
|
# SIP is totally messed up here -- the dialog wrapper or something
|
|
# is always prematurely cleaned which results in broken QObject
|
|
# connections throughout.
|
|
# Hack around this by storing dialog instances in a global list to
|
|
# prevent too early wrapper garbage collection
|
|
ScriptEditorDialog.DIALOG_STORE.append(self)
|
|
|
|
def clean_up_store():
|
|
ScriptEditorDialog.DIALOG_STORE = [
|
|
d for d in ScriptEditorDialog.DIALOG_STORE if d != self
|
|
]
|
|
|
|
self.destroyed.connect(clean_up_store)
|
|
|
|
self.setupUi(self)
|
|
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
|
|
|
QgsGui.instance().enableAutoGeometryRestore(self)
|
|
|
|
vl = QVBoxLayout()
|
|
vl.setContentsMargins(0, 0, 0, 0)
|
|
self.editor_container.setLayout(vl)
|
|
|
|
self.editor = ScriptEdit()
|
|
self.code_editor_widget = QgsCodeEditorWidget(self.editor)
|
|
vl.addWidget(self.code_editor_widget)
|
|
|
|
if iface is not None:
|
|
self.toolBar.setIconSize(iface.iconSize())
|
|
self.setStyleSheet(iface.mainWindow().styleSheet())
|
|
|
|
self.actionOpenScript.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionScriptOpen.svg")
|
|
)
|
|
self.actionSaveScript.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionFileSave.svg")
|
|
)
|
|
self.actionSaveScriptAs.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionFileSaveAs.svg")
|
|
)
|
|
self.actionRunScript.setIcon(QgsApplication.getThemeIcon("/mActionStart.svg"))
|
|
self.actionCut.setIcon(QgsApplication.getThemeIcon("/mActionEditCut.svg"))
|
|
self.actionCopy.setIcon(QgsApplication.getThemeIcon("/mActionEditCopy.svg"))
|
|
self.actionPaste.setIcon(QgsApplication.getThemeIcon("/mActionEditPaste.svg"))
|
|
self.actionUndo.setIcon(QgsApplication.getThemeIcon("/mActionUndo.svg"))
|
|
self.actionRedo.setIcon(QgsApplication.getThemeIcon("/mActionRedo.svg"))
|
|
self.actionFindReplace.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionFindReplace.svg")
|
|
)
|
|
self.actionIncreaseFontSize.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionIncreaseFont.svg")
|
|
)
|
|
self.actionDecreaseFontSize.setIcon(
|
|
QgsApplication.getThemeIcon("/mActionDecreaseFont.svg")
|
|
)
|
|
self.actionToggleComment.setIcon(
|
|
QgsApplication.getThemeIcon(
|
|
"console/iconCommentEditorConsole.svg",
|
|
self.palette().color(QPalette.ColorRole.WindowText),
|
|
)
|
|
)
|
|
|
|
# Connect signals and slots
|
|
self.actionOpenScript.triggered.connect(self.openScript)
|
|
self.actionSaveScript.triggered.connect(self.save)
|
|
self.actionSaveScriptAs.triggered.connect(self.saveAs)
|
|
self.actionRunScript.triggered.connect(self.runAlgorithm)
|
|
self.actionCut.triggered.connect(self.editor.cut)
|
|
self.actionCopy.triggered.connect(self.editor.copy)
|
|
self.actionPaste.triggered.connect(self.editor.paste)
|
|
self.actionUndo.triggered.connect(self.editor.undo)
|
|
self.actionRedo.triggered.connect(self.editor.redo)
|
|
self.actionFindReplace.toggled.connect(
|
|
self.code_editor_widget.setSearchBarVisible
|
|
)
|
|
self.code_editor_widget.searchBarToggled.connect(
|
|
self.actionFindReplace.setChecked
|
|
)
|
|
|
|
self.actionIncreaseFontSize.triggered.connect(self.editor.zoomIn)
|
|
self.actionDecreaseFontSize.triggered.connect(self.editor.zoomOut)
|
|
self.actionToggleComment.triggered.connect(self.editor.toggleComment)
|
|
self.editor.modificationChanged.connect(self._on_text_modified)
|
|
|
|
self.run_dialog = None
|
|
|
|
if filePath is not None:
|
|
self._loadFile(filePath)
|
|
|
|
self.setHasChanged(False)
|
|
|
|
def update_dialog_title(self):
|
|
"""
|
|
Updates the script editor dialog title
|
|
"""
|
|
if self.code_editor_widget.filePath():
|
|
path, file_name = os.path.split(self.code_editor_widget.filePath())
|
|
else:
|
|
file_name = self.tr("Untitled Script")
|
|
|
|
if self.hasChanged:
|
|
file_name = "*" + file_name
|
|
|
|
self.setWindowTitle(self.tr("{} - Processing Script Editor").format(file_name))
|
|
|
|
def closeEvent(self, event):
|
|
settings = QgsSettings()
|
|
settings.setValue("/Processing/stateScriptEditor", self.saveState())
|
|
settings.setValue("/Processing/geometryScriptEditor", self.saveGeometry())
|
|
|
|
if self.hasChanged:
|
|
ret = QMessageBox.question(
|
|
self,
|
|
self.tr("Save Script?"),
|
|
self.tr(
|
|
"There are unsaved changes in this script. Do you want to keep those?"
|
|
),
|
|
QMessageBox.StandardButton.Save
|
|
| QMessageBox.StandardButton.Cancel
|
|
| QMessageBox.StandardButton.Discard,
|
|
QMessageBox.StandardButton.Cancel,
|
|
)
|
|
|
|
if ret == QMessageBox.StandardButton.Save:
|
|
self.saveScript(False)
|
|
event.accept()
|
|
elif ret == QMessageBox.StandardButton.Discard:
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
else:
|
|
event.accept()
|
|
|
|
def openScript(self):
|
|
if self.hasChanged:
|
|
ret = QMessageBox.warning(
|
|
self,
|
|
self.tr("Unsaved changes"),
|
|
self.tr("There are unsaved changes in the script. Continue?"),
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if ret == QMessageBox.StandardButton.No:
|
|
return
|
|
|
|
scriptDir = ScriptUtils.scriptsFolders()[0]
|
|
fileName, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
self.tr("Open script"),
|
|
scriptDir,
|
|
self.tr("Processing scripts (*.py *.PY)"),
|
|
)
|
|
|
|
if fileName == "":
|
|
return
|
|
|
|
with OverrideCursor(Qt.CursorShape.WaitCursor):
|
|
self._loadFile(fileName)
|
|
|
|
def save(self):
|
|
self.saveScript(False)
|
|
|
|
def saveAs(self):
|
|
self.saveScript(True)
|
|
|
|
def saveScript(self, saveAs):
|
|
newPath = None
|
|
if not self.code_editor_widget.filePath() or saveAs:
|
|
scriptDir = ScriptUtils.scriptsFolders()[0]
|
|
newPath, _ = QFileDialog.getSaveFileName(
|
|
self,
|
|
self.tr("Save script"),
|
|
scriptDir,
|
|
self.tr("Processing scripts (*.py *.PY)"),
|
|
)
|
|
|
|
if newPath:
|
|
newPath = QgsFileUtils.ensureFileNameHasExtension(newPath, ["py"])
|
|
self.code_editor_widget.save(newPath)
|
|
elif self.code_editor_widget.filePath():
|
|
self.code_editor_widget.save()
|
|
|
|
self.setHasChanged(False)
|
|
QgsApplication.processingRegistry().providerById("script").refreshAlgorithms()
|
|
|
|
def _on_text_modified(self, modified):
|
|
self.setHasChanged(modified)
|
|
|
|
def setHasChanged(self, hasChanged):
|
|
self.hasChanged = hasChanged
|
|
self.actionSaveScript.setEnabled(hasChanged)
|
|
self.update_dialog_title()
|
|
|
|
def runAlgorithm(self):
|
|
if self.run_dialog and not sip.isdeleted(self.run_dialog):
|
|
self.run_dialog.close()
|
|
self.run_dialog = None
|
|
|
|
_locals = {}
|
|
try:
|
|
exec(self.editor.text(), _locals)
|
|
except Exception as e:
|
|
error = QgsError(traceback.format_exc(), "Processing")
|
|
QgsErrorDialog.show(error, self.tr("Execution error"))
|
|
return
|
|
|
|
alg = None
|
|
try:
|
|
alg = algfactory.instances.pop().createInstance()
|
|
except IndexError:
|
|
for name, attr in _locals.items():
|
|
if (
|
|
inspect.isclass(attr)
|
|
and issubclass(
|
|
attr,
|
|
(QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm),
|
|
)
|
|
and attr.__name__
|
|
not in (
|
|
"QgsProcessingAlgorithm",
|
|
"QgsProcessingFeatureBasedAlgorithm",
|
|
)
|
|
):
|
|
alg = attr()
|
|
break
|
|
|
|
if alg is None:
|
|
QMessageBox.warning(
|
|
self,
|
|
self.tr("No script found"),
|
|
self.tr("Seems there is no valid script in the file."),
|
|
)
|
|
return
|
|
|
|
alg.setProvider(QgsApplication.processingRegistry().providerById("script"))
|
|
alg.initAlgorithm()
|
|
|
|
self.run_dialog = alg.createCustomParametersWidget(self)
|
|
if not self.run_dialog:
|
|
self.run_dialog = AlgorithmDialog(alg, parent=self)
|
|
|
|
canvas = iface.mapCanvas()
|
|
prevMapTool = canvas.mapTool()
|
|
|
|
self.run_dialog.show()
|
|
|
|
if canvas.mapTool() != prevMapTool:
|
|
try:
|
|
canvas.mapTool().reset()
|
|
except:
|
|
pass
|
|
canvas.setMapTool(prevMapTool)
|
|
|
|
def _loadFile(self, filePath):
|
|
|
|
self.code_editor_widget.loadFile(filePath)
|
|
self.hasChanged = False
|
|
|
|
self.update_dialog_title()
|