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:
2026-05-14 20:50:45 +08:00
parent 43ba1c4642
commit b2de04962d
2 changed files with 198 additions and 32 deletions
@@ -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: {