diff --git a/src/controllers/addonmetadataparser.cpp b/src/controllers/addonmetadataparser.cpp index 85e6dd4121..683fa92c6b 100644 --- a/src/controllers/addonmetadataparser.cpp +++ b/src/controllers/addonmetadataparser.cpp @@ -80,6 +80,8 @@ AddOnFilterDescriptor AddOnMetadataParser::parse(const QString &service, out.minimum = QString::fromUtf8(parameter.get("minimum")); out.maximum = QString::fromUtf8(parameter.get("maximum")); out.description = QString::fromUtf8(parameter.get("description")); + out.normalizedCoordinates = parseYesNoBool(parameter.get("normalized_coordinates")); + out.normalizedDefault = parseYesNoBool(parameter.get("normalized_default")); out.name = QString::fromUtf8(parameter.get("identifier")); const QString parameterType = out.type.trimmed().toLower(); diff --git a/src/controllers/addonmetadataparser.h b/src/controllers/addonmetadataparser.h index 4855cf45de..5429c6ce47 100644 --- a/src/controllers/addonmetadataparser.h +++ b/src/controllers/addonmetadataparser.h @@ -39,6 +39,8 @@ struct AddOnParameterDescriptor QString minimum; QString maximum; QString description; + bool normalizedCoordinates = false; + bool normalizedDefault = false; }; struct AddOnFilterDescriptor diff --git a/src/controllers/addonqmlgenerator.cpp b/src/controllers/addonqmlgenerator.cpp index 61c24f9a31..b79f259065 100644 --- a/src/controllers/addonqmlgenerator.cpp +++ b/src/controllers/addonqmlgenerator.cpp @@ -124,6 +124,8 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, QStringList quotedMaximums; QStringList quotedDescriptions; QStringList quotedValueLists; + QStringList quotedNormalizedCoordinates; + QStringList quotedNormalizedDefault; QStringList quotedKeyframeProperties; QStringList quotedKeyframeMapEntries; QStringList setControlsLines; @@ -161,6 +163,14 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, quotedMaximums << QStringLiteral("%1: %2").arg(quotedJsString(parameter.name), quotedJsString(parameter.maximum)); } + if (parameter.normalizedCoordinates) { + quotedNormalizedCoordinates + << QStringLiteral("%1: 'yes'").arg(quotedJsString(parameter.name)); + } + if (parameter.normalizedDefault) { + quotedNormalizedDefault + << QStringLiteral("%1: 'yes'").arg(quotedJsString(parameter.name)); + } if (!parameter.description.isEmpty()) { quotedDescriptions << QStringLiteral("%1: %2").arg(quotedJsString(parameter.name), quotedJsString( @@ -216,6 +226,14 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, } else if (parameterType == QStringLiteral("boolean")) { setControlsLines << QStringLiteral(" %1.checked = root.booleanValue(%2);") .arg(editorId, nameLiteral); + } else if (parameterType == QStringLiteral("rect") + && (parameterWidget == QStringLiteral("point") + || parameterWidget == QStringLiteral("size"))) { + setControlsLines + << QStringLiteral( + " { var _r = filter.getRect(%2); %1.valueX = isNaN(_r.x) ? 0 : " + "_r.x; %1.valueY = isNaN(_r.y) ? 0 : _r.y; }") + .arg(editorId, nameLiteral); } else if ((parameterType == QStringLiteral("integer") || parameterType == QStringLiteral("float")) && !useTextForNumericEditor) { @@ -274,6 +292,12 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, << "}\n\n" " property var propertyValues: {" << quotedValueLists.join(QStringLiteral(", ")) + << "}\n\n" + " property var propertyNormalizedCoordinates: {" + << quotedNormalizedCoordinates.join(QStringLiteral(", ")) + << "}\n\n" + " property var propertyNormalizedDefault: {" + << quotedNormalizedDefault.join(QStringLiteral(", ")) << "}\n\n" " keyframableParameters: [" << quotedKeyframeProperties.join(QStringLiteral(", ")) @@ -434,10 +458,24 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, " var d = root.propertyDefaults[p];\n" " var type = propertyType(p);\n" " var widget = propertyWidget(p);\n" - " if (d !== undefined && d !== null && d !== '')\n" - " filter.set(p, isBooleanType(type) ? (booleanValue(p) ? '1' : '0') " + " if (d !== undefined && d !== null && d !== '') {\n" + " if (type === 'rect' && root.propertyNormalizedDefault\n" + " && root.propertyNormalizedDefault[p] === 'yes') {\n" + " var _dp = String(d).trim().split(/\\s+/);\n" + " var _dx = _dp.length > 0 ? parseFloat(_dp[0]) : 0;\n" + " var _dy = _dp.length > 1 ? parseFloat(_dp[1]) : 0;\n" + " if (isNaN(_dx)) _dx = 0;\n" + " if (isNaN(_dy)) _dy = 0;\n" + " _dx *= profile.width;\n" + " _dy *= profile.height;\n" + " filter.set(p, _dx + ' ' + _dy + ' 0 0 0');\n" + " } else {\n" + " filter.set(p, isBooleanType(type) ? (booleanValue(p) ? '1' : " + "'0') " ": ((isNumericType(type) && widget === 'text') ? String(d) : (isNumericType(type) ? " "Number(d) : d)));\n" + " }\n" + " }\n" " }\n" " filter.savePreset(propertyNames);\n" " }\n" @@ -740,6 +778,48 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, " }\n" " }\n" " }\n"; + } else if (parameterType == QStringLiteral("rect") + && (parameterWidget == QStringLiteral("point") + || parameterWidget == QStringLiteral("size"))) { + const QString labelFirst = (parameterWidget == QStringLiteral("size")) + ? QStringLiteral("W") + : QStringLiteral("X"); + const QString labelSecond = (parameterWidget == QStringLiteral("size")) + ? QStringLiteral("H") + : QStringLiteral("Y"); + stream + << " Shotcut.Number2D {\n" + " id: " + << editorId + << "\n" + " Layout.columnSpan: " + << (parameter.hideLabel ? "2" : "1") + << "\n" + " Layout.fillWidth: true\n" + " Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter\n" + " readonly property string propertyName: " + << nameLiteral + << "\n" + " readonly property string typeName: " + "root.propertyType(propertyName)\n" + " decimals: root.isIntegerType(typeName) ? 0 : 3\n" + " from: (root.propertyMinimums && " + "root.propertyMinimums[propertyName] !== undefined && " + "root.propertyMinimums[propertyName] !== '') ? " + "Number(root.propertyMinimums[propertyName]) : -99999\n" + " to: (root.propertyMaximums && root.propertyMaximums[propertyName] " + "!== undefined && root.propertyMaximums[propertyName] !== '') ? " + "Number(root.propertyMaximums[propertyName]) : 99999\n" + " labelFirst: " + << quotedJsString(labelFirst) + << "\n" + " labelSecond: " + << quotedJsString(labelSecond) + << "\n" + " onValuesModified: {\n" + " filter.set(propertyName, valueX + ' ' + valueY + ' 0 0 1');\n" + " }\n" + " }\n"; } else if ((parameterType == QStringLiteral("integer") || parameterType == QStringLiteral("float")) && !useTextForNumericEditor) { @@ -914,6 +994,23 @@ bool AddOnQmlGenerator::generate(const AddOnFilterDescriptor &descriptor, " filter.set(propertyName, defaultValue);\n" " root.setControls();\n"; } + } else if (parameterType == QStringLiteral("rect") + && (parameterWidget == QStringLiteral("point") + || parameterWidget == QStringLiteral("size"))) { + stream << " var _ds = root.propertyDefaults ? " + "(root.propertyDefaults[propertyName] || '') : '';\n" + " var _dp = String(_ds).trim().split(/\\s+/);\n" + " var _dx = _dp.length > 0 ? parseFloat(_dp[0]) : 0;\n" + " var _dy = _dp.length > 1 ? parseFloat(_dp[1]) : 0;\n" + " if (isNaN(_dx)) _dx = 0;\n" + " if (isNaN(_dy)) _dy = 0;\n" + " if (root.propertyNormalizedDefault &&\n" + "root.propertyNormalizedDefault[propertyName] === 'yes') {\n" + " _dx *= profile.width;\n" + " _dy *= profile.height;\n" + " }\n" + " filter.set(propertyName, _dx + ' ' + _dy + ' 0 0 0');\n" + " root.setControls();\n"; } else { stream << " var defaultValue = root.defaultTextValue(propertyName);\n" diff --git a/src/qml/modules/Shotcut/Controls/Number2D.qml b/src/qml/modules/Shotcut/Controls/Number2D.qml new file mode 100644 index 0000000000..1e6cc5ee03 --- /dev/null +++ b/src/qml/modules/Shotcut/Controls/Number2D.qml @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Meltytech, LLC + * + * 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 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Shotcut.Controls as Shotcut + +// A pair of spinboxes for two-dimensional parameters (point or size). +// The caller sets valueX / valueY and connects to valuesModified to write back. +// Set decimals: 0 for integer parameters, > 0 for floating-point parameters. +RowLayout { + id: root + + property real valueX: 0 + property real valueY: 0 + property int decimals: 0 + property real from: -99999 + property real to: 99999 + property real stepSize: 1 + property string labelFirst: 'X' + property string labelSecond: 'Y' + + signal valuesModified + + spacing: 4 + + onValueXChanged: { + if (Math.abs(xBox.value - valueX) > 1e-9) + xBox.value = valueX; + } + onValueYChanged: { + if (Math.abs(yBox.value - valueY) > 1e-9) + yBox.value = valueY; + } + + Label { + text: root.labelFirst + textFormat: Text.PlainText + verticalAlignment: Text.AlignVCenter + } + + Shotcut.DoubleSpinBox { + id: xBox + + Layout.fillWidth: true + value: root.valueX + decimals: root.decimals + from: root.from + to: root.to + stepSize: root.stepSize + onValueModified: { + root.valueX = value; + root.valuesModified(); + } + } + + Label { + text: root.labelSecond + textFormat: Text.PlainText + verticalAlignment: Text.AlignVCenter + } + + Shotcut.DoubleSpinBox { + id: yBox + + Layout.fillWidth: true + value: root.valueY + decimals: root.decimals + from: root.from + to: root.to + stepSize: root.stepSize + onValueModified: { + root.valueY = value; + root.valuesModified(); + } + } +} diff --git a/src/qml/modules/Shotcut/Controls/qmldir b/src/qml/modules/Shotcut/Controls/qmldir index ef6e222724..95887a56d4 100644 --- a/src/qml/modules/Shotcut/Controls/qmldir +++ b/src/qml/modules/Shotcut/Controls/qmldir @@ -29,3 +29,4 @@ MotionTrackerDialog 1.0 MotionTrackerDialog.qml HorizontalScrollBar 1.0 HorizontalScrollBar.qml VerticalScrollBar 1.0 VerticalScrollBar.qml ChannelMask 1.0 ChannelMask.qml +Number2D 1.0 Number2D.qml