ModelerParametersDialog.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. """
  2. ***************************************************************************
  3. ModelerParametersDialog.py
  4. ---------------------
  5. Date : August 2012
  6. Copyright : (C) 2012 by Victor Olaya
  7. Email : volayaf 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__ = 'Victor Olaya'
  18. __date__ = 'August 2012'
  19. __copyright__ = '(C) 2012, Victor Olaya'
  20. import webbrowser
  21. from qgis.PyQt.QtCore import Qt
  22. from qgis.PyQt.QtWidgets import (QDialog, QDialogButtonBox, QLabel, QLineEdit,
  23. QFrame, QPushButton, QSizePolicy, QVBoxLayout,
  24. QHBoxLayout, QWidget, QTabWidget, QTextEdit)
  25. from qgis.PyQt.QtGui import QColor
  26. from qgis.core import (Qgis,
  27. QgsProject,
  28. QgsProcessingParameterDefinition,
  29. QgsProcessingModelOutput,
  30. QgsProcessingModelChildAlgorithm,
  31. QgsProcessingModelChildParameterSource,
  32. QgsProcessingOutputDefinition)
  33. from qgis.gui import (QgsGui,
  34. QgsMessageBar,
  35. QgsScrollArea,
  36. QgsFilterLineEdit,
  37. QgsHelp,
  38. QgsProcessingContextGenerator,
  39. QgsProcessingModelerParameterWidget,
  40. QgsProcessingParameterWidgetContext,
  41. QgsPanelWidget,
  42. QgsPanelWidgetStack,
  43. QgsColorButton,
  44. QgsModelChildDependenciesWidget)
  45. from qgis.utils import iface
  46. from processing.gui.wrappers import WidgetWrapperFactory
  47. from processing.gui.wrappers import InvalidParameterValue
  48. from processing.tools.dataobjects import createContext
  49. from processing.gui.wrappers import WidgetWrapper
  50. class ModelerParametersDialog(QDialog):
  51. def __init__(self, alg, model, algName=None, configuration=None):
  52. super().__init__()
  53. self.setObjectName('ModelerParametersDialog')
  54. self.setModal(True)
  55. if iface is not None:
  56. self.setStyleSheet(iface.mainWindow().styleSheet())
  57. # dammit this is SUCH as mess... stupid stable API
  58. self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm
  59. self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm
  60. self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time
  61. self.configuration = configuration
  62. self.context = createContext()
  63. self.setWindowTitle(self._alg.displayName())
  64. self.widget = ModelerParametersWidget(alg, model, algName, configuration, context=self.context, dialog=self)
  65. QgsGui.enableAutoGeometryRestore(self)
  66. self.buttonBox = QDialogButtonBox()
  67. self.buttonBox.setOrientation(Qt.Horizontal)
  68. self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Help)
  69. self.buttonBox.accepted.connect(self.okPressed)
  70. self.buttonBox.rejected.connect(self.reject)
  71. self.buttonBox.helpRequested.connect(self.openHelp)
  72. mainLayout = QVBoxLayout()
  73. mainLayout.addWidget(self.widget, 1)
  74. mainLayout.addWidget(self.buttonBox)
  75. self.setLayout(mainLayout)
  76. def algorithm(self):
  77. return self._alg
  78. def setComments(self, text):
  79. self.widget.setComments(text)
  80. def comments(self):
  81. return self.widget.comments()
  82. def setCommentColor(self, color):
  83. self.widget.setCommentColor(color)
  84. def commentColor(self):
  85. return self.widget.commentColor()
  86. def switchToCommentTab(self):
  87. self.widget.switchToCommentTab()
  88. def getAvailableValuesOfType(self, paramType, outTypes=[], dataTypes=[]):
  89. # upgrade paramType to list
  90. if paramType is None:
  91. paramType = []
  92. elif not isinstance(paramType, (tuple, list)):
  93. paramType = [paramType]
  94. if outTypes is None:
  95. outTypes = []
  96. elif not isinstance(outTypes, (tuple, list)):
  97. outTypes = [outTypes]
  98. return self.model.availableSourcesForChild(self.childId, [p.typeName() for p in paramType if
  99. issubclass(p, QgsProcessingParameterDefinition)],
  100. [o.typeName() for o in outTypes if
  101. issubclass(o, QgsProcessingOutputDefinition)], dataTypes)
  102. def resolveValueDescription(self, value):
  103. if isinstance(value, QgsProcessingModelChildParameterSource):
  104. if value.source() == Qgis.ProcessingModelChildParameterSource.StaticValue:
  105. return value.staticValue()
  106. elif value.source() == Qgis.ProcessingModelChildParameterSource.ModelParameter:
  107. return self.model.parameterDefinition(value.parameterName()).description()
  108. elif value.source() == Qgis.ProcessingModelChildParameterSource.ChildOutput:
  109. alg = self.model.childAlgorithm(value.outputChildId())
  110. output_name = alg.algorithm().outputDefinition(value.outputName()).description()
  111. # see if this output has been named by the model designer -- if so, we use that friendly name
  112. for name, output in alg.modelOutputs().items():
  113. if output.childOutputName() == value.outputName():
  114. output_name = name
  115. break
  116. return self.tr("'{0}' from algorithm '{1}'").format(output_name, alg.description())
  117. return value
  118. def setPreviousValues(self):
  119. self.widget.setPreviousValues()
  120. def createAlgorithm(self):
  121. return self.widget.createAlgorithm()
  122. def okPressed(self):
  123. if self.createAlgorithm() is not None:
  124. self.accept()
  125. def openHelp(self):
  126. algHelp = self.widget.algorithm().helpUrl()
  127. if not algHelp:
  128. algHelp = QgsHelp.helpUrl("processing_algs/{}/{}.html#{}".format(
  129. self.widget.algorithm().provider().helpId(), self.algorithm().groupId(),
  130. f"{self.algorithm().provider().helpId()}{self.algorithm().name()}")).toString()
  131. if algHelp not in [None, ""]:
  132. webbrowser.open(algHelp)
  133. class ModelerParametersPanelWidget(QgsPanelWidget):
  134. def __init__(self, alg, model, algName=None, configuration=None, dialog=None, context=None):
  135. super().__init__()
  136. self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm
  137. self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm
  138. self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time
  139. self.configuration = configuration
  140. self.context = context
  141. self.dialog = dialog
  142. self.widget_labels = {}
  143. self.previous_output_definitions = {}
  144. class ContextGenerator(QgsProcessingContextGenerator):
  145. def __init__(self, context):
  146. super().__init__()
  147. self.processing_context = context
  148. def processingContext(self):
  149. return self.processing_context
  150. self.context_generator = ContextGenerator(self.context)
  151. self.setupUi()
  152. self.params = None
  153. def algorithm(self):
  154. return self._alg
  155. def setupUi(self):
  156. self.showAdvanced = False
  157. self.wrappers = {}
  158. self.algorithmItem = None
  159. self.mainLayout = QVBoxLayout()
  160. self.mainLayout.setContentsMargins(0, 0, 0, 0)
  161. self.verticalLayout = QVBoxLayout()
  162. self.bar = QgsMessageBar()
  163. self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
  164. self.verticalLayout.addWidget(self.bar)
  165. hLayout = QHBoxLayout()
  166. hLayout.setContentsMargins(0, 0, 0, 0)
  167. descriptionLabel = QLabel(self.tr("Description"))
  168. self.descriptionBox = QLineEdit()
  169. self.descriptionBox.setText(self._alg.displayName())
  170. hLayout.addWidget(descriptionLabel)
  171. hLayout.addWidget(self.descriptionBox)
  172. self.verticalLayout.addLayout(hLayout)
  173. line = QFrame()
  174. line.setFrameShape(QFrame.HLine)
  175. line.setFrameShadow(QFrame.Sunken)
  176. self.verticalLayout.addWidget(line)
  177. widget_context = QgsProcessingParameterWidgetContext()
  178. widget_context.setProject(QgsProject.instance())
  179. if iface is not None:
  180. widget_context.setMapCanvas(iface.mapCanvas())
  181. widget_context.setActiveLayer(iface.activeLayer())
  182. widget_context.setModel(self.model)
  183. widget_context.setModelChildAlgorithmId(self.childId)
  184. self.algorithmItem = QgsGui.instance().processingGuiRegistry().algorithmConfigurationWidget(self._alg)
  185. if self.algorithmItem:
  186. self.algorithmItem.setWidgetContext(widget_context)
  187. self.algorithmItem.registerProcessingContextGenerator(self.context_generator)
  188. if self.configuration:
  189. self.algorithmItem.setConfiguration(self.configuration)
  190. self.verticalLayout.addWidget(self.algorithmItem)
  191. for param in self._alg.parameterDefinitions():
  192. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced:
  193. self.advancedButton = QPushButton()
  194. self.advancedButton.setText(self.tr('Show advanced parameters'))
  195. self.advancedButton.clicked.connect(
  196. self.showAdvancedParametersClicked)
  197. advancedButtonHLayout = QHBoxLayout()
  198. advancedButtonHLayout.addWidget(self.advancedButton)
  199. advancedButtonHLayout.addStretch()
  200. self.verticalLayout.addLayout(advancedButtonHLayout)
  201. break
  202. for param in self._alg.parameterDefinitions():
  203. if param.isDestination() or param.flags() & QgsProcessingParameterDefinition.FlagHidden:
  204. continue
  205. wrapper = WidgetWrapperFactory.create_wrapper(param, self.dialog)
  206. self.wrappers[param.name()] = wrapper
  207. wrapper.setWidgetContext(widget_context)
  208. wrapper.registerProcessingContextGenerator(self.context_generator)
  209. if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
  210. widget = wrapper
  211. else:
  212. widget = wrapper.widget
  213. if widget is not None:
  214. if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
  215. label = wrapper.createLabel()
  216. else:
  217. tooltip = param.description()
  218. widget.setToolTip(tooltip)
  219. label = wrapper.label
  220. self.widget_labels[param.name()] = label
  221. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced:
  222. label.setVisible(self.showAdvanced)
  223. widget.setVisible(self.showAdvanced)
  224. self.verticalLayout.addWidget(label)
  225. self.verticalLayout.addWidget(widget)
  226. for output in self._alg.destinationParameterDefinitions():
  227. if output.flags() & QgsProcessingParameterDefinition.FlagHidden:
  228. continue
  229. widget = QgsGui.processingGuiRegistry().createModelerParameterWidget(self.model,
  230. self.childId,
  231. output,
  232. self.context)
  233. widget.setDialog(self.dialog)
  234. widget.setWidgetContext(widget_context)
  235. widget.registerProcessingContextGenerator(self.context_generator)
  236. self.wrappers[output.name()] = widget
  237. item = QgsFilterLineEdit()
  238. if hasattr(item, 'setPlaceholderText'):
  239. item.setPlaceholderText(self.tr('[Enter name if this is a final result]'))
  240. label = widget.createLabel()
  241. if label is not None:
  242. self.verticalLayout.addWidget(label)
  243. self.verticalLayout.addWidget(widget)
  244. label = QLabel(' ')
  245. self.verticalLayout.addWidget(label)
  246. label = QLabel(self.tr('Dependencies'))
  247. self.dependencies_panel = QgsModelChildDependenciesWidget(self, self.model, self.childId)
  248. self.verticalLayout.addWidget(label)
  249. self.verticalLayout.addWidget(self.dependencies_panel)
  250. self.verticalLayout.addStretch(1000)
  251. self.setPreviousValues()
  252. self.verticalLayout2 = QVBoxLayout()
  253. self.verticalLayout2.setSpacing(2)
  254. self.verticalLayout2.setMargin(0)
  255. self.paramPanel = QWidget()
  256. self.paramPanel.setLayout(self.verticalLayout)
  257. self.scrollArea = QgsScrollArea()
  258. self.scrollArea.setWidget(self.paramPanel)
  259. self.scrollArea.setWidgetResizable(True)
  260. self.scrollArea.setFrameStyle(QFrame.NoFrame)
  261. self.verticalLayout2.addWidget(self.scrollArea)
  262. w = QWidget()
  263. w.setLayout(self.verticalLayout2)
  264. self.mainLayout.addWidget(w)
  265. self.setLayout(self.mainLayout)
  266. def showAdvancedParametersClicked(self):
  267. self.showAdvanced = not self.showAdvanced
  268. if self.showAdvanced:
  269. self.advancedButton.setText(self.tr('Hide advanced parameters'))
  270. else:
  271. self.advancedButton.setText(self.tr('Show advanced parameters'))
  272. for param in self._alg.parameterDefinitions():
  273. if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced:
  274. wrapper = self.wrappers[param.name()]
  275. if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
  276. wrapper.setVisible(self.showAdvanced)
  277. else:
  278. wrapper.widget.setVisible(self.showAdvanced)
  279. self.widget_labels[param.name()].setVisible(self.showAdvanced)
  280. def setPreviousValues(self):
  281. if self.childId is not None:
  282. alg = self.model.childAlgorithm(self.childId)
  283. self.descriptionBox.setText(alg.description())
  284. for param in alg.algorithm().parameterDefinitions():
  285. if param.isDestination() or param.flags() & QgsProcessingParameterDefinition.FlagHidden:
  286. continue
  287. value = None
  288. if param.name() in alg.parameterSources():
  289. value = alg.parameterSources()[param.name()]
  290. if isinstance(value, list) and len(value) == 1:
  291. value = value[0]
  292. elif isinstance(value, list) and len(value) == 0:
  293. value = None
  294. wrapper = self.wrappers[param.name()]
  295. if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
  296. if value is None:
  297. value = QgsProcessingModelChildParameterSource.fromStaticValue(param.defaultValue())
  298. wrapper.setWidgetValue(value)
  299. else:
  300. if value is None:
  301. value = param.defaultValue()
  302. if isinstance(value,
  303. QgsProcessingModelChildParameterSource) and value.source() == Qgis.ProcessingModelChildParameterSource.StaticValue:
  304. value = value.staticValue()
  305. wrapper.setValue(value)
  306. for output in self.algorithm().destinationParameterDefinitions():
  307. if output.flags() & QgsProcessingParameterDefinition.FlagHidden:
  308. continue
  309. model_output_name = None
  310. for name, out in alg.modelOutputs().items():
  311. if out.childId() == self.childId and out.childOutputName() == output.name():
  312. # this destination parameter is linked to a model output
  313. model_output_name = out.name()
  314. self.previous_output_definitions[output.name()] = out
  315. break
  316. value = None
  317. if model_output_name is None and output.name() in alg.parameterSources():
  318. value = alg.parameterSources()[output.name()]
  319. if isinstance(value, list) and len(value) == 1:
  320. value = value[0]
  321. elif isinstance(value, list) and len(value) == 0:
  322. value = None
  323. wrapper = self.wrappers[output.name()]
  324. if model_output_name is not None:
  325. wrapper.setToModelOutput(model_output_name)
  326. elif value is not None or output.defaultValue() is not None:
  327. if value is None:
  328. value = QgsProcessingModelChildParameterSource.fromStaticValue(output.defaultValue())
  329. wrapper.setWidgetValue(value)
  330. self.dependencies_panel.setValue(alg.dependencies())
  331. def createAlgorithm(self):
  332. alg = QgsProcessingModelChildAlgorithm(self._alg.id())
  333. if not self.childId:
  334. alg.generateChildId(self.model)
  335. else:
  336. alg.setChildId(self.childId)
  337. alg.setDescription(self.descriptionBox.text())
  338. if self.algorithmItem:
  339. alg.setConfiguration(self.algorithmItem.configuration())
  340. self._alg = alg.algorithm().create(self.algorithmItem.configuration())
  341. for param in self._alg.parameterDefinitions():
  342. if param.isDestination() or param.flags() & QgsProcessingParameterDefinition.FlagHidden:
  343. continue
  344. try:
  345. wrapper = self.wrappers[param.name()]
  346. if issubclass(wrapper.__class__, WidgetWrapper):
  347. val = wrapper.value()
  348. elif issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget):
  349. val = wrapper.value()
  350. else:
  351. val = wrapper.parameterValue()
  352. except InvalidParameterValue:
  353. val = None
  354. if isinstance(val, QgsProcessingModelChildParameterSource):
  355. val = [val]
  356. elif not (isinstance(val, list) and all(
  357. [isinstance(subval, QgsProcessingModelChildParameterSource) for subval in val])):
  358. val = [QgsProcessingModelChildParameterSource.fromStaticValue(val)]
  359. valid = True
  360. for subval in val:
  361. if (isinstance(subval, QgsProcessingModelChildParameterSource)
  362. and subval.source() == Qgis.ProcessingModelChildParameterSource.StaticValue
  363. and not param.checkValueIsAcceptable(subval.staticValue())) \
  364. or (subval is None and not param.flags() & QgsProcessingParameterDefinition.FlagOptional):
  365. valid = False
  366. break
  367. if valid:
  368. alg.addParameterSources(param.name(), val)
  369. outputs = {}
  370. for output in self._alg.destinationParameterDefinitions():
  371. if not output.flags() & QgsProcessingParameterDefinition.FlagHidden:
  372. wrapper = self.wrappers[output.name()]
  373. if wrapper.isModelOutput():
  374. name = wrapper.modelOutputName()
  375. if name:
  376. # if there was a previous output definition already for this output, we start with it,
  377. # otherwise we'll lose any existing output comments, coloring, position, etc
  378. model_output = self.previous_output_definitions.get(output.name(), QgsProcessingModelOutput(name, name))
  379. model_output.setDescription(name)
  380. model_output.setChildId(alg.childId())
  381. model_output.setChildOutputName(output.name())
  382. outputs[name] = model_output
  383. else:
  384. val = wrapper.value()
  385. if isinstance(val, QgsProcessingModelChildParameterSource):
  386. val = [val]
  387. alg.addParameterSources(output.name(), val)
  388. if output.flags() & QgsProcessingParameterDefinition.FlagIsModelOutput:
  389. if output.name() not in outputs:
  390. model_output = QgsProcessingModelOutput(output.name(), output.name())
  391. model_output.setChildId(alg.childId())
  392. model_output.setChildOutputName(output.name())
  393. outputs[output.name()] = model_output
  394. alg.setModelOutputs(outputs)
  395. alg.setDependencies(self.dependencies_panel.value())
  396. return alg
  397. class ModelerParametersWidget(QWidget):
  398. def __init__(self, alg, model, algName=None, configuration=None, dialog=None, context=None):
  399. super().__init__()
  400. self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm
  401. self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm
  402. self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time
  403. self.configuration = configuration
  404. self.context = context
  405. self.dialog = dialog
  406. self.widget = ModelerParametersPanelWidget(alg, model, algName, configuration, dialog, context)
  407. class ContextGenerator(QgsProcessingContextGenerator):
  408. def __init__(self, context):
  409. super().__init__()
  410. self.processing_context = context
  411. def processingContext(self):
  412. return self.processing_context
  413. self.context_generator = ContextGenerator(self.context)
  414. self.setupUi()
  415. self.params = None
  416. def algorithm(self):
  417. return self._alg
  418. def switchToCommentTab(self):
  419. self.tab.setCurrentIndex(1)
  420. self.commentEdit.setFocus()
  421. self.commentEdit.selectAll()
  422. def setupUi(self):
  423. self.mainLayout = QVBoxLayout()
  424. self.mainLayout.setContentsMargins(0, 0, 0, 0)
  425. self.tab = QTabWidget()
  426. self.mainLayout.addWidget(self.tab)
  427. self.param_widget = QgsPanelWidgetStack()
  428. self.widget.setDockMode(True)
  429. self.param_widget.setMainPanel(self.widget)
  430. self.tab.addTab(self.param_widget, self.tr('Properties'))
  431. self.commentLayout = QVBoxLayout()
  432. self.commentEdit = QTextEdit()
  433. self.commentEdit.setAcceptRichText(False)
  434. self.commentLayout.addWidget(self.commentEdit, 1)
  435. hl = QHBoxLayout()
  436. hl.setContentsMargins(0, 0, 0, 0)
  437. hl.addWidget(QLabel(self.tr('Color')))
  438. self.comment_color_button = QgsColorButton()
  439. self.comment_color_button.setAllowOpacity(True)
  440. self.comment_color_button.setWindowTitle(self.tr('Comment Color'))
  441. self.comment_color_button.setShowNull(True, self.tr('Default'))
  442. hl.addWidget(self.comment_color_button)
  443. self.commentLayout.addLayout(hl)
  444. w2 = QWidget()
  445. w2.setLayout(self.commentLayout)
  446. self.tab.addTab(w2, self.tr('Comments'))
  447. self.setLayout(self.mainLayout)
  448. def setComments(self, text):
  449. self.commentEdit.setPlainText(text)
  450. def comments(self):
  451. return self.commentEdit.toPlainText()
  452. def setCommentColor(self, color):
  453. if color.isValid():
  454. self.comment_color_button.setColor(color)
  455. else:
  456. self.comment_color_button.setToNull()
  457. def commentColor(self):
  458. return self.comment_color_button.color() if not self.comment_color_button.isNull() else QColor()
  459. def setPreviousValues(self):
  460. self.widget.setPreviousValues()
  461. def createAlgorithm(self):
  462. alg = self.widget.createAlgorithm()
  463. if alg:
  464. alg.comment().setDescription(self.comments())
  465. alg.comment().setColor(self.commentColor())
  466. return alg