/*** Olive - Non-Linear Video Editor Copyright (C) 2021 Olive Team 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 "nodeparamview.h" #include #include #include #include #include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "node/output/viewer/viewer.h" #include "widget/nodeview/nodeviewundo.h" #include "widget/timeruler/timeruler.h" namespace olive { #define super TimeBasedWidget NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : super(true, false, parent), last_scroll_val_(0), focused_node_(nullptr), time_target_(nullptr), show_all_nodes_(true) { // Create horizontal layout to place scroll area in (and keyframe editing eventually) QHBoxLayout* layout = new QHBoxLayout(this); layout->setSpacing(0); layout->setMargin(0); QSplitter* splitter = new QSplitter(Qt::Horizontal); layout->addWidget(splitter); // Set up scroll area for params param_scroll_area_ = new QScrollArea(); param_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); param_scroll_area_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); param_scroll_area_->setWidgetResizable(true); splitter->addWidget(param_scroll_area_); // Param widget param_widget_container_ = new QWidget(); param_scroll_area_->setWidget(param_widget_container_); param_widget_area_ = new NodeParamViewDockArea(); QVBoxLayout* param_widget_container_layout = new QVBoxLayout(param_widget_container_); QMargins param_widget_margin = param_widget_container_layout->contentsMargins(); param_widget_margin.setTop(ruler()->height()); param_widget_container_layout->setContentsMargins(param_widget_margin); param_widget_container_layout->setSpacing(0); param_widget_container_layout->addWidget(param_widget_area_); param_widget_container_layout->addStretch(INT_MAX); // Create contexts for three different types context_items_.resize(Track::kCount + 1); for (int i=0; isetVisible(false); connect(c, &NodeParamViewContext::AboutToDeleteItem, this, &NodeParamView::ItemAboutToBeRemoved, Qt::DirectConnection); NodeParamViewItemTitleBar *title_bar = static_cast(c->titleBarWidget()); if (i == Track::kVideo || i == Track::kAudio) { title_bar->SetAddEffectButtonVisible(true); title_bar->SetText(tr("%1 Nodes").arg(Footage::GetStreamTypeName(static_cast(i)))); } else { title_bar->SetText(tr("Other")); } context_items_[i] = c; param_widget_area_->AddItem(c); } // Disable collapsing param view (but collapsing keyframe view is permitted) splitter->setCollapsible(0, false); // Create global vertical scrollbar on the right vertical_scrollbar_ = new QScrollBar(); vertical_scrollbar_->setMaximum(0); layout->addWidget(vertical_scrollbar_); // Connect scrollbars together connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::rangeChanged, vertical_scrollbar_, &QScrollBar::setRange); connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::rangeChanged, this, &NodeParamView::UpdateGlobalScrollBar); connect(vertical_scrollbar_, &QScrollBar::valueChanged, param_scroll_area_->verticalScrollBar(), &QScrollBar::setValue); if (create_keyframe_view) { // Set up keyframe view QWidget* keyframe_area = new QWidget(); QVBoxLayout* keyframe_area_layout = new QVBoxLayout(keyframe_area); keyframe_area_layout->setSpacing(0); keyframe_area_layout->setMargin(0); // Create ruler object keyframe_area_layout->addWidget(ruler()); // Create keyframe view keyframe_view_ = new KeyframeView(); keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); keyframe_view_->SetSnapService(this); ConnectTimelineView(keyframe_view_); keyframe_area_layout->addWidget(keyframe_view_); // Connect ruler and keyframe view together connect(ruler(), &TimeRuler::TimeChanged, keyframe_view_, &KeyframeView::SetTime); connect(keyframe_view_, &KeyframeView::TimeChanged, ruler(), &TimeRuler::SetTime); connect(keyframe_view_, &KeyframeView::TimeChanged, this, &NodeParamView::SetTime); connect(keyframe_view_, &KeyframeView::Dragged, this, &NodeParamView::KeyframeViewDragged); // Connect keyframe view scaling to this connect(keyframe_view_, &KeyframeView::ScaleChanged, this, &NodeParamView::SetScale); splitter->addWidget(keyframe_area); // Set both widgets to 50/50 splitter->setSizes({INT_MAX, INT_MAX}); connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, param_scroll_area_->verticalScrollBar(), &QScrollBar::setValue); connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); connect(vertical_scrollbar_, &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); // TimeBasedWidget's scrollbar has extra functionality that we can take advantage of keyframe_view_->setHorizontalScrollBar(scrollbar()); keyframe_view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); connect(keyframe_view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); } else { keyframe_view_ = nullptr; } // Set a default scale - FIXME: Hardcoded SetScale(120); // Pickup on widget focus changes connect(qApp, &QApplication::focusChanged, this, &NodeParamView::FocusChanged); } NodeParamView::~NodeParamView() { qDeleteAll(context_items_); } void NodeParamView::CloseContextsBelongingToProject(Project *p) { QVector new_contexts = contexts_; for (int i=0; iproject() == p) { new_contexts.removeAt(i); i--; } } SetContexts(new_contexts); } /*void NodeParamView::SelectNodes(const QVector &nodes) { return; int original_node_count = items_.size(); foreach (Node* n, nodes) { // If we've already added this node (either a duplicate or a pinned node), don't add another if (items_.contains(n)) { continue; } // Add to "active" list to represent currently selected node active_nodes_.append(n); // Create node UI AddNode(n, param_widget_area_); } if (items_.size() > original_node_count ) { UpdateItemTime(GetTime()); // Re-arrange keyframes QueueKeyframePositionUpdate(); SignalNodeOrder(); } } void NodeParamView::DeselectNodes(const QVector &nodes) { return; // Remove item from map and delete the widget int original_node_count = items_.size(); foreach (Node* n, nodes) { // Filter out duplicates if (!items_.contains(n)) { continue; } if (!pinned_nodes_.contains(n)) { // Store expanded state node_expanded_state_.insert(n, items_.value(n)->IsExpanded()); // Remove all keyframes from this node RemoveNode(n); } active_nodes_.removeOne(n); } if (items_.size() < original_node_count) { // Re-arrange keyframes QueueKeyframePositionUpdate(); SignalNodeOrder(); } }*/ void NodeParamView::UpdateContexts() { //TIME_THIS_FUNCTION; bool changes_made = false; foreach (Node *ctx, current_contexts_) { if (!contexts_.contains(ctx)) { // Context is being removed RemoveContext(ctx); changes_made = true; } } foreach (Node *ctx, contexts_) { if (!current_contexts_.contains(ctx)) { // Context is being added AddContext(ctx); changes_made = true; } } if (changes_made) { current_contexts_ = contexts_; if (IsGroupMode()) { // Check inputs that have been passed through NodeGroup *group = static_cast(contexts_.first()); for (auto it=group->GetInputPassthroughs().cbegin(); it!=group->GetInputPassthroughs().cend(); it++) { GroupInputPassthroughAdded(group, it->second); } connect(group, &NodeGroup::InputPassthroughAdded, this, &NodeParamView::GroupInputPassthroughAdded); connect(group, &NodeGroup::InputPassthroughRemoved, this, &NodeParamView::GroupInputPassthroughRemoved); } foreach (NodeParamViewContext *ctx, context_items_) { SortItemsInContext(ctx); } if (keyframe_view_) { QueueKeyframePositionUpdate(); } } } void NodeParamView::ItemAboutToBeRemoved(NodeParamViewItem *item) { if (keyframe_view_) { for (auto it=item->GetKeyframeConnections().begin(); it!=item->GetKeyframeConnections().end(); it++) { for (auto jt=it->begin(); jt!=it->end(); jt++) { for (auto kt=jt->begin(); kt!=jt->end(); kt++) { keyframe_view_->RemoveKeyframesOfTrack(*kt); } } } } if (focused_node_ == item) { focused_node_ = nullptr; emit FocusedNodeChanged(nullptr); } } void NodeParamView::SetContexts(const QVector &contexts) { // Setting contexts is expensive, so we queue it here to prevent multiple calls in a short timespan contexts_ = contexts; UpdateContexts(); } void NodeParamView::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); vertical_scrollbar_->setPageStep(vertical_scrollbar_->height()); } void NodeParamView::ScaleChangedEvent(const double &scale) { super::ScaleChangedEvent(scale); if (keyframe_view_) { keyframe_view_->SetScale(scale); } } void NodeParamView::TimebaseChangedEvent(const rational &timebase) { super::TimebaseChangedEvent(timebase); if (keyframe_view_) { keyframe_view_->SetTimebase(timebase); } foreach (NodeParamViewContext* ctx, context_items_) { ctx->SetTimebase(timebase); } UpdateItemTime(GetTime()); } void NodeParamView::TimeChangedEvent(const rational &time) { super::TimeChangedEvent(time); if (keyframe_view_) { keyframe_view_->SetTime(time); } UpdateItemTime(time); } void NodeParamView::ConnectedNodeChangeEvent(ViewerOutput *n) { if (keyframe_view_) { // Set viewer as a time target keyframe_view_->SetTimeTarget(n); } foreach (NodeParamViewContext* item, context_items_) { item->SetTimeTarget(n); } time_target_ = n; } Node *NodeParamView::GetTimeTarget() const { return time_target_; } void NodeParamView::DeleteSelected() { if (keyframe_view_ && keyframe_view_->hasFocus()) { keyframe_view_->DeleteSelected(); } else if (focused_node_) { MultiUndoCommand *c = new MultiUndoCommand(); Node *n = focused_node_->GetNode(); // Create command to delete node from context and/or graph NodeViewDeleteCommand *dc = new NodeViewDeleteCommand(); dc->AddNode(n, focused_node_->GetContext()); c->add_child(dc); // Copy any outputs that were connected if (n->GetEffectInput().IsValid()) { if (Node *out = n->GetEffectInput().GetConnectedOutput()) { for (auto it=n->output_connections().cbegin(); it!=n->output_connections().cend(); it++) { c->add_child(new NodeEdgeAddCommand(out, it->second)); } } } Core::instance()->undo_stack()->push(c); } } void NodeParamView::SelectNodes(const QVector &nodes) { // Do nothing, this is a placeholder if we ever need this to do anything in the future } void NodeParamView::DeselectNodes(const QVector &nodes) { // Do nothing, this is a placeholder if we ever need this to do anything in the future } void NodeParamView::UpdateItemTime(const rational &time) { foreach (NodeParamViewContext* item, context_items_) { item->SetTime(time); } } void NodeParamView::QueueKeyframePositionUpdate() { QMetaObject::invokeMethod(this, &NodeParamView::UpdateElementY, Qt::QueuedConnection); } void NodeParamView::AddContext(Node *ctx) { // Queued so that if any further work is done in connecting this node to the context, it'll be // done before our sorting function is called connect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext, Qt::QueuedConnection); connect(ctx, &Node::NodeRemovedFromContext, this, &NodeParamView::NodeRemovedFromContext, Qt::QueuedConnection); NodeParamViewContext *item = GetContextItemFromContext(ctx); item->AddContext(ctx); item->setVisible(true); for (auto it=ctx->GetContextPositions().cbegin(); it!=ctx->GetContextPositions().cend(); it++) { AddNode(it.key(), ctx, item); } } void NodeParamView::RemoveContext(Node *ctx) { disconnect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext); disconnect(ctx, &Node::NodeRemovedFromContext, this, &NodeParamView::NodeRemovedFromContext); foreach (NodeParamViewContext *item, context_items_) { item->RemoveContext(ctx); item->RemoveNodesWithContext(ctx); if (item->GetContexts().isEmpty()) { item->setVisible(false); } } } void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context) { if ((n->GetFlags() & Node::kDontShowInParamView) && !IsGroupMode() && !show_all_nodes_) { return; } NodeParamViewItem* item = new NodeParamViewItem(n, IsGroupMode() ? kCheckBoxesOnNonConnected : kNoCheckBoxes, context); connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); connect(item, &NodeParamViewItem::InputCheckedChanged, this, &NodeParamView::InputCheckBoxChanged); item->SetContext(ctx); item->SetTimeTarget(GetTimeTarget()); item->SetTimebase(timebase()); item->SetTime(GetTime()); context->AddNode(item); if (!focused_node_ && n->HasGizmos()) { // We'll focus this node now item->SetHighlighted(true); focused_node_ = item; emit FocusedNodeChanged(n); } if (keyframe_view_) { connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate); item->SetKeyframeConnections(keyframe_view_->AddKeyframesOfNode(n)); } } int GetDistanceBetweenNodes(Node *start, Node *end) { if (start == end) { return 0; } for (auto it=start->input_connections().cbegin(); it!=start->input_connections().cend(); it++) { int this_node_dist = GetDistanceBetweenNodes(it->second, end); if (this_node_dist != -1) { return 1 + this_node_dist; } } return -1; } void NodeParamView::SortItemsInContext(NodeParamViewContext *context_item) { QVector > distances; for (auto it=context_item->GetItems().cbegin(); it!=context_item->GetItems().cend(); it++) { NodeParamViewItem *item = *it; int distance = -1; foreach (Node *ctx, context_item->GetContexts()) { distance = qMax(distance, GetDistanceBetweenNodes(ctx, item->GetNode())); } if (distance == -1) { distance = INT_MAX; } bool inserted = false; QPair dist(item, distance); for (int i=0; iGetDockArea()->AddItem(info.first); } } NodeParamViewContext *NodeParamView::GetContextItemFromContext(Node *ctx) { Track::Type ctx_type = Track::kCount; if (ClipBlock *clip = dynamic_cast(ctx)) { if (clip->track()) { if (clip->track()->type() != Track::kNone) { ctx_type = clip->track()->type(); } } } else if (Track *track = dynamic_cast(ctx)) { if (track->type() != Track::kNone) { ctx_type = track->type(); } } return context_items_.at(ctx_type); } void NodeParamView::UpdateGlobalScrollBar() { if (keyframe_view_) { keyframe_view_->SetMaxScroll(param_widget_container_->height() - ruler()->height()); } } void NodeParamView::PinNode(bool pin) { NodeParamViewItem* item = static_cast(sender()); Node* node = item->GetNode(); if (pin) { pinned_nodes_.append(node); } else { pinned_nodes_.removeOne(node); if (!active_nodes_.contains(node)) { //RemoveNode(node); } } } void NodeParamView::FocusChanged(QWidget* old, QWidget* now) { Q_UNUSED(old) QObject* parent = now; while (parent) { if (NodeParamViewItem* item = dynamic_cast(parent)) { if (item != focused_node_) { // Found a NodeParamViewItem that isn't already focused, see if it belongs to us bool ours = false; do { parent = parent->parent(); if (parent == this) { ours = true; break; } } while (parent); if (ours) { // This item is ours, if (focused_node_) { // De-focus current node focused_node_->SetHighlighted(false); } focused_node_ = item; item->SetHighlighted(true); emit FocusedNodeChanged(item->GetNode()); } } break; } parent = parent->parent(); } } void NodeParamView::KeyframeViewDragged(int x, int y) { Q_UNUSED(y) QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection, Q_ARG(int, x)); } void NodeParamView::UpdateElementY() { foreach (NodeParamViewContext *ctx, context_items_) { for (auto it=ctx->GetItems().cbegin(); it!=ctx->GetItems().cend(); it++) { NodeParamViewItem *item = *it; Node *node = item->GetNode(); const KeyframeView::NodeConnections &connections = item->GetKeyframeConnections(); if (!connections.isEmpty()) { foreach (const QString& input, node->inputs()) { if (!(node->GetInputFlags(input) & kInputFlagHidden)) { int arr_sz = NodeGroup::ResolveInput(NodeInput(node, input)).GetArraySize(); for (int i=-1; iGetElementY(ic); // For some reason Qt's mapToGlobal doesn't seem to handle this, so we offset here y += vertical_scrollbar_->value(); const KeyframeView::InputConnections &input_con = connections.value(input); int use_index = i + 1; if (use_index < input_con.size()) { const KeyframeView::ElementConnections &ele_con = input_con.at(ic.element()+1); foreach (KeyframeViewInputConnection *track, ele_con) { track->SetKeyframeY(y); } } } } } } } } } void NodeParamView::NodeAddedToContext(Node *n) { Node *ctx = static_cast(sender()); NodeParamViewContext *item = GetContextItemFromContext(ctx); AddNode(n, ctx, item); SortItemsInContext(item); if (keyframe_view_) { QueueKeyframePositionUpdate(); } } void NodeParamView::NodeRemovedFromContext(Node *n) { Node *ctx = static_cast(sender()); foreach (NodeParamViewContext *ctx_item, context_items_) { ctx_item->RemoveNode(n, ctx); } if (keyframe_view_) { QueueKeyframePositionUpdate(); } } void NodeParamView::InputCheckBoxChanged(const NodeInput &input, bool e) { NodeGroup *group = static_cast(contexts_.first()); if (e) { group->AddInputPassthrough(input); } else { group->RemoveInputPassthrough(input); } } void NodeParamView::GroupInputPassthroughAdded(NodeGroup *group, const NodeInput &input) { foreach (NodeParamViewContext *pvctx, context_items_) { pvctx->SetInputChecked(input, true); } } void NodeParamView::GroupInputPassthroughRemoved(NodeGroup *group, const NodeInput &input) { foreach (NodeParamViewContext *pvctx, context_items_) { pvctx->SetInputChecked(input, false); } } }