ScriptEditorDialog.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. """
  2. ***************************************************************************
  3. EditScriptDialog.py
  4. ---------------------
  5. Date : December 2012
  6. Copyright : (C) 2012 by Alexander Bruy
  7. Email : alexander dot bruy at gmail dot com
  8. ***************************************************************************
  9. * *
  10. * This program is free software; you can redistribute it and/or modify *
  11. * it under the terms of the GNU General Public License as published by *
  12. * the Free Software Foundation; either version 2 of the License, or *
  13. * (at your option) any later version. *
  14. * *
  15. ***************************************************************************
  16. """
  17. __author__ = 'Alexander Bruy'
  18. __date__ = 'December 2012'
  19. __copyright__ = '(C) 2012, Alexander Bruy'
  20. import os
  21. import codecs
  22. import inspect
  23. import traceback
  24. import warnings
  25. from qgis.PyQt import uic, sip
  26. from qgis.PyQt.QtCore import Qt
  27. from qgis.PyQt.QtGui import QCursor
  28. from qgis.PyQt.QtWidgets import (
  29. QMessageBox,
  30. QFileDialog,
  31. QVBoxLayout
  32. )
  33. from qgis.gui import QgsGui, QgsErrorDialog
  34. from qgis.core import (QgsApplication,
  35. QgsSettings,
  36. QgsError,
  37. QgsProcessingAlgorithm,
  38. QgsProcessingFeatureBasedAlgorithm)
  39. from qgis.utils import iface, OverrideCursor
  40. from qgis.processing import alg as algfactory
  41. from processing.gui.AlgorithmDialog import AlgorithmDialog
  42. from processing.script import ScriptUtils
  43. from .ScriptEdit import ScriptEdit
  44. pluginPath = os.path.split(os.path.dirname(__file__))[0]
  45. with warnings.catch_warnings():
  46. warnings.filterwarnings("ignore", category=DeprecationWarning)
  47. WIDGET, BASE = uic.loadUiType(
  48. os.path.join(pluginPath, "ui", "DlgScriptEditor.ui"))
  49. class ScriptEditorDialog(BASE, WIDGET):
  50. hasChanged = False
  51. DIALOG_STORE = []
  52. def __init__(self, filePath=None, parent=None):
  53. super().__init__(parent)
  54. # SIP is totally messed up here -- the dialog wrapper or something
  55. # is always prematurely cleaned which results in broken QObject
  56. # connections throughout.
  57. # Hack around this by storing dialog instances in a global list to
  58. # prevent too early wrapper garbage collection
  59. ScriptEditorDialog.DIALOG_STORE.append(self)
  60. def clean_up_store():
  61. ScriptEditorDialog.DIALOG_STORE =\
  62. [d for d in ScriptEditorDialog.DIALOG_STORE if d != self]
  63. self.destroyed.connect(clean_up_store)
  64. self.setupUi(self)
  65. self.setAttribute(Qt.WA_DeleteOnClose)
  66. QgsGui.instance().enableAutoGeometryRestore(self)
  67. vl = QVBoxLayout()
  68. vl.setContentsMargins(0, 0, 0, 0)
  69. self.editor_container.setLayout(vl)
  70. self.editor = ScriptEdit()
  71. vl.addWidget(self.editor)
  72. self.searchWidget.setVisible(False)
  73. if iface is not None:
  74. self.toolBar.setIconSize(iface.iconSize())
  75. self.setStyleSheet(iface.mainWindow().styleSheet())
  76. self.actionOpenScript.setIcon(
  77. QgsApplication.getThemeIcon('/mActionScriptOpen.svg'))
  78. self.actionSaveScript.setIcon(
  79. QgsApplication.getThemeIcon('/mActionFileSave.svg'))
  80. self.actionSaveScriptAs.setIcon(
  81. QgsApplication.getThemeIcon('/mActionFileSaveAs.svg'))
  82. self.actionRunScript.setIcon(
  83. QgsApplication.getThemeIcon('/mActionStart.svg'))
  84. self.actionCut.setIcon(
  85. QgsApplication.getThemeIcon('/mActionEditCut.svg'))
  86. self.actionCopy.setIcon(
  87. QgsApplication.getThemeIcon('/mActionEditCopy.svg'))
  88. self.actionPaste.setIcon(
  89. QgsApplication.getThemeIcon('/mActionEditPaste.svg'))
  90. self.actionUndo.setIcon(
  91. QgsApplication.getThemeIcon('/mActionUndo.svg'))
  92. self.actionRedo.setIcon(
  93. QgsApplication.getThemeIcon('/mActionRedo.svg'))
  94. self.actionFindReplace.setIcon(
  95. QgsApplication.getThemeIcon('/mActionFindReplace.svg'))
  96. self.actionIncreaseFontSize.setIcon(
  97. QgsApplication.getThemeIcon('/mActionIncreaseFont.svg'))
  98. self.actionDecreaseFontSize.setIcon(
  99. QgsApplication.getThemeIcon('/mActionDecreaseFont.svg'))
  100. self.actionToggleComment.setIcon(
  101. QgsApplication.getThemeIcon('console/iconCommentEditorConsole.svg'))
  102. # Connect signals and slots
  103. self.actionOpenScript.triggered.connect(self.openScript)
  104. self.actionSaveScript.triggered.connect(self.save)
  105. self.actionSaveScriptAs.triggered.connect(self.saveAs)
  106. self.actionRunScript.triggered.connect(self.runAlgorithm)
  107. self.actionCut.triggered.connect(self.editor.cut)
  108. self.actionCopy.triggered.connect(self.editor.copy)
  109. self.actionPaste.triggered.connect(self.editor.paste)
  110. self.actionUndo.triggered.connect(self.editor.undo)
  111. self.actionRedo.triggered.connect(self.editor.redo)
  112. self.actionFindReplace.toggled.connect(self.toggleSearchBox)
  113. self.actionIncreaseFontSize.triggered.connect(self.editor.zoomIn)
  114. self.actionDecreaseFontSize.triggered.connect(self.editor.zoomOut)
  115. self.actionToggleComment.triggered.connect(self.editor.toggleComment)
  116. self.editor.textChanged.connect(self._on_text_modified)
  117. self.leFindText.returnPressed.connect(self.find)
  118. self.btnFind.clicked.connect(self.find)
  119. self.btnReplace.clicked.connect(self.replace)
  120. self.lastSearch = None
  121. self.run_dialog = None
  122. self.filePath = None
  123. if filePath is not None:
  124. self._loadFile(filePath)
  125. self.setHasChanged(False)
  126. def update_dialog_title(self):
  127. """
  128. Updates the script editor dialog title
  129. """
  130. if self.filePath:
  131. path, file_name = os.path.split(self.filePath)
  132. else:
  133. file_name = self.tr('Untitled Script')
  134. if self.hasChanged:
  135. file_name = '*' + file_name
  136. self.setWindowTitle(self.tr('{} - Processing Script Editor').format(file_name))
  137. def closeEvent(self, event):
  138. settings = QgsSettings()
  139. settings.setValue("/Processing/stateScriptEditor", self.saveState())
  140. settings.setValue("/Processing/geometryScriptEditor", self.saveGeometry())
  141. if self.hasChanged:
  142. ret = QMessageBox.question(
  143. self, self.tr('Save Script?'),
  144. self.tr('There are unsaved changes in this script. Do you want to keep those?'),
  145. QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard, QMessageBox.Cancel)
  146. if ret == QMessageBox.Save:
  147. self.saveScript(False)
  148. event.accept()
  149. elif ret == QMessageBox.Discard:
  150. event.accept()
  151. else:
  152. event.ignore()
  153. else:
  154. event.accept()
  155. def openScript(self):
  156. if self.hasChanged:
  157. ret = QMessageBox.warning(self,
  158. self.tr("Unsaved changes"),
  159. self.tr("There are unsaved changes in the script. Continue?"),
  160. QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
  161. if ret == QMessageBox.No:
  162. return
  163. scriptDir = ScriptUtils.scriptsFolders()[0]
  164. fileName, _ = QFileDialog.getOpenFileName(self,
  165. self.tr("Open script"),
  166. scriptDir,
  167. self.tr("Processing scripts (*.py *.PY)"))
  168. if fileName == "":
  169. return
  170. with OverrideCursor(Qt.WaitCursor):
  171. self._loadFile(fileName)
  172. def save(self):
  173. self.saveScript(False)
  174. def saveAs(self):
  175. self.saveScript(True)
  176. def saveScript(self, saveAs):
  177. newPath = None
  178. if self.filePath is None or saveAs:
  179. scriptDir = ScriptUtils.scriptsFolders()[0]
  180. newPath, _ = QFileDialog.getSaveFileName(self,
  181. self.tr("Save script"),
  182. scriptDir,
  183. self.tr("Processing scripts (*.py *.PY)"))
  184. if newPath:
  185. if not newPath.lower().endswith(".py"):
  186. newPath += ".py"
  187. self.filePath = newPath
  188. if self.filePath:
  189. text = self.editor.text()
  190. try:
  191. with codecs.open(self.filePath, "w", encoding="utf-8") as f:
  192. f.write(text)
  193. except OSError as e:
  194. QMessageBox.warning(self,
  195. self.tr("I/O error"),
  196. self.tr("Unable to save edits:\n{}").format(str(e))
  197. )
  198. return
  199. self.setHasChanged(False)
  200. QgsApplication.processingRegistry().providerById("script").refreshAlgorithms()
  201. def _on_text_modified(self):
  202. self.setHasChanged(True)
  203. def setHasChanged(self, hasChanged):
  204. self.hasChanged = hasChanged
  205. self.actionSaveScript.setEnabled(hasChanged)
  206. self.update_dialog_title()
  207. def runAlgorithm(self):
  208. if self.run_dialog and not sip.isdeleted(self.run_dialog):
  209. self.run_dialog.close()
  210. self.run_dialog = None
  211. _locals = {}
  212. try:
  213. exec(self.editor.text(), _locals)
  214. except Exception as e:
  215. error = QgsError(traceback.format_exc(), "Processing")
  216. QgsErrorDialog.show(error,
  217. self.tr("Execution error")
  218. )
  219. return
  220. alg = None
  221. try:
  222. alg = algfactory.instances.pop().createInstance()
  223. except IndexError:
  224. for name, attr in _locals.items():
  225. if inspect.isclass(attr) and issubclass(attr, (QgsProcessingAlgorithm, QgsProcessingFeatureBasedAlgorithm)) and attr.__name__ not in ("QgsProcessingAlgorithm", "QgsProcessingFeatureBasedAlgorithm"):
  226. alg = attr()
  227. break
  228. if alg is None:
  229. QMessageBox.warning(self,
  230. self.tr("No script found"),
  231. self.tr("Seems there is no valid script in the file.")
  232. )
  233. return
  234. alg.setProvider(QgsApplication.processingRegistry().providerById("script"))
  235. alg.initAlgorithm()
  236. self.run_dialog = alg.createCustomParametersWidget(self)
  237. if not self.run_dialog:
  238. self.run_dialog = AlgorithmDialog(alg, parent=self)
  239. canvas = iface.mapCanvas()
  240. prevMapTool = canvas.mapTool()
  241. self.run_dialog.show()
  242. if canvas.mapTool() != prevMapTool:
  243. try:
  244. canvas.mapTool().reset()
  245. except:
  246. pass
  247. canvas.setMapTool(prevMapTool)
  248. def find(self):
  249. textToFind = self.leFindText.text()
  250. caseSensitive = self.chkCaseSensitive.isChecked()
  251. wholeWord = self.chkWholeWord.isChecked()
  252. if self.lastSearch is None or textToFind != self.lastSearch:
  253. self.editor.findFirst(textToFind, False, caseSensitive, wholeWord, True)
  254. else:
  255. self.editor.findNext()
  256. def replace(self):
  257. textToReplace = self.leReplaceText.text()
  258. self.editor.replaceSelectedText(textToReplace)
  259. def toggleSearchBox(self, checked):
  260. self.searchWidget.setVisible(checked)
  261. if (checked):
  262. self.leFindText.setFocus()
  263. def _loadFile(self, filePath):
  264. with codecs.open(filePath, "r", encoding="utf-8") as f:
  265. txt = f.read()
  266. self.editor.setText(txt)
  267. self.hasChanged = False
  268. self.editor.setModified(False)
  269. self.editor.recolor()
  270. self.filePath = filePath
  271. self.update_dialog_title()