feat: heuristic semantic display for OFX RGBA parameters
ColorCorrectOFX and similar plugins declare per-channel controls
(Gamma, Contrast, Saturation, Gain, Offset) as kOfxParamTypeRGBA.
Olive previously mapped every RGBA param to NodeValue::kColor and
rendered it as a ColorButton, which is semantically wrong for
adjustment sliders.
This commit adds heuristic semantic detection to distinguish
"true color" inputs (color pickers) from "per-channel scalar"
inputs (float sliders):
- label/hint/name keywords ("gamma", "contrast", "gain", ...)
- display range outside [0, 1]
- uniform default values across all channels
The detected semantic ("color" or "scalar") is stored as the
node input property "color_semantic". The display range and hint
are also persisted as "min" / "max" / "tooltip".
NodeParamViewWidgetBridge now branches on "color_semantic":
- "scalar" → 4× FloatSlider (reuses existing ProcessSlider /
keyframe-track logic, since kColor already splits into 4 tracks)
- otherwise → ColorButton (unchanged)
All 4 test suites pass.
This commit is contained in:
@@ -100,6 +100,99 @@ QVariant DefaultValueForParam(const OFX::Host::Param::Base *param)
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Deduce whether an RGB/RGBA parameter semantically represents a color
|
||||
* pickers or per-channel scalar values (e.g. gamma, contrast).
|
||||
*
|
||||
* Uses heuristics based on label, hint, display range, default values,
|
||||
* and parent group name.
|
||||
*/
|
||||
QString DeduceColorSemantic(const OFX::Host::Param::Base *param,
|
||||
const QHash<QString, QString> &group_labels)
|
||||
{
|
||||
const std::string &ofxType = param->getType();
|
||||
if (ofxType != kOfxParamTypeRGB && ofxType != kOfxParamTypeRGBA) {
|
||||
return QStringLiteral("color");
|
||||
}
|
||||
|
||||
const QString label = QString::fromStdString(param->getLabel()).toLower();
|
||||
const QString hint = QString::fromStdString(param->getHint()).toLower();
|
||||
const QString name = QString::fromStdString(param->getName()).toLower();
|
||||
|
||||
// Rule 1: explicit color keywords → color
|
||||
static const QStringList kColorKeywords = {
|
||||
QStringLiteral("color"), QStringLiteral("colour"),
|
||||
QStringLiteral("fill"), QStringLiteral("tint"),
|
||||
QStringLiteral("key")
|
||||
};
|
||||
for (const QString &kw : kColorKeywords) {
|
||||
if (label.contains(kw) || hint.contains(kw) || name.contains(kw)) {
|
||||
return QStringLiteral("color");
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 2: explicit scalar/adjustment keywords → scalar
|
||||
static const QStringList kScalarKeywords = {
|
||||
QStringLiteral("gamma"), QStringLiteral("contrast"),
|
||||
QStringLiteral("gain"), QStringLiteral("offset"),
|
||||
QStringLiteral("saturation"), QStringLiteral("exposure"),
|
||||
QStringLiteral("brightness"), QStringLiteral("lift"),
|
||||
QStringLiteral("multiply"), QStringLiteral("scale"),
|
||||
QStringLiteral("pivot")
|
||||
};
|
||||
for (const QString &kw : kScalarKeywords) {
|
||||
if (label.contains(kw) || hint.contains(kw) || name.contains(kw)) {
|
||||
return QStringLiteral("scalar");
|
||||
}
|
||||
}
|
||||
|
||||
// Rule 3: display range significantly outside/asymmetric to [0,1] → scalar
|
||||
const auto &props = param->getProperties();
|
||||
const int dim = (ofxType == kOfxParamTypeRGBA) ? 4 : 3;
|
||||
double dmin[4] = {0, 0, 0, 0};
|
||||
double dmax[4] = {1, 1, 1, 1};
|
||||
props.getDoublePropertyN(kOfxParamPropDisplayMin, dmin, dim);
|
||||
props.getDoublePropertyN(kOfxParamPropDisplayMax, dmax, dim);
|
||||
bool range_looks_scalar = false;
|
||||
for (int i = 0; i < dim; ++i) {
|
||||
if (dmin[i] < -0.01 || dmax[i] > 1.01) {
|
||||
range_looks_scalar = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (range_looks_scalar) {
|
||||
return QStringLiteral("scalar");
|
||||
}
|
||||
|
||||
// Rule 4: default values all equal → scalar (lean)
|
||||
double defs[4] = {0, 0, 0, 1};
|
||||
props.getDoublePropertyN(kOfxParamPropDefault, defs, dim);
|
||||
bool all_equal = true;
|
||||
for (int i = 1; i < dim; ++i) {
|
||||
if (defs[i] != defs[0]) {
|
||||
all_equal = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_equal) {
|
||||
return QStringLiteral("scalar");
|
||||
}
|
||||
|
||||
// Rule 5: parent group contains scalar keywords → scalar
|
||||
const QString parent =
|
||||
QString::fromStdString(param->getParentName()).toLower();
|
||||
if (!parent.isEmpty()) {
|
||||
for (const QString &kw : kScalarKeywords) {
|
||||
if (parent.contains(kw)) {
|
||||
return QStringLiteral("scalar");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
return QStringLiteral("color");
|
||||
}
|
||||
|
||||
QHash<QString, QVariant>
|
||||
BuildDefaultValues(const std::map<std::string, OFX::Host::Param::Instance *> ¶ms)
|
||||
{
|
||||
@@ -274,6 +367,36 @@ olive::plugin::PluginNode::PluginNode(
|
||||
SetInputProperty(input_id, QStringLiteral("ui_page"),
|
||||
page_for_param.value(input_id));
|
||||
}
|
||||
if (type == NodeValue::kColor) {
|
||||
QString semantic =
|
||||
DeduceColorSemantic(param.second, group_labels);
|
||||
SetInputProperty(input_id,
|
||||
QStringLiteral("color_semantic"),
|
||||
semantic);
|
||||
|
||||
const int dim =
|
||||
(ofxType == kOfxParamTypeRGBA) ? 4 : 3;
|
||||
double dmin[4] = {0, 0, 0, 0};
|
||||
double dmax[4] = {1, 1, 1, 1};
|
||||
props.getDoublePropertyN(kOfxParamPropDisplayMin,
|
||||
dmin, dim);
|
||||
props.getDoublePropertyN(kOfxParamPropDisplayMax,
|
||||
dmax, dim);
|
||||
SetInputProperty(input_id,
|
||||
QStringLiteral("min"),
|
||||
dmin[0]);
|
||||
SetInputProperty(input_id,
|
||||
QStringLiteral("max"),
|
||||
dmax[0]);
|
||||
|
||||
const QString hint = QString::fromStdString(
|
||||
param.second->getHint());
|
||||
if (!hint.isEmpty()) {
|
||||
SetInputProperty(input_id,
|
||||
QStringLiteral("tooltip"),
|
||||
hint);
|
||||
}
|
||||
}
|
||||
if (type == NodeValue::kCombo || type == NodeValue::kStrCombo) {
|
||||
QStringList option_labels;
|
||||
QStringList option_values;
|
||||
|
||||
@@ -146,11 +146,18 @@ void NodeParamViewWidgetBridge::CreateWidgets()
|
||||
break;
|
||||
}
|
||||
case NodeValue::kColor: {
|
||||
ColorButton *color_button = new ColorButton(
|
||||
GetInnerInput().node()->project()->color_manager(), parent);
|
||||
widgets_.append(color_button);
|
||||
connect(color_button, &ColorButton::ColorChanged, this,
|
||||
if (GetInnerInput().GetProperty("color_semantic")
|
||||
.toString() == QStringLiteral("scalar")) {
|
||||
CreateSliders<FloatSlider>(4, parent);
|
||||
} else {
|
||||
ColorButton *color_button = new ColorButton(
|
||||
GetInnerInput().node()->project()->color_manager(),
|
||||
parent);
|
||||
widgets_.append(color_button);
|
||||
connect(
|
||||
color_button, &ColorButton::ColorChanged, this,
|
||||
&NodeParamViewWidgetBridge::WidgetCallback);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NodeValue::kText: {
|
||||
@@ -322,30 +329,42 @@ void NodeParamViewWidgetBridge::WidgetCallback()
|
||||
break;
|
||||
}
|
||||
case NodeValue::kColor: {
|
||||
// Sender is a ColorButton
|
||||
ManagedColor c = static_cast<ColorButton *>(sender())->GetColor();
|
||||
if (GetInnerInput().GetProperty("color_semantic")
|
||||
.toString() == QStringLiteral("scalar")) {
|
||||
FloatSlider *slider =
|
||||
static_cast<FloatSlider *>(sender());
|
||||
ProcessSlider(slider, slider->GetValue());
|
||||
} else {
|
||||
// Sender is a ColorButton
|
||||
ManagedColor c =
|
||||
static_cast<ColorButton *>(sender())->GetColor();
|
||||
|
||||
MultiUndoCommand *command = new MultiUndoCommand();
|
||||
MultiUndoCommand *command = new MultiUndoCommand();
|
||||
|
||||
SetInputValueInternal(c.red(), 0, command, false);
|
||||
SetInputValueInternal(c.green(), 1, command, false);
|
||||
SetInputValueInternal(c.blue(), 2, command, false);
|
||||
SetInputValueInternal(c.alpha(), 3, command, false);
|
||||
SetInputValueInternal(c.red(), 0, command, false);
|
||||
SetInputValueInternal(c.green(), 1, command, false);
|
||||
SetInputValueInternal(c.blue(), 2, command, false);
|
||||
SetInputValueInternal(c.alpha(), 3, command, false);
|
||||
|
||||
Node *n = GetInnerInput().node();
|
||||
n->blockSignals(true);
|
||||
n->SetInputProperty(GetInnerInput().input(),
|
||||
QStringLiteral("col_input"), c.color_input());
|
||||
n->SetInputProperty(GetInnerInput().input(),
|
||||
QStringLiteral("col_display"),
|
||||
c.color_output().display());
|
||||
n->SetInputProperty(GetInnerInput().input(), QStringLiteral("col_view"),
|
||||
c.color_output().view());
|
||||
n->SetInputProperty(GetInnerInput().input(), QStringLiteral("col_look"),
|
||||
c.color_output().look());
|
||||
n->blockSignals(false);
|
||||
Node *n = GetInnerInput().node();
|
||||
n->blockSignals(true);
|
||||
n->SetInputProperty(
|
||||
GetInnerInput().input(), QStringLiteral("col_input"),
|
||||
c.color_input());
|
||||
n->SetInputProperty(
|
||||
GetInnerInput().input(), QStringLiteral("col_display"),
|
||||
c.color_output().display());
|
||||
n->SetInputProperty(
|
||||
GetInnerInput().input(), QStringLiteral("col_view"),
|
||||
c.color_output().view());
|
||||
n->SetInputProperty(
|
||||
GetInnerInput().input(), QStringLiteral("col_look"),
|
||||
c.color_output().look());
|
||||
n->blockSignals(false);
|
||||
|
||||
Core::instance()->undo_stack()->push(command, GetCommandName());
|
||||
Core::instance()->undo_stack()->push(command,
|
||||
GetCommandName());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NodeValue::kText: {
|
||||
@@ -539,18 +558,42 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues()
|
||||
break;
|
||||
}
|
||||
case NodeValue::kColor: {
|
||||
ManagedColor mc =
|
||||
GetInnerInput().GetValueAtTime(node_time).value<Color>();
|
||||
if (GetInnerInput().GetProperty("color_semantic")
|
||||
.toString() == QStringLiteral("scalar")) {
|
||||
Color c = GetInnerInput()
|
||||
.GetValueAtTime(node_time)
|
||||
.value<Color>();
|
||||
static_cast<FloatSlider *>(widgets_.at(0))
|
||||
->SetValue(static_cast<double>(c.red()));
|
||||
static_cast<FloatSlider *>(widgets_.at(1))
|
||||
->SetValue(static_cast<double>(c.green()));
|
||||
static_cast<FloatSlider *>(widgets_.at(2))
|
||||
->SetValue(static_cast<double>(c.blue()));
|
||||
static_cast<FloatSlider *>(widgets_.at(3))
|
||||
->SetValue(static_cast<double>(c.alpha()));
|
||||
} else {
|
||||
ManagedColor mc = GetInnerInput()
|
||||
.GetValueAtTime(node_time)
|
||||
.value<Color>();
|
||||
|
||||
mc.set_color_input(GetInnerInput().GetProperty("col_input").toString());
|
||||
mc.set_color_input(
|
||||
GetInnerInput().GetProperty("col_input").toString());
|
||||
|
||||
QString d = GetInnerInput().GetProperty("col_display").toString();
|
||||
QString v = GetInnerInput().GetProperty("col_view").toString();
|
||||
QString l = GetInnerInput().GetProperty("col_look").toString();
|
||||
QString d = GetInnerInput()
|
||||
.GetProperty("col_display")
|
||||
.toString();
|
||||
QString v = GetInnerInput()
|
||||
.GetProperty("col_view")
|
||||
.toString();
|
||||
QString l = GetInnerInput()
|
||||
.GetProperty("col_look")
|
||||
.toString();
|
||||
|
||||
mc.set_color_output(ColorTransform(d, v, l));
|
||||
mc.set_color_output(ColorTransform(d, v, l));
|
||||
|
||||
static_cast<ColorButton *>(widgets_.first())->SetColor(mc);
|
||||
static_cast<ColorButton *>(widgets_.first())
|
||||
->SetColor(mc);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case NodeValue::kText: {
|
||||
|
||||
Reference in New Issue
Block a user