QmlDesigner: Fix issues with dynamic vectors

The conversion from Qt.vectorxd was not strict enough
and expressions like Qt.vector2d(7 / Constants.width, 7 / Constants.height)
were read as [7, 7]. Reflection was not handled properly and the [7,7]
was written on selection.

Fixes:

* Avoid reflection
* Improve Qt.vector parsing; Move code to C++
* Handle "real" expression case and show binding indicator
* Fix setting an expression. We allow the binding editor/expression
  indicator only on the first component of the vector

Task-number: QDS-12242
Change-Id: I6707bdac860852d96d416adbe911d43fe9128e8a
Reviewed-by: Henning Gründl <henning.gruendl@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
Reviewed-by: Qt CI Patch Build Bot <ci_patchbuild_bot@qt.io>
This commit is contained in:
Thomas Hartmann
2024-03-14 20:55:51 +01:00
parent 4fcff3adca
commit d72f55bbd0
4 changed files with 107 additions and 36 deletions

View File

@@ -291,53 +291,64 @@ Section {
property int vecSize: 0
property var proxyValues: []
property var spinBoxes: [boxX, boxY, boxZ, boxW]
property bool block: false
signal remove
onVecSizeChanged: updateProxyValues()
onVecSizeChanged: layoutVector.updateProxyValues()
spacing: StudioTheme.Values.sectionRowSpacing / 2
spacing: StudioTheme.Values.sectionRowSpacing
function isValidValue(v) {
return !(v === undefined || isNaN(v))
}
function updateExpression() {
function updateExpressionFromExpression() {
if (layoutVector.block)
return
layoutVector.backendValue.expression = layoutVector.proxyValues[0].expression
// Only the first proxy value has an expression editor enabled
}
function updateExpressionFromValue() {
if (layoutVector.block)
return
for (let i = 0; i < vecSize; ++i) {
if (!isValidValue(proxyValues[i].value))
if (!layoutVector.isValidValue(layoutVector.proxyValues[i].value))
return
}
let expStr = "Qt.vector" + vecSize + "d("+proxyValues[0].value
for (let j=1; j < vecSize; ++j)
expStr += ", " + proxyValues[j].value
let expStr = "Qt.vector" + layoutVector.vecSize + "d(" + layoutVector.proxyValues[0].value
for (let j=1; j < layoutVector.vecSize; ++j)
expStr += ", " + layoutVector.proxyValues[j].value
expStr += ")"
layoutVector.backendValue.expression = expStr
}
function updateProxyValues() {
if (!backendValue)
if (!layoutVector.backendValue)
return;
const startIndex = backendValue.expression.indexOf('(')
const endIndex = backendValue.expression.indexOf(')')
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex)
return
const numberStr = backendValue.expression.slice(startIndex + 1, endIndex)
const numbers = numberStr.split(",")
if (!Array.isArray(numbers) || numbers.length !== vecSize)
return
let vals = layoutVector.backendValue.getExpressionAsVector()
let vals = []
for (let i = 0; i < vecSize; ++i) {
vals[i] = parseFloat(numbers[i])
if (!isValidValue(vals[i]))
return
layoutVector.block = true
if (layoutVector.vecSize === vals.length) {
for (let j = 0; j < layoutVector.vecSize; ++j) {
layoutVector.proxyValues[j].setForceBound(false)
layoutVector.proxyValues[j].value = vals[j]
}
} else {
for (let j = 0; j < layoutVector.vecSize; ++j) {
layoutVector.proxyValues[j].setForceBound(true) // Required since the backendValue is just proxied
layoutVector.proxyValues[j].expression = layoutVector.backendValue.expression
}
}
for (let j = 0; j < vecSize; ++j)
proxyValues[j].value = vals[j]
layoutVector.block = false
}
SecondColumnLayout {
@@ -357,15 +368,18 @@ Section {
tooltip: "X"
}
Spacer { implicitWidth: StudioTheme.Values.controlGap }
Spacer {
implicitWidth: StudioTheme.Values.controlGap
+ StudioTheme.Values.actionIndicatorWidth
}
SpinBox {
id: boxY
actionIndicatorVisible: false
minimumValue: -9999999
maximumValue: 9999999
decimals: 2
implicitWidth: StudioTheme.Values.twoControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth
}
Spacer { implicitWidth: StudioTheme.Values.controlLabelGap }
@@ -386,14 +400,16 @@ Section {
}
SecondColumnLayout {
visible: vecSize > 2
visible: layoutVector.vecSize > 2
Spacer { implicitWidth: StudioTheme.Values.actionIndicatorWidth }
SpinBox {
id: boxZ
actionIndicatorVisible: false
minimumValue: -9999999
maximumValue: 9999999
decimals: 2
implicitWidth: StudioTheme.Values.twoControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth
}
Spacer { implicitWidth: StudioTheme.Values.controlLabelGap }
@@ -401,19 +417,22 @@ Section {
ControlLabel {
text: "Z"
tooltip: "Z"
visible: vecSize > 2
visible: layoutVector.vecSize > 2
}
Spacer { implicitWidth: StudioTheme.Values.controlGap }
Spacer {
implicitWidth: StudioTheme.Values.controlGap
+ StudioTheme.Values.actionIndicatorWidth
}
SpinBox {
id: boxW
actionIndicatorVisible: false
minimumValue: -9999999
maximumValue: 9999999
decimals: 2
implicitWidth: StudioTheme.Values.twoControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth
visible: vecSize > 3
visible: layoutVector.vecSize > 3
}
Spacer { implicitWidth: StudioTheme.Values.controlLabelGap }
@@ -421,7 +440,7 @@ Section {
ControlLabel {
text: "W"
tooltip: "W"
visible: vecSize > 3
visible: layoutVector.vecSize > 3
}
Spacer { implicitWidth: StudioTheme.Values.controlGap }
@@ -430,7 +449,7 @@ Section {
height: 10
implicitWidth: StudioTheme.Values.twoControlColumnWidth
+ StudioTheme.Values.actionIndicatorWidth
visible: vecSize === 2 // Placeholder for last spinbox
visible: layoutVector.vecSize === 2 // Placeholder for last spinbox
}
Spacer { implicitWidth: StudioTheme.Values.twoControlColumnGap }
@@ -486,9 +505,12 @@ Section {
model: root.propertiesModel
row: index
}
PropertyLabel {
text: propertyName
tooltip: propertyType
Layout.alignment: Qt.AlignTop
Layout.topMargin: 6
}
Loader {
@@ -540,7 +562,8 @@ Section {
for (let i = 0; i < vecSize; ++i) {
var newProxyValue = propertyRow.createProxyBackendValue()
loader.item.proxyValues.push(newProxyValue)
newProxyValue.valueChangedQml.connect(loader.item.updateExpression)
newProxyValue.valueChangedQml.connect(loader.item.updateExpressionFromValue)
newProxyValue.expressionChangedQml.connect(loader.item.updateExpressionFromExpression)
loader.item.spinBoxes[i].backendValue = newProxyValue
}
propertyRow.backendValue.expressionChanged.connect(loader.item.updateProxyValues)

View File

@@ -22,6 +22,9 @@ Item {
property alias realDragRange: spinBox.realDragRange
property alias pixelsPerUnit: spinBox.pixelsPerUnit
property alias actionIndicatorEnabled: spinBox.actionIndicator.enabled
property alias actionIndicatorVisible: spinBox.actionIndicatorVisible
width: 96
implicitHeight: spinBox.height

View File

@@ -153,7 +153,8 @@ void PropertyEditorValue::setExpressionWithEmit(const QString &expression)
if (m_expression != expression) {
setExpression(expression);
m_value.clear();
emit expressionChanged(nameAsQString()); // Note that we set the name in this case
emit expressionChanged(nameAsQString());
emit expressionChangedQml();// Note that we set the name in this case
}
}
@@ -180,7 +181,7 @@ bool PropertyEditorValue::isInSubState() const
bool PropertyEditorValue::isBound() const
{
const QmlObjectNode objectNode(modelNode());
return objectNode.isValid() && objectNode.hasBindingProperty(name());
return m_forceBound || (objectNode.isValid() && objectNode.hasBindingProperty(name()));
}
bool PropertyEditorValue::isInModel() const
@@ -334,6 +335,7 @@ void PropertyEditorValue::resetValue()
m_expression = QString();
emit valueChanged(nameAsQString(), QVariant());
emit expressionChanged({});
emit expressionChangedQml();
}
}
@@ -425,6 +427,34 @@ QStringList PropertyEditorValue::getExpressionAsList() const
return generateStringList(expression());
}
QVector<double> PropertyEditorValue::getExpressionAsVector() const
{
const QRegularExpression rx(
QRegularExpression::anchoredPattern("Qt.vector(2|3|4)d\\((.*?)\\)"));
const QRegularExpressionMatch match = rx.match(expression());
if (!match.hasMatch())
return {};
const QStringList floats = match.captured(2).split(',', Qt::SkipEmptyParts);
bool ok;
const int num = match.captured(1).toInt();
if (num != floats.count())
return {};
QVector<double> ret;
for (const QString &number : floats) {
ret.append(number.toDouble(&ok));
if (!ok)
return {};
}
return ret;
}
bool PropertyEditorValue::idListAdd(const QString &value)
{
const QmlObjectNode objectNode(modelNode());
@@ -507,6 +537,15 @@ void PropertyEditorValue::openMaterialEditor(int idx)
m_modelNode.view()->emitCustomNotification("select_material", {}, {idx});
}
void PropertyEditorValue::setForceBound(bool b)
{
if (m_forceBound == b)
return;
m_forceBound = b;
emit isBoundChanged();
}
QStringList PropertyEditorValue::generateStringList(const QString &string) const
{
QString copy = string;

View File

@@ -126,12 +126,15 @@ public:
bool isIdList() const;
Q_INVOKABLE QStringList getExpressionAsList() const;
Q_INVOKABLE QVector<double> getExpressionAsVector() const;
Q_INVOKABLE bool idListAdd(const QString &value);
Q_INVOKABLE bool idListRemove(int idx);
Q_INVOKABLE bool idListReplace(int idx, const QString &value);
Q_INVOKABLE void commitDrop(const QString &dropData);
Q_INVOKABLE void openMaterialEditor(int idx);
Q_INVOKABLE void setForceBound(bool b);
public slots:
void resetValue();
void setEnumeration(const QString &scope, const QString &name);
@@ -143,6 +146,8 @@ signals:
void expressionChanged(const QString &name); // HACK - We use the same notifer for the backend and frontend.
// If name is empty the signal is used for QML.
void expressionChangedQml();
void exportPropertyAsAliasRequested(const QString &name);
void removeAliasExportRequested(const QString &name);
@@ -168,6 +173,7 @@ private:
bool m_hasActiveDrag = false;
bool m_isValid = false; // if the property value belongs to a non-existing complexProperty it is invalid
PropertyEditorNodeWrapper *m_complexNode;
bool m_forceBound = false;
};
} // namespace QmlDesigner