/*** Olive - Non-Linear Video Editor Copyright (C) 2022 Olive Team Modifications Copyright (C) 2025 mikesolar 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 . ***/ #include "nodeparamviewwidgetbridge.h" #include "playback/playbackcontroller.h" #include #include #include #include #include #include #include "oakutil/qtutils.h" #include "core.h" #include "nodeparambutton.h" #include "nodeparamviewarraywidget.h" #include "nodeparamviewtextedit.h" #include "oakengine/color.h" #include "oakengine/events.h" #include "common/nodevaluehandle.h" #include "common/oakvaluehelper.h" #include "oakengine/node.h" #include "oakengine/plugin.h" #include "oakengine/viewer.h" #include "oakengine/lut.h" #include "oakengine/undo.h" #include "widget/bezier/bezierwidget.h" #include "widget/colorbutton/colorbutton.h" #include "widget/filefield/filefield.h" #include "widget/filefield/lutfilefield.h" #include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/slider/floatslider.h" #include "widget/slider/integerslider.h" #include "widget/slider/rationalslider.h" //#include namespace olive { static int64_t rational_to_node_ts(OakEngineNode *node, const Rational &time) { int num = 0, den = 1; oakengine_node_frame_time_base(node, &num, &den); return core::Timecode::time_to_timestamp(time, Rational(num, den)); } static QVariant GetInputValueAtTime(const oak::Input &input, const Rational &node_time) { OakEngineNode *node = input.node_handle(); if (!node) { return QVariant(); } const NodeValueType::Type type = static_cast(input.data_type()); const QByteArray input_utf8 = input.input_id().toUtf8(); const char *input_id = input_utf8.constData(); const int element = input.element(); const int64_t time_ts = rational_to_node_ts(node, node_time); const OakEngineNode *enode = node; switch (type) { case NodeValueType::k_int: case NodeValueType::k_float: case NodeValueType::k_boolean: case NodeValueType::k_rational: case NodeValueType::k_color: case NodeValueType::k_vec2: case NodeValueType::k_vec3: case NodeValueType::k_vec4: case NodeValueType::k_combo: { oak_node_value v; if (oakengine_node_get_input_at_time(enode, input_id, element, -1, time_ts, 1, &v) == OAKENGINE_OK) { return OakNodeValueToQVariant(v); } break; } case NodeValueType::k_file: case NodeValueType::k_text: case NodeValueType::k_font: case NodeValueType::k_str_combo: { char buf[4096]; const int len = oakengine_node_get_input_string_at_time( enode, input_id, element, time_ts, 0, buf, sizeof(buf)); if (len >= 0) { return QString::fromUtf8(buf, len); } break; } case NodeValueType::k_binary: { const int len = oakengine_node_get_input_binary_at_time( enode, input_id, element, time_ts, 0, nullptr, 0); if (len > 0) { QByteArray bytes(len, '\0'); oakengine_node_get_input_binary_at_time( enode, input_id, element, time_ts, 0, bytes.data(), len); return bytes; } else if (len == 0) { return QByteArray(); } break; } case NodeValueType::k_bezier: { double out[6]; if (oakengine_node_get_input_bezier_at_time( enode, input_id, element, time_ts, 0, out) == OAKENGINE_OK) { return QVariant::fromValue( Bezier(out[0], out[1], out[2], out[3], out[4], out[5])); } break; } default: break; } return QVariant(); } static bool ResolveGroupInput(oak::Input *input) { OakEngineNode *node = input->node_handle(); char input_id[256]; int element = input->element(); const QByteArray utf = input->input_id().toUtf8(); const int len = qMin(sizeof(input_id) - 1, utf.size()); memcpy(input_id, utf.constData(), len); input_id[len] = '\0'; if (!oakengine_node_group_get_inner(&node, input_id, sizeof(input_id), &element)) { return false; } *input = oak::Input(node, QString::fromUtf8(input_id), element); return true; } NodeParamViewWidgetBridge::NodeParamViewWidgetBridge(const oak::Input &input, QObject *parent) : QObject(parent) , bridge_(new EngineEventBridge(this)) { connect(bridge_, &EngineEventBridge::node_input_value_changed, this, &NodeParamViewWidgetBridge::input_value_changed); connect(bridge_, &EngineEventBridge::node_input_property_changed, this, [this](OakEngineNode *, const QString &input_id) { property_changed(input_id); }); connect(bridge_, &EngineEventBridge::node_input_data_type_changed, this, &NodeParamViewWidgetBridge::input_data_type_changed); oak::Input resolved = input; do { input_hierarchy_.append(resolved); bridge_->subscribe(reinterpret_cast(resolved.node_handle()), OAKENGINE_EVENT_NODE_INPUT_VALUE_CHANGED); bridge_->subscribe(reinterpret_cast(resolved.node_handle()), OAKENGINE_EVENT_NODE_INPUT_PROPERTY_CHANGED); bridge_->subscribe(reinterpret_cast(resolved.node_handle()), OAKENGINE_EVENT_NODE_INPUT_DATA_TYPE_CHANGED); } while (ResolveGroupInput(&resolved)); dragger_ = oakengine_dragger_create( get_inner_input().node_handle(), get_inner_input().input_id().toUtf8().constData(), get_inner_input().element(), -1); // track set in process_slider at start time create_widgets(); } NodeParamViewWidgetBridge::~NodeParamViewWidgetBridge() { // oakengine_dragger_create() ownership is ours (no-op on NULL) oakengine_dragger_free(dragger_); disconnect(viewer_conn_); } int get_slider_count(NodeValueType::Type type) { return oakengine_node_value_keyframe_track_count(node_value_type_to_c(type)); } namespace { // Map a panel widget's per-track scalar QVariant into the facade POD. // Returns false for types that have no facade mapping (the caller keeps // the legacy path for those). bool variant_to_c_value(NodeValueType::Type type, const QVariant &value, oak_node_value *out) { memset(out, 0, sizeof(*out)); switch (type) { case NodeValueType::k_int: out->type = OAK_NODE_VALUE_INT; out->num = value.toLongLong(); return true; case NodeValueType::k_combo: out->type = OAK_NODE_VALUE_COMBO; out->num = value.toLongLong(); return true; case NodeValueType::k_float: case NodeValueType::k_bezier: out->type = OAK_NODE_VALUE_FLOAT; out->f[0] = value.toDouble(); return true; case NodeValueType::k_boolean: out->type = OAK_NODE_VALUE_BOOL; out->num = value.toBool() ? 1 : 0; return true; case NodeValueType::k_rational: { const Rational r = value.value(); out->type = OAK_NODE_VALUE_RATIONAL; out->num = r.numerator(); out->den = r.denominator(); return true; } case NodeValueType::k_color: out->type = OAK_NODE_VALUE_COLOR; out->f[0] = value.toDouble(); return true; case NodeValueType::k_vec2: out->type = OAK_NODE_VALUE_VEC2; out->f[0] = value.toDouble(); return true; case NodeValueType::k_vec3: out->type = OAK_NODE_VALUE_VEC3; out->f[0] = value.toDouble(); return true; case NodeValueType::k_vec4: out->type = OAK_NODE_VALUE_VEC4; out->f[0] = value.toDouble(); return true; default: return false; } } // Convert rational node time to the facade's frame timestamps using the // facade's own timebase (so the round trip is exact). int64_t node_time_to_ts(OakEngineNode *node, const Rational &time) { int tbn = 0, tbd = 0; oakengine_node_frame_time_base(node, &tbn, &tbd); return Timecode::time_to_timestamp(time, Rational(tbn, tbd), Timecode::k_round); } // Mirrors of engine NodeValue::type_is_numeric()/type_is_vector() // (engine/node/value.h) for the NodeValueType mirror ordinals. bool type_is_numeric(NodeValueType::Type type) { return type == NodeValueType::k_float || type == NodeValueType::k_int || type == NodeValueType::k_rational; } bool type_is_vector(NodeValueType::Type type) { return type == NodeValueType::k_vec2 || type == NodeValueType::k_vec3 || type == NodeValueType::k_vec4; } // NodeInput property access through the typed facade getters (replaces the // engine's QVariant-returning NodeInput::get_property()). QString input_property_string(const oak::Input &input, const char *key) { char buf[4096]; buf[0] = '\0'; oakengine_node_input_get_property_string( input.node_handle(), input.input_id().toUtf8().constData(), key, buf, sizeof(buf)); return QString::fromUtf8(buf); } QStringList input_property_string_list(const oak::Input &input, const char *key) { QStringList out; OakEngineNode *node = input.node_handle(); const QByteArray id = input.input_id().toUtf8(); const int count = oakengine_node_input_get_property_string_list_count(node, id.constData(), key); for (int i = 0; i < count; i++) { char buf[1024]; if (oakengine_node_input_get_property_string_list( node, id.constData(), key, i, buf, sizeof(buf)) >= 0) { out.append(QString::fromUtf8(buf)); } } return out; } bool input_property_bool(const oak::Input &input, const char *key) { int64_t v = 0; return oakengine_node_input_get_property_int( input.node_handle(), input.input_id().toUtf8().constData(), key, &v) == OAKENGINE_OK && v != 0; } // NodeInput::get_split_default_value_for_track() through the facade: the // getter returns the per-track component as a POD typed with the input's // declared type; rebuild the per-track QVariant the sliders expect. QVariant split_default_value_for_track(const oak::Input &input, int track) { oak_node_value v; memset(&v, 0, sizeof(v)); if (oakengine_node_input_get_default_value( input.node_handle(), input.input_id().toUtf8().constData(), track, &v) != OAKENGINE_OK) { return QVariant(); } switch (v.type) { case OAK_NODE_VALUE_INT: case OAK_NODE_VALUE_COMBO: return QVariant::fromValue(v.num); case OAK_NODE_VALUE_RATIONAL: return QVariant::fromValue(Rational(int(v.num), int(v.den))); default: return v.f[0]; } } // Rebuild one input property as a QVariant through the typed facade getters, // replacing Node::get_input_properties() (a QVariantHash) in // update_properties(). WRAPPER-GAP: the facade has no generic QVariant // property getter (NodeInput::get_property()), only typed string/number/ // int/rational/string-list getters, so the value is reconstructed per key. // Only the keys consumed by set_property() are mapped; unknown keys yield an // invalid QVariant, which set_property() ignores. // `data_type` is the property-owning input's declared type (needed to // rebuild the min/max/offset values for the vector types). QVariant read_input_property(const oak::Input &input, const QString &key, NodeValueType::Type data_type) { OakEngineNode *node = input.node_handle(); const QByteArray id = input.input_id().toUtf8(); const QByteArray key_utf8 = key.toUtf8(); const char *k = key_utf8.constData(); if (key == QStringLiteral("min") || key == QStringLiteral("max") || key == QStringLiteral("offset")) { switch (data_type) { case NodeValueType::k_int: case NodeValueType::k_combo: { int64_t v = 0; if (oakengine_node_input_get_property_int(node, id.constData(), k, &v) == OAKENGINE_OK) { return QVariant::fromValue(v); } break; } case NodeValueType::k_float: { double v = 0; if (oakengine_node_input_get_property_number(node, id.constData(), k, -1, &v) == OAKENGINE_OK) { return v; } break; } case NodeValueType::k_rational: { int num = 0, den = 1; if (oakengine_node_input_get_property_rational( node, id.constData(), k, &num, &den) == OAKENGINE_OK) { return QVariant::fromValue(Rational(num, den)); } break; } case NodeValueType::k_vec2: case NodeValueType::k_vec3: case NodeValueType::k_vec4: { const int tracks = get_slider_count(data_type); double c[4] = { 0, 0, 0, 0 }; for (int i = 0; i < tracks; i++) { if (oakengine_node_input_get_property_track_number( node, id.constData(), k, i, &c[i]) != OAKENGINE_OK) { return QVariant(); } } if (data_type == NodeValueType::k_vec2) { return QVariant::fromValue(QVector2D(float(c[0]), float(c[1]))); } if (data_type == NodeValueType::k_vec3) { return QVariant::fromValue( QVector3D(float(c[0]), float(c[1]), float(c[2]))); } return QVariant::fromValue( QVector4D(float(c[0]), float(c[1]), float(c[2]), float(c[3]))); } default: break; } return QVariant(); } if (key == QStringLiteral("combo_str") || key == QStringLiteral("combo_value_str")) { return input_property_string_list(input, k); } if (key == QStringLiteral("tooltip") || key == QStringLiteral("placeholder") || key == QStringLiteral("filter") || key.startsWith(QStringLiteral("color"))) { return input_property_string(input, k); } if (key == QStringLiteral("view") || key == QStringLiteral("decimalplaces")) { int64_t v = 0; if (oakengine_node_input_get_property_int(node, id.constData(), k, &v) == OAKENGINE_OK) { return int(v); } return QVariant(); } if (key == QStringLiteral("base")) { double v = 0; if (oakengine_node_input_get_property_number(node, id.constData(), k, -1, &v) == OAKENGINE_OK) { return v; } return QVariant(); } if (key.startsWith(QStringLiteral("disable")) || key.startsWith(QStringLiteral("enabled")) || key == QStringLiteral("autotrim") || key == QStringLiteral("viewlock") || key == QStringLiteral("directory") || key == QStringLiteral("lut_library") || key == QStringLiteral("vieweronly")) { return input_property_bool(input, k); } return QVariant(); } } // namespace void NodeParamViewWidgetBridge::create_widgets() { QWidget *parent = dynamic_cast(this->parent()); if (get_inner_input().is_array() && get_inner_input().element() == -1) { NodeParamViewArrayWidget *w = new NodeParamViewArrayWidget( get_inner_input().node(), get_inner_input().input_id(), parent); connect(w, &NodeParamViewArrayWidget::double_clicked, this, &NodeParamViewWidgetBridge::array_widget_double_clicked); widgets_.append(w); } else { // We assume the first data type is the "primary" type NodeValueType::Type t = get_data_type(); switch (t) { // None of these inputs have applicable UI widgets case NodeValueType::k_none: case NodeValueType::k_texture: case NodeValueType::k_matrix: case NodeValueType::k_samples: case NodeValueType::k_video_params: case NodeValueType::k_audio_params: case NodeValueType::k_subtitle_params: case NodeValueType::k_binary: case NodeValueType::k_data_type_count: break; case NodeValueType::k_int: { create_sliders(1, parent); break; } case NodeValueType::k_rational: { create_sliders(1, parent); break; } case NodeValueType::k_float: case NodeValueType::k_vec2: case NodeValueType::k_vec3: case NodeValueType::k_vec4: { create_sliders(get_slider_count(t), parent); break; } case NodeValueType::k_combo: case NodeValueType::k_str_combo: { QComboBox *combobox = new QComboBox(parent); QStringList items = input_property_string_list(get_inner_input(), "combo_str"); QStringList values = input_property_string_list(get_inner_input(), "combo_value_str"); const bool use_value_data = (t == NodeValueType::k_str_combo) && !values.isEmpty(); for (int i = 0; i < items.size(); ++i) { const QString &label = items.at(i); if (use_value_data && i < values.size()) { combobox->addItem(label, values.at(i)); } else { combobox->addItem(label); } } widgets_.append(combobox); connect(combobox, static_cast( &QComboBox::currentIndexChanged), this, &NodeParamViewWidgetBridge::widget_callback); break; } case NodeValueType::k_file: { FileField *file_field; if (input_property_bool(get_inner_input(), "lut_library")) { // File inputs that accept LUTs get a combo box for picking // from the global LUT library file_field = new LutFileField(parent); } else { file_field = new FileField(parent); } widgets_.append(file_field); connect(file_field, &FileField::filename_changed, this, &NodeParamViewWidgetBridge::widget_callback); break; } case NodeValueType::k_color: { if (input_property_string(get_inner_input(), "color_semantic") == QStringLiteral("scalar")) { create_sliders(4, parent); } else { ColorButton *color_button = new ColorButton( oakengine_color_manager_from_project( oakengine_node_get_project( get_inner_input().node_handle())), parent); widgets_.append(color_button); connect(color_button, &ColorButton::color_changed, this, &NodeParamViewWidgetBridge::widget_callback); } break; } case NodeValueType::k_text: { NodeParamViewTextEdit *line_edit = new NodeParamViewTextEdit(parent); widgets_.append(line_edit); connect(line_edit, &NodeParamViewTextEdit::text_edited, this, &NodeParamViewWidgetBridge::widget_callback); connect(line_edit, &NodeParamViewTextEdit::request_edit_in_viewer, this, &NodeParamViewWidgetBridge::request_edit_text_in_viewer); break; } case NodeValueType::k_boolean: { QCheckBox *check_box = new QCheckBox(parent); widgets_.append(check_box); connect(check_box, &QCheckBox::clicked, this, &NodeParamViewWidgetBridge::widget_callback); break; } case NodeValueType::k_font: { QFontComboBox *font_combobox = new QFontComboBox(parent); widgets_.append(font_combobox); connect(font_combobox, &QFontComboBox::currentFontChanged, this, &NodeParamViewWidgetBridge::widget_callback); break; } case NodeValueType::k_bezier: { BezierWidget *bezier = new BezierWidget(parent); widgets_.append(bezier); connect(bezier->x_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); connect(bezier->y_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); connect(bezier->cp1_x_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); connect(bezier->cp1_y_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); connect(bezier->cp2_x_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); connect(bezier->cp2_y_slider(), &FloatSlider::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); break; } case NodeValueType::k_push_button: { const oak::Input input = get_inner_input(); NodeParamButton *button = new NodeParamButton(input.name(), parent); widgets_.append(button); // PluginNode predicate goes through the C ABI (replaces the old // dynamic_cast); the handle itself is passed // opaquely to the push-button facade. OakEngineNode *plugin_node = input.node_handle(); if (!oakengine_node_has_plugin(plugin_node)) { plugin_node = nullptr; } connect(button, &NodeParamButton::on_pressed, this, [plugin_node](const QString &name) { oakengine_plugin_node_push_button_clicked( plugin_node, name.toUtf8().constData()); }); } } // Check all properties update_properties(); update_widget_values(); // Install event filter to disable widgets picking up scroll events foreach (QWidget *w, widgets_) { w->installEventFilter(&scroll_filter_); } } } void NodeParamViewWidgetBridge::set_input_value(const QVariant &value, int track) { // POD values go through the liboakengine C ABI facade (one undoable // command with the same set-value-at-time semantics as the old // app-side assembly); types without a facade mapping keep the legacy // undo assembly below. const oak::Input &input = get_inner_input(); oak_node_value c_value; if (!variant_to_c_value(get_data_type(), value, &c_value)) { void *command = oakengine_undo_command_create_multi(); set_input_value_internal(value, track, command, true); oakengine_undo_push(command, get_command_name().toUtf8().constData()); return; } oakengine_node_set_input_at_time( input.node_handle(), input.input_id().toUtf8().constData(), input.element(), node_time_to_ts(input.node_handle(), get_current_time_as_node_time()), track, &c_value, 1); } void NodeParamViewWidgetBridge::set_string_value(const QString &value) { // String-family inputs (file/text/font/str_combo) through the facade. const oak::Input &input = get_inner_input(); oakengine_node_set_input_string_at_time( input.node_handle(), input.input_id().toUtf8().constData(), input.element(), node_time_to_ts(input.node_handle(), get_current_time_as_node_time()), value.toUtf8().constData()); } void NodeParamViewWidgetBridge::set_input_value_internal( const QVariant &value, int track, void *command, bool insert_on_all_tracks_if_no_key) { const oak::Input &input = get_inner_input(); olive::Rational t = get_current_time_as_node_time(); oak_node_value c_value; if (variant_to_c_value(get_data_type(), value, &c_value)) { oakengine_undo_command_multi_add_child( command, oakengine_node_set_value_at_time_command( reinterpret_cast(input.node_handle()), input.input_id().toUtf8().constData(), input.element(), t.numerator(), t.denominator(), &c_value, track, insert_on_all_tracks_if_no_key ? 1 : 0)); } } void NodeParamViewWidgetBridge::process_slider(NumericSliderBase *slider, int slider_track, const QVariant &value) { if (slider->is_dragging()) { // While we're dragging, we block the input's normal signalling and create our own if (!oakengine_dragger_is_started(dragger_)) { OakEngineNode *node = get_inner_input().node_handle(); Rational node_time = get_current_time_as_node_time(); int64_t ts = node_time_to_ts(node, node_time); oakengine_dragger_start(dragger_, ts, slider_track, 0); } oak_node_value c_value; if (variant_to_c_value(get_data_type(), value, &c_value)) { oakengine_dragger_drag(dragger_, &c_value); } } else if (oakengine_dragger_is_started(dragger_)) { // We were dragging and just stopped oak_node_value c_value; if (variant_to_c_value(get_data_type(), value, &c_value)) { oakengine_dragger_drag(dragger_, &c_value); } oakengine_dragger_end(dragger_, get_command_name().toUtf8().constData()); } else { // No drag was involved, we can just push the value set_input_value(value, slider_track); } } void NodeParamViewWidgetBridge::widget_callback() { switch (get_data_type()) { // None of these inputs have applicable UI widgets case NodeValueType::k_none: case NodeValueType::k_texture: case NodeValueType::k_matrix: case NodeValueType::k_samples: case NodeValueType::k_video_params: case NodeValueType::k_audio_params: case NodeValueType::k_push_button: case NodeValueType::k_subtitle_params: case NodeValueType::k_data_type_count: break; case NodeValueType::k_int: { // Widget is a IntegerSlider IntegerSlider *slider = static_cast(sender()); process_slider(slider, QVariant::fromValue(slider->get_value())); break; } case NodeValueType::k_float: { // Widget is a FloatSlider FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); break; } case NodeValueType::k_rational: { // Widget is a RationalSlider RationalSlider *slider = static_cast(sender()); process_slider(slider, QVariant::fromValue(slider->get_value())); break; } case NodeValueType::k_vec2: { // Widget is a FloatSlider FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); break; } case NodeValueType::k_vec3: { // Widget is a FloatSlider FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); break; } case NodeValueType::k_vec4: { // Widget is a FloatSlider FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); break; } case NodeValueType::k_file: { set_string_value(static_cast(sender())->get_filename()); break; } case NodeValueType::k_color: { if (input_property_string(get_inner_input(), "color_semantic") == QStringLiteral("scalar")) { FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); } else { // Sender is a ColorButton: all four components go through the // facade in one undoable command (track -1). The // color-management input properties are not undoable in the // engine and stay direct (same as the old code). ManagedColor c = static_cast(sender())->get_color(); const oak::Input &input = get_inner_input(); oak_node_value c_value; memset(&c_value, 0, sizeof(c_value)); c_value.type = OAK_NODE_VALUE_COLOR; c_value.f[0] = c.red(); c_value.f[1] = c.green(); c_value.f[2] = c.blue(); c_value.f[3] = c.alpha(); oakengine_node_set_input_at_time( input.node_handle(), input.input_id().toUtf8().constData(), input.element(), node_time_to_ts(input.node_handle(), get_current_time_as_node_time()), -1, &c_value, 0); // The old code bracketed these with Node::blockSignals(true/false); // the facade's notify=0 already applies a QSignalBlocker inside // oakengine_node_set_input_property_string, so the bracket is // subsumed. OakEngineNode *n = get_inner_input().node_handle(); const QByteArray input_id = get_inner_input().input_id().toUtf8(); oakengine_node_set_input_property_string( n, input_id.constData(), "col_input", c.color_input().toUtf8().constData(), 0); oakengine_node_set_input_property_string( n, input_id.constData(), "col_display", c.color_output().display().toUtf8().constData(), 0); oakengine_node_set_input_property_string( n, input_id.constData(), "col_view", c.color_output().view().toUtf8().constData(), 0); oakengine_node_set_input_property_string( n, input_id.constData(), "col_look", c.color_output().look().toUtf8().constData(), 0); } break; } case NodeValueType::k_text: { // Sender is a NodeParamViewRichText set_string_value(static_cast(sender())->text()); break; } case NodeValueType::k_binary: { QString text = static_cast(sender())->text(); QByteArray raw = text.toUtf8(); QByteArray decoded = QByteArray::fromBase64(raw); if (decoded.isEmpty() && !raw.isEmpty()) { decoded = raw; } set_input_value(decoded, 0); break; } case NodeValueType::k_boolean: { // Widget is a QCheckBox set_input_value(static_cast(sender())->isChecked(), 0); break; } case NodeValueType::k_font: { // Widget is a QFontComboBox set_string_value( static_cast(sender())->currentFont().family()); break; } case NodeValueType::k_combo: { // Widget is a QComboBox QComboBox *cb = static_cast(widgets_.first()); int index = cb->currentIndex(); // Subtract any splitters up until this point for (int i = index - 1; i >= 0; i--) { if (cb->itemData(i, Qt::AccessibleDescriptionRole).toString() == QStringLiteral("separator")) { index--; } } set_input_value(index, 0); break; } case NodeValueType::k_str_combo: { QComboBox *cb = static_cast(widgets_.first()); const QVariant data = cb->currentData(); if (data.isValid()) { set_string_value(data.toString()); } else { set_string_value(cb->currentText()); } break; } case NodeValueType::k_bezier: { // Widget is a FloatSlider (child of BezierWidget) BezierWidget *bw = static_cast(widgets_.first()); FloatSlider *fs = static_cast(sender()); int index = -1; if (fs == bw->x_slider()) { index = 0; } else if (fs == bw->y_slider()) { index = 1; } else if (fs == bw->cp1_x_slider()) { index = 2; } else if (fs == bw->cp1_y_slider()) { index = 3; } else if (fs == bw->cp2_x_slider()) { index = 4; } else if (fs == bw->cp2_y_slider()) { index = 5; } if (index != -1) { process_slider(fs, index, fs->get_value()); } break; } } } template void NodeParamViewWidgetBridge::create_sliders(int count, QWidget *parent) { for (int i = 0; i < count; i++) { T *fs = new T(parent); fs->SliderBase::set_default_value( split_default_value_for_track(get_inner_input(), i)); fs->set_ladder_element_count(2); // HACK: Force some spacing between sliders fs->setContentsMargins( 0, 0, QtUtils::q_font_metrics_width(fs->fontMetrics(), QStringLiteral(" ")), 0); widgets_.append(fs); connect(fs, &T::value_changed, this, &NodeParamViewWidgetBridge::widget_callback); } } void NodeParamViewWidgetBridge::update_widget_values() { if (get_inner_input().is_array() && get_inner_input().element() == -1) { return; } Rational node_time; if (get_inner_input().is_keyframing()) { node_time = get_current_time_as_node_time(); } // We assume the first data type is the "primary" type switch (get_data_type()) { // None of these inputs have applicable UI widgets case NodeValueType::k_none: case NodeValueType::k_texture: case NodeValueType::k_matrix: case NodeValueType::k_samples: case NodeValueType::k_video_params: case NodeValueType::k_audio_params: case NodeValueType::k_push_button: case NodeValueType::k_subtitle_params: case NodeValueType::k_data_type_count: break; case NodeValueType::k_binary: { NodeParamViewTextEdit *e = static_cast(widgets_.first()); QByteArray bytes = GetInputValueAtTime(get_inner_input(), node_time).toByteArray(); e->setTextPreservingCursor(QString::fromUtf8(bytes.toBase64())); break; } case NodeValueType::k_int: { static_cast(widgets_.first()) ->set_value(GetInputValueAtTime(get_inner_input(), node_time).toLongLong()); break; } case NodeValueType::k_float: { static_cast(widgets_.first()) ->set_value(GetInputValueAtTime(get_inner_input(), node_time).toDouble()); break; } case NodeValueType::k_rational: { static_cast(widgets_.first()) ->set_value( GetInputValueAtTime(get_inner_input(), node_time).value()); break; } case NodeValueType::k_vec2: { QVector2D vec2 = GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec2.x())); static_cast(widgets_.at(1)) ->set_value(static_cast(vec2.y())); break; } case NodeValueType::k_vec3: { QVector3D vec3 = GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec3.x())); static_cast(widgets_.at(1)) ->set_value(static_cast(vec3.y())); static_cast(widgets_.at(2)) ->set_value(static_cast(vec3.z())); break; } case NodeValueType::k_vec4: { QVector4D vec4 = GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(vec4.x())); static_cast(widgets_.at(1)) ->set_value(static_cast(vec4.y())); static_cast(widgets_.at(2)) ->set_value(static_cast(vec4.z())); static_cast(widgets_.at(3)) ->set_value(static_cast(vec4.w())); break; } case NodeValueType::k_file: { FileField *ff = static_cast(widgets_.first()); ff->set_filename(GetInputValueAtTime(get_inner_input(), node_time).toString()); break; } case NodeValueType::k_color: { if (input_property_string(get_inner_input(), "color_semantic") == QStringLiteral("scalar")) { Color c = GetInputValueAtTime(get_inner_input(), node_time).value(); static_cast(widgets_.at(0)) ->set_value(static_cast(c.red())); static_cast(widgets_.at(1)) ->set_value(static_cast(c.green())); static_cast(widgets_.at(2)) ->set_value(static_cast(c.blue())); static_cast(widgets_.at(3)) ->set_value(static_cast(c.alpha())); } else { ManagedColor mc = GetInputValueAtTime(get_inner_input(), node_time).value(); mc.set_color_input( input_property_string(get_inner_input(), "col_input")); QString d = input_property_string(get_inner_input(), "col_display"); QString v = input_property_string(get_inner_input(), "col_view"); QString l = input_property_string(get_inner_input(), "col_look"); mc.set_color_output(oak::ColorTransform(d, v, l)); static_cast(widgets_.first())->set_color(mc); } break; } case NodeValueType::k_text: { NodeParamViewTextEdit *e = static_cast(widgets_.first()); e->setTextPreservingCursor( GetInputValueAtTime(get_inner_input(), node_time).toString()); break; } case NodeValueType::k_boolean: static_cast(widgets_.first()) ->setChecked(GetInputValueAtTime(get_inner_input(), node_time).toBool()); break; case NodeValueType::k_font: { QFontComboBox *fc = static_cast(widgets_.first()); fc->blockSignals(true); fc->setCurrentFont( GetInputValueAtTime(get_inner_input(), node_time).toString()); fc->blockSignals(false); break; } case NodeValueType::k_combo: { QComboBox *cb = static_cast(widgets_.first()); cb->blockSignals(true); int index = GetInputValueAtTime(get_inner_input(), node_time).toInt(); for (int i = 0; i < cb->count(); i++) { if (cb->itemData(i).toInt() == index) { cb->setCurrentIndex(i); } } cb->blockSignals(false); break; } case NodeValueType::k_str_combo: { QComboBox *cb = static_cast(widgets_.first()); cb->blockSignals(true); const QString current = GetInputValueAtTime(get_inner_input(), node_time).toString(); for (int i = 0; i < cb->count(); ++i) { const QVariant data = cb->itemData(i); if ((data.isValid() && data.toString() == current) || (!data.isValid() && cb->itemText(i) == current)) { cb->setCurrentIndex(i); break; } } cb->blockSignals(false); break; } case NodeValueType::k_bezier: { BezierWidget *bw = static_cast(widgets_.first()); bw->set_value(GetInputValueAtTime(get_inner_input(), node_time).value()); break; } } } Rational NodeParamViewWidgetBridge::get_current_time_as_node_time() const { if (get_time_target()) { // ViewerOutput::get_playhead() via the C ABI int64_t pn = 0, pd = 1; oakengine_viewer_get_playhead(get_time_target(), &pn, &pd); return get_adjusted_time( get_time_target(), get_inner_input().node_handle(), Rational(pn, pd), k_transform_towards_input); } else { return 0; } } QString NodeParamViewWidgetBridge::get_command_name() const { const oak::Input i = get_inner_input(); return tr("Edited Value Of %1 - %2") .arg(i.node().label_and_name(), i.name()); } void NodeParamViewWidgetBridge::set_timebase(const Rational &timebase) { if (get_data_type() == NodeValueType::k_rational) { static_cast(widgets_.first())->set_timebase(timebase); } } void NodeParamViewWidgetBridge::TimeTargetDisconnectEvent(OakEngineNode *v) { disconnect(viewer_conn_); } void NodeParamViewWidgetBridge::TimeTargetConnectEvent(OakEngineNode *v) { viewer_conn_ = connect( PlaybackController::instance(), &PlaybackController::playhead_changed, this, [this, v](OakEngineNode *n, const core::Rational &) { if (n == v) { update_widget_values(); } }); } void NodeParamViewWidgetBridge::input_value_changed(OakEngineNode *source, const QString &input, int element, qint64 in_ts, qint64 out_ts) { const oak::Input ni(source, input, element); if (get_time_target() && get_inner_input() == ni && !oakengine_dragger_is_started(dragger_)) { int64_t pn = 0, pd = 1; oakengine_viewer_get_playhead( reinterpret_cast(get_time_target()), &pn, &pd); int64_t playhead_ts = node_time_to_ts(source, Rational(pn, pd)); if (in_ts <= playhead_ts && out_ts >= playhead_ts) { update_widget_values(); } } } void NodeParamViewWidgetBridge::set_property(const QString &key, const QVariant &value) { NodeValueType::Type data_type = get_data_type(); // Parameters for all types bool key_is_disable = key.startsWith(QStringLiteral("disable")); if (key_is_disable || key.startsWith(QStringLiteral("enabled"))) { bool e = value.toBool(); if (key_is_disable) { e = !e; } if (key.size() == 7) { // just the word "disable" or "enabled" for (int i = 0; i < widgets_.size(); i++) { widgets_.at(i)->setEnabled(e); } } else { // set specific track/widget bool ok; int element = key.mid(7).toInt(&ok); int tracks = oakengine_node_value_keyframe_track_count(node_value_type_to_c(data_type)); if (ok && element >= 0 && element < tracks) { widgets_.at(element)->setEnabled(e); } } } if (key == QStringLiteral("tooltip")) { for (int i = 0; i < widgets_.size(); i++) { widgets_.at(i)->setToolTip(value.toString()); } } // Parameters for integers, floats, and vectors if (type_is_numeric(data_type) || type_is_vector(data_type)) { if (key == QStringLiteral("min")) { switch (data_type) { case NodeValueType::k_int: static_cast(widgets_.first()) ->set_minimum(value.value()); break; case NodeValueType::k_float: static_cast(widgets_.first()) ->set_minimum(value.toDouble()); break; case NodeValueType::k_rational: static_cast(widgets_.first()) ->set_minimum(value.value()); break; case NodeValueType::k_vec2: { QVector2D min = value.value(); static_cast(widgets_.at(0))->set_minimum(min.x()); static_cast(widgets_.at(1))->set_minimum(min.y()); break; } case NodeValueType::k_vec3: { QVector3D min = value.value(); static_cast(widgets_.at(0))->set_minimum(min.x()); static_cast(widgets_.at(1))->set_minimum(min.y()); static_cast(widgets_.at(2))->set_minimum(min.z()); break; } case NodeValueType::k_vec4: { QVector4D min = value.value(); static_cast(widgets_.at(0))->set_minimum(min.x()); static_cast(widgets_.at(1))->set_minimum(min.y()); static_cast(widgets_.at(2))->set_minimum(min.z()); static_cast(widgets_.at(3))->set_minimum(min.w()); break; } default: break; } } else if (key == QStringLiteral("max")) { switch (data_type) { case NodeValueType::k_int: static_cast(widgets_.first()) ->set_maximum(value.value()); break; case NodeValueType::k_float: static_cast(widgets_.first()) ->set_maximum(value.toDouble()); break; case NodeValueType::k_rational: static_cast(widgets_.first()) ->set_maximum(value.value()); break; case NodeValueType::k_vec2: { QVector2D max = value.value(); static_cast(widgets_.at(0))->set_maximum(max.x()); static_cast(widgets_.at(1))->set_maximum(max.y()); break; } case NodeValueType::k_vec3: { QVector3D max = value.value(); static_cast(widgets_.at(0))->set_maximum(max.x()); static_cast(widgets_.at(1))->set_maximum(max.y()); static_cast(widgets_.at(2))->set_maximum(max.z()); break; } case NodeValueType::k_vec4: { QVector4D max = value.value(); static_cast(widgets_.at(0))->set_maximum(max.x()); static_cast(widgets_.at(1))->set_maximum(max.y()); static_cast(widgets_.at(2))->set_maximum(max.z()); static_cast(widgets_.at(3))->set_maximum(max.w()); break; } default: break; } } else if (key == QStringLiteral("offset")) { const int c_type = node_value_type_to_c(data_type); int tracks = oakengine_node_value_keyframe_track_count(c_type); oak_node_value normal; QVector track_vals(tracks); if (QVariantToOakNodeValue(data_type, value, &normal) && oakengine_node_value_split_to_tracks( c_type, &normal, track_vals.data(), tracks) == OAKENGINE_OK) { for (int i = 0; i < tracks; i++) { static_cast(widgets_.at(i)) ->set_offset(track_vals.at(i).f[0]); } } update_widget_values(); } else if (key.startsWith(QStringLiteral("color"))) { QColor c(value.toString()); int tracks = oakengine_node_value_keyframe_track_count(node_value_type_to_c(data_type)); if (key.size() == 5) { // Set for all tracks for (int i = 0; i < tracks; i++) { static_cast(widgets_.at(i))->set_color(c); } } else { bool ok; int element = key.mid(5).toInt(&ok); if (ok && element >= 0 && element < tracks) { static_cast(widgets_.at(element))->set_color(c); } } } else if (key == QStringLiteral("base")) { double d = value.toDouble(); for (int i = 0; i < widgets_.size(); i++) { static_cast(widgets_.at(i)) ->set_drag_multiplier(d); } } } // ComboBox strings changing if (data_type == NodeValueType::k_combo || data_type == NodeValueType::k_str_combo) { if (key == QStringLiteral("combo_str")) { QComboBox *cb = static_cast(widgets_.first()); int old_index = cb->currentIndex(); // Block the combobox changed signals since we anticipate the index will be the same and not require a re-render cb->blockSignals(true); cb->clear(); QStringList items = value.toStringList(); QStringList values = input_property_string_list(get_inner_input(), "combo_value_str"); const bool use_value_data = (data_type == NodeValueType::k_str_combo) && !values.isEmpty(); int index = 0; for (int i = 0; i < items.size(); ++i) { const QString &s = items.at(i); if (s.isEmpty()) { cb->insertSeparator(cb->count()); cb->setItemData(cb->count() - 1, -1); } else { if (use_value_data && i < values.size()) { cb->addItem(s, values.at(i)); } else { cb->addItem(s, index); index++; } } } cb->setCurrentIndex(old_index); cb->blockSignals(false); // In case the amount of items is LESS and the previous index cannot be set, NOW we trigger a re-cache since the // value has changed if (cb->currentIndex() != old_index) { widget_callback(); } } } // Parameters for floats and vectors only if (data_type == NodeValueType::k_float || type_is_vector(data_type)) { if (key == QStringLiteral("view")) { FloatSlider::DisplayType display_type = static_cast(value.toInt()); foreach (QWidget *w, widgets_) { static_cast(w)->set_display_type(display_type); } } else if (key == QStringLiteral("decimalplaces")) { int dec_places = value.toInt(); foreach (QWidget *w, widgets_) { static_cast(w)->set_decimal_places(dec_places); } } else if (key == QStringLiteral("autotrim")) { bool autotrim = value.toBool(); foreach (QWidget *w, widgets_) { static_cast(w)->set_auto_trim_decimal_places( autotrim); } } } if (data_type == NodeValueType::k_rational) { if (key == QStringLiteral("view")) { RationalSlider::DisplayType display_type = static_cast(value.toInt()); foreach (QWidget *w, widgets_) { static_cast(w)->set_display_type(display_type); } } else if (key == QStringLiteral("viewlock")) { bool locked = value.toBool(); foreach (QWidget *w, widgets_) { static_cast(w)->set_lock_display_type(locked); } } } // Parameters for files if (data_type == NodeValueType::k_file) { FileField *ff = static_cast(widgets_.first()); if (key == QStringLiteral("placeholder")) { ff->set_placeholder(value.toString()); } else if (key == QStringLiteral("directory")) { ff->set_directory_mode(value.toBool()); } else if (key == QStringLiteral("filter")) { ff->set_name_filter(value.toString()); } else if (key == QStringLiteral("lut_library") && value.toBool()) { // Offer the global LUT library directories as sidebar shortcuts in // the browse dialog QList sidebar_urls; { int dir_count = oakengine_lut_directory_count(); for (int i = 0; i < dir_count; i++) { char buf[4096]; int len = oakengine_lut_directory_at(i, buf, sizeof(buf)); if (len > 0) { sidebar_urls.append( QUrl::fromLocalFile( QString::fromUtf8(buf, len))); } } } if (!sidebar_urls.isEmpty()) { ff->set_sidebar_urls(sidebar_urls); } } } // Parameters for text if (data_type == NodeValueType::k_text) { NodeParamViewTextEdit *tex = static_cast(widgets_.first()); if (key == QStringLiteral("vieweronly")) { tex->set_edit_in_viewer_only_mode(value.toBool()); } } } void NodeParamViewWidgetBridge::input_data_type_changed(OakEngineNode *source, const QString &input) { if (source == get_outer_input().node_handle() && input == get_outer_input().input_id()) { qDeleteAll(widgets_); widgets_.clear(); create_widgets(); emit widgets_recreated(get_outer_input()); } } void NodeParamViewWidgetBridge::property_changed(const QString &input) { bool found = false; for (auto it = input_hierarchy_.cbegin(); it != input_hierarchy_.cend(); it++) { if (it->input_id() == input) { found = true; break; } } if (found) { update_properties(); } } void NodeParamViewWidgetBridge::update_properties() { // Set properties from the last entry (the innermost input) to the first (the outermost) for (auto it = input_hierarchy_.crbegin(); it != input_hierarchy_.crend(); it++) { OakEngineNode *node = it->node_handle(); const QByteArray id = it->input_id().toUtf8(); // Node::get_input_properties() (a QVariantHash) has no facade // equivalent (WRAPPER-GAP); enumerate the keys and rebuild each value // through the typed getters (read_input_property()). const int count = oakengine_node_input_get_property_count(node, id.constData()); for (int i = 0; i < count; i++) { char key_buf[256]; if (oakengine_node_input_get_property_key(node, id.constData(), i, key_buf, sizeof(key_buf)) < 0) { continue; } const QString key = QString::fromUtf8(key_buf); set_property(key, read_input_property( *it, key, static_cast(it->data_type()))); } } } bool NodeParamViewScrollBlocker::eventFilter(QObject *watched, QEvent *event) { Q_UNUSED(watched) if (event->type() == QEvent::Wheel) { // Block this event return true; } return false; } }