Skip to content

Commit

Permalink
[processing] Restore spin box widget for number inputs outside modeller
Browse files Browse the repository at this point in the history
This commit restores some pre 3.0 processing behaviour for number inputs.
Now, if a number input is required outside of modeller than a spin box
will be shown instead of a free text input.

Clicking the expression builder button results in an expression
which is evaluated immediately to avoid users expecting that
the expression will be evaluated per feature.
  • Loading branch information
nyalldawson committed Nov 11, 2016
1 parent 10648df commit a3ae0b2
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 61 deletions.
214 changes: 156 additions & 58 deletions python/plugins/processing/gui/NumberInputPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
__revision__ = '$Format:%H$'

import os
import math

from qgis.PyQt import uic
from qgis.PyQt.QtCore import pyqtSignal
Expand All @@ -45,16 +46,25 @@
from processing.modeler.ModelerAlgorithm import ValueFromInput, ValueFromOutput, CompoundValue

pluginPath = os.path.split(os.path.dirname(__file__))[0]
NUMBER_WIDGET, NUMBER_BASE = uic.loadUiType(
os.path.join(pluginPath, 'ui', 'widgetNumberSelector.ui'))
WIDGET, BASE = uic.loadUiType(
os.path.join(pluginPath, 'ui', 'widgetBaseSelector.ui'))


class NumberInputPanel(BASE, WIDGET):
class ModellerNumberInputPanel(BASE, WIDGET):
"""
Number input panel for use inside the modeller - this input panel
is based off the base input panel and includes a text based line input
for entering values. This allows expressions and other non-numeric
values to be set, which are later evalauted to numbers when the model
is run.
"""

hasChanged = pyqtSignal()

def __init__(self, param, modelParametersDialog=None):
super(NumberInputPanel, self).__init__(None)
def __init__(self, param, modelParametersDialog):
super(ModellerNumberInputPanel, self).__init__(None)
self.setupUi(self)

self.param = param
Expand All @@ -66,69 +76,157 @@ def __init__(self, param, modelParametersDialog=None):

def showExpressionsBuilder(self):
context = self.param.expressionContext()
dlg = QgsExpressionBuilderDialog(None, self.leText.text(), self, 'generic', context)
if self.modelParametersDialog is not None:
context.popScope()
values = self.modelParametersDialog.getAvailableValuesOfType(ParameterNumber, OutputNumber)
variables = {}
for value in values:
if isinstance(value, ValueFromInput):
name = value.name
element = self.modelParametersDialog.model.inputs[name].param
desc = element.description
else:
name = "%s_%s" % (value.alg, value.output)
alg = self.modelParametersDialog.model.algs[value.alg]
out = alg.algorithm.getOutputFromName(value.output)
desc = "Output '%s' from algorithm '%s" % (out.description, alg.description)
variables[name] = desc
values = self.modelParametersDialog.getAvailableValuesOfType(ParameterVector, OutputVector)
values.extend(self.modelParametersDialog.getAvailableValuesOfType(ParameterRaster, OutputRaster))
for value in values:
if isinstance(value, ValueFromInput):
name = value.name
element = self.modelParametersDialog.model.inputs[name].param
desc = element.description
else:
name = "%s_%s" % (value.alg, value.output)
alg = self.modelParametersDialog.model.algs[value.alg]
element = alg.algorithm.getOutputFromName(value.output)
desc = "Output '%s' from algorithm '%s" % (element.description, alg.description)
variables['%s_minx' % name] = "Minimum X of %s" % desc
variables['%s_miny' % name] = "Maximum X of %s" % desc
variables['%s_maxx' % name] = "Minimum Y of %s" % desc
variables['%s_maxy' % name] = "Maximum Y of %s" % desc
if isinstance(element, (ParameterRaster, OutputRaster)):
variables['%s_min' % name] = "Minimum value of %s" % desc
variables['%s_max' % name] = "Maximum value of %s" % desc
variables['%s_avg' % name] = "Mean value of %s" % desc
variables['%s_stddev' % name] = "Standard deviation of %s" % desc
for variable, desc in variables.items():
dlg.expressionBuilder().registerItem("Modeler", variable, "@" + variable, desc, highlightedItem=True)
dlg = QgsExpressionBuilderDialog(None, str(self.leText.text()), self, 'generic', context)

context.popScope()
values = self.modelParametersDialog.getAvailableValuesOfType(ParameterNumber, OutputNumber)
variables = {}
for value in values:
if isinstance(value, ValueFromInput):
name = value.name
element = self.modelParametersDialog.model.inputs[name].param
desc = element.description
else:
name = "%s_%s" % (value.alg, value.output)
alg = self.modelParametersDialog.model.algs[value.alg]
out = alg.algorithm.getOutputFromName(value.output)
desc = "Output '%s' from algorithm '%s" % (out.description, alg.description)
variables[name] = desc
values = self.modelParametersDialog.getAvailableValuesOfType(ParameterVector, OutputVector)
values.extend(self.modelParametersDialog.getAvailableValuesOfType(ParameterRaster, OutputRaster))
for value in values:
if isinstance(value, ValueFromInput):
name = value.name
element = self.modelParametersDialog.model.inputs[name].param
desc = element.description
else:
name = "%s_%s" % (value.alg, value.output)
alg = self.modelParametersDialog.model.algs[value.alg]
element = alg.algorithm.getOutputFromName(value.output)
desc = "Output '%s' from algorithm '%s" % (element.description, alg.description)
variables['%s_minx' % name] = "Minimum X of %s" % desc
variables['%s_miny' % name] = "Maximum X of %s" % desc
variables['%s_maxx' % name] = "Minimum Y of %s" % desc
variables['%s_maxy' % name] = "Maximum Y of %s" % desc
if isinstance(element, (ParameterRaster, OutputRaster)):
variables['%s_min' % name] = "Minimum value of %s" % desc
variables['%s_max' % name] = "Maximum value of %s" % desc
variables['%s_avg' % name] = "Mean value of %s" % desc
variables['%s_stddev' % name] = "Standard deviation of %s" % desc
for variable, desc in variables.items():
dlg.expressionBuilder().registerItem("Modeler", variable, "@" + variable, desc, highlightedItem=True)

dlg.setWindowTitle(self.tr('Expression based input'))
if dlg.exec_() == QDialog.Accepted:
exp = QgsExpression(dlg.expressionText())
if not exp.hasParserError():
self.setValue(dlg.expressionText())

def getValue(self):
if self.modelParametersDialog:
value = self.leText.text()
values = []
for param in self.modelParametersDialog.model.parameters:
if isinstance(param, ParameterNumber):
if "@" + param.name in value:
values.append(ValueFromInput(param.name))
for alg in list(self.modelParametersDialog.model.algs.values()):
for out in alg.algorithm.outputs:
if isinstance(out, OutputNumber) and "@%s_%s" % (alg.name, out.name) in value:
values.append(ValueFromOutput(alg.name, out.name))
if values:
return CompoundValue(values, value)
else:
return value
value = self.leText.text()
values = []
for param in self.modelParametersDialog.model.parameters:
if isinstance(param, ParameterNumber):
if "@" + param.name in value:
values.append(ValueFromInput(param.name))
for alg in list(self.modelParametersDialog.model.algs.values()):
for out in alg.algorithm.outputs:
if isinstance(out, OutputNumber) and "@%s_%s" % (alg.name, out.name) in value:
values.append(ValueFromOutput(alg.name, out.name))
if values:
return CompoundValue(values, value)
else:
return self.leText.text()
return value

def setValue(self, value):
self.leText.setText(str(value))


class NumberInputPanel(NUMBER_BASE, NUMBER_WIDGET):
"""
Number input panel for use outside the modeller - this input panel
contains a user friendly spin box for entering values. It also
allows expressions to be evaluated, but these expressions are evaluated
immediately after entry and are not stored anywhere.
"""

hasChanged = pyqtSignal()

def __init__(self, param):
super(NumberInputPanel, self).__init__(None)
self.setupUi(self)

self.spnValue.setExpressionsEnabled(True)

self.param = param
if self.param.isInteger:
self.spnValue.setDecimals(0)
else:
# Guess reasonable step value
if self.param.max is not None and self.param.min is not None:
try:
self.spnValue.setSingleStep(self.calculateStep(float(self.param.min), float(self.param.max)))
except:
pass

if self.param.max is not None:
self.spnValue.setMaximum(self.param.max)
else:
self.spnValue.setMaximum(999999999)
if self.param.min is not None:
self.spnValue.setMinimum(self.param.min)
else:
self.spnValue.setMinimum(-999999999)

# set default value
if param.default is not None:
self.setValue(param.default)
try:
self.spnValue.setClearValue(float(param.default))
except:
pass
elif self.param.min is not None:
try:
self.setValue(float(self.param.min))
self.spnValue.setClearValue(float(self.param.min))
except:
pass
else:
self.setValue(0)
self.spnValue.setClearValue(0)
self.btnSelect.setFixedHeight(self.spnValue.height())

self.btnSelect.clicked.connect(self.showExpressionsBuilder)
self.spnValue.valueChanged.connect(lambda: self.hasChanged.emit())

def showExpressionsBuilder(self):
context = self.param.expressionContext()
dlg = QgsExpressionBuilderDialog(None, str(self.spnValue.value()), self, 'generic', context)

dlg.setWindowTitle(self.tr('Expression based input'))
if dlg.exec_() == QDialog.Accepted:
exp = QgsExpression(dlg.expressionText())
if not exp.hasParserError():
try:
val = float(exp.evaluate(context))
self.setValue(val)
except:
return

def getValue(self):
return self.spnValue.value()

def setValue(self, value):
try:
self.spnValue.setValue(float(value))
except:
return

def calculateStep(self, minimum, maximum):
value_range = maximum - minimum
if value_range <= 1.0:
step = value_range / 10.0
# round to 1 significant figrue
return round(step, -int(math.floor(math.log10(step))))
else:
return 1.0
6 changes: 3 additions & 3 deletions python/plugins/processing/gui/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from qgis.PyQt.QtWidgets import QCheckBox, QComboBox, QLineEdit, QPlainTextEdit
from qgis.PyQt.QtCore import pyqtSignal, QObject, QVariant

from processing.gui.NumberInputPanel import NumberInputPanel
from processing.gui.NumberInputPanel import NumberInputPanel, ModellerNumberInputPanel
from processing.gui.InputLayerSelectorPanel import InputLayerSelectorPanel
from processing.modeler.MultilineTextPanel import MultilineTextPanel
from processing.gui.CrsSelectionPanel import CrsSelectionPanel
Expand Down Expand Up @@ -466,9 +466,9 @@ class NumberWidgetWrapper(WidgetWrapper):

def createWidget(self):
if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH):
return NumberInputPanel(self.param, None)
return NumberInputPanel(self.param)
else:
return NumberInputPanel(self.param, self.dialog)
return ModellerNumberInputPanel(self.param, self.dialog)

def setValue(self, value):
self.widget.setValue(value)
Expand Down
76 changes: 76 additions & 0 deletions python/plugins/processing/ui/widgetNumberSelector.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>249</width>
<height>37</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>2</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QgsDoubleSpinBox" name="spnValue">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="decimals">
<number>6</number>
</property>
<property name="minimum">
<double>-9999999999999.000000000000000</double>
</property>
<property name="maximum">
<double>9999999999999.000000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="btnSelect">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>qgis.gui</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

0 comments on commit a3ae0b2

Please sign in to comment.