/* * Olive Community Edition - Non-Linear Video Editor * Copyright (C) 2025 Olive CE 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 "nodeviewitem.h" #include #include #include #include #include #include "common/qtutils.h" #include "config/config.h" #include "core.h" #include "node/nodeundo.h" #include "pluginSupport/OlivePluginInstance.h" #include "nodeview.h" #include "nodeviewscene.h" #include "ui/colorcoding.h" #include "ui/icons/icons.h" #include "window/mainwindow/mainwindow.h" namespace olive { NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, Node *context, QGraphicsItem *parent) : QGraphicsRectItem(parent) , node_(node) , input_(input) , element_(element) , context_(context) , expanded_(false) , highlighted_(false) , flow_dir_(NodeViewCommon::kInvalidDirection) , arrow_click_(false) , label_as_output_(false) { // // We use font metrics to set all the UI measurements for DPI-awareness // // Set border width node_border_width_ = DefaultItemBorder(); // Set rect size to default SetRectSize(); // Create connector input_connector_ = new NodeViewItemConnector(false, this); output_connector_ = new NodeViewItemConnector(true, this); connect(node_, &Node::LabelChanged, this, &NodeViewItem::NodeAppearanceChanged); connect(node_, &Node::ColorChanged, this, &NodeViewItem::NodeAppearanceChanged); connect(node_, &Node::MessageCountChanged, this, &NodeViewItem::NodeAppearanceChanged); if (IsOutputItem()) { connect(node_, &Node::InputAdded, this, &NodeViewItem::RepopulateInputs); connect(node_, &Node::InputRemoved, this, &NodeViewItem::RepopulateInputs); RepopulateInputs(); // Set flags for this widget setFlag(QGraphicsItem::ItemSendsGeometryChanges); setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemIsSelectable); if (context_) { SetNodePosition(context_->GetNodePositionDataInContext(node_)); } } else { output_connector_->setVisible(false); connect(node_, &Node::InputArraySizeChanged, this, &NodeViewItem::InputArraySizeChanged); connect(node_, &Node::InputArraySizeChanged, this, &NodeViewItem::InputArraySizeChanged); } // This should be set during runtime, but just in case here's a default fallback SetFlowDirection(NodeViewCommon::kLeftToRight); } NodeViewItem::~NodeViewItem() { Q_ASSERT(edges_.isEmpty()); } Node::Position NodeViewItem::GetNodePositionData() const { return Node::Position(GetNodePosition(), IsExpanded()); } QPointF NodeViewItem::GetNodePosition() const { return ScreenToNodePoint(pos(), flow_dir_); } void NodeViewItem::SetNodePosition(const QPointF &pos) { cached_node_pos_ = pos; UpdateNodePosition(); } void NodeViewItem::SetNodePosition(const Node::Position &pos) { SetNodePosition(pos.position); SetExpanded(pos.expanded); } QVector NodeViewItem::GetAllEdgesRecursively() const { QVector list = edges_; foreach (NodeViewItem *item, children_) { list.append(item->GetAllEdgesRecursively()); } return list; } int NodeViewItem::DefaultTextPadding() { return QFontMetrics(QFont()).height() / 4; } int NodeViewItem::DefaultItemHeight() { return QFontMetrics(QFont()).height() + DefaultTextPadding() * 2; } int NodeViewItem::DefaultItemWidth() { return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHHHHHH"); ; } int NodeViewItem::DefaultItemBorder() { return QFontMetrics(QFont()).height() / 12; } QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection direction) { switch (direction) { case NodeViewCommon::kLeftToRight: // NodeGraphs are always left-to-right internally, no need to translate break; case NodeViewCommon::kRightToLeft: // Invert X value p.setX(-p.x()); break; case NodeViewCommon::kTopToBottom: // Swap X/Y p = QPointF(p.y(), p.x()); break; case NodeViewCommon::kBottomToTop: // Swap X/Y and invert Y p = QPointF(p.y(), -p.x()); break; case NodeViewCommon::kInvalidDirection: break; } // Multiply by item sizes for this direction p.setX(p.x() * DefaultItemHorizontalPadding(direction)); p.setY(p.y() * DefaultItemVerticalPadding(direction)); return p; } QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection direction) { // Divide by item sizes for this direction p.setX(p.x() / DefaultItemHorizontalPadding(direction)); p.setY(p.y() / DefaultItemVerticalPadding(direction)); switch (direction) { case NodeViewCommon::kLeftToRight: // NodeGraphs are always left-to-right internally, no need to translate break; case NodeViewCommon::kRightToLeft: // Invert X value p.setX(-p.x()); break; case NodeViewCommon::kTopToBottom: // Swap X/Y p = QPointF(p.y(), p.x()); break; case NodeViewCommon::kBottomToTop: // Swap X/Y and invert Y p = QPointF(-p.y(), p.x()); break; case NodeViewCommon::kInvalidDirection: break; } return p; } qreal NodeViewItem::DefaultItemHorizontalPadding( NodeViewCommon::FlowDirection dir) { if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemWidth() * 1.5; } else { return DefaultItemWidth() * 1.25; } } qreal NodeViewItem::DefaultItemVerticalPadding(NodeViewCommon::FlowDirection dir) { if (NodeViewCommon::GetFlowOrientation(dir) == Qt::Horizontal) { return DefaultItemHeight() * 1.5; } else { return DefaultItemHeight() * 2.0; } } qreal NodeViewItem::DefaultItemHorizontalPadding() const { return DefaultItemHorizontalPadding(flow_dir_); } qreal NodeViewItem::DefaultItemVerticalPadding() const { return DefaultItemVerticalPadding(flow_dir_); } void NodeViewItem::AddEdge(NodeViewEdge *edge) { edges_.append(edge); } void NodeViewItem::RemoveEdge(NodeViewEdge *edge) { edges_.removeOne(edge); } void NodeViewItem::SetExpanded(bool e, bool hide_titlebar) { if (!CanBeExpanded() || (expanded_ == e)) { return; } expanded_ = e; if (context_) { context_->SetNodeExpandedInContext(node_, e); } if (IsOutputItem()) { // We don't have to check has_connectable_inputs_ here because we did it at the top input_connector_->setVisible(!expanded_); } if (expanded_) { node_->Retranslate(); if (IsOutputItem()) { // Create items for each input of the node foreach (const QString &input, node_->inputs()) { if (IsInputValid(input)) { NodeViewItem *item = new NodeViewItem(node_, input, -1, context_, this); children_.append(item); } } QVector edges = edges_; for (auto it = edges.cbegin(); it != edges.cend(); it++) { if ((*it)->to_item() == this) { (*it)->set_to_item(GetItemForInput((*it)->input())); } } } else { // Create items for each element of the input array int arr_sz = node_->InputArraySize(input_); children_.resize(arr_sz); for (int i = 0; i < arr_sz; i++) { NodeViewItem *item = new NodeViewItem(node_, input_, i, context_, this); children_[i] = item; } QVector edges = edges_; for (auto it = edges.cbegin(); it != edges.cend(); it++) { if ((*it)->to_item() == this) { (*it)->set_to_item(GetItemForInput((*it)->input())); } } } } else { foreach (NodeViewItem *child, children_) { QVector child_edges = child->edges(); foreach (NodeViewEdge *edge, child_edges) { edge->set_to_item(this); } delete child; } children_.clear(); } UpdateChildrenPositions(); if (flow_dir_ == NodeViewCommon::kTopToBottom) { UpdateOutputConnectorPosition(); } ReadjustAllEdges(); UpdateContextRect(); update(); } void NodeViewItem::ToggleExpanded() { SetExpanded(!IsExpanded()); } void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { // Use main window palette since the palette passed in `widget` is the NodeView palette which // has been slightly modified QPalette app_pal = Core::instance()->main_window()->palette(); // We only draw a single unit's worth QRectF single_unit_rect = rect(); single_unit_rect.setHeight(DefaultItemHeight()); if (IsOutputItem()) { // Set output item colors painter->setPen(Qt::black); painter->setBrush( node_->brush(single_unit_rect.top(), single_unit_rect.bottom())); } else { // Set input item colors painter->setPen(Qt::NoPen); painter->setBrush(element_ == -1 ? app_pal.color(QPalette::Window) : app_pal.color(QPalette::Base)); } painter->drawRect(single_unit_rect); // Draw highlight if applicable if (highlighted_) { QColor highlight_col = app_pal.color(QPalette::Text); highlight_col.setAlpha(64); painter->setBrush(highlight_col); painter->drawRect(rect()); } // Determine what text to draw and whether to draw an arrow QString node_label, node_name; if (IsOutputItem()) { if (label_as_output_) { node_name = QCoreApplication::translate("NodeViewItem", "Output"); } else { node_label = node_->GetLabel(); node_name = node_->ShortName(); } } else { if (element_ == -1) { node_name = node_->GetInputName(input_); } else { node_name = QString::number( element_ + node_->GetInputProperty(input_, QStringLiteral("arraystart")) .toInt()); } } // Draw arrow if necessary int arrow_size = CanBeExpanded() ? DrawExpandArrow(painter) : 0; if (IsOutputItem()) { // Determine the text color (automatically calculate from node background color) painter->setPen(ColorCoding::GetUISelectorColor(node_->color())); } else { // Just use text item painter->setPen(app_pal.text().color()); } if (node_label.isEmpty()) { // Draw name only DrawNodeTitle(painter, node_name, single_unit_rect, Qt::AlignVCenter, arrow_size); } else { int text_pad = DefaultTextPadding() / 2; QRectF safe_label_bounds = single_unit_rect.adjusted(text_pad, text_pad, -text_pad, -text_pad); QFont f; qreal font_sz = f.pointSizeF(); // Draw label as larger/upper text f.setPointSizeF(font_sz * 0.8); painter->setFont(f); DrawNodeTitle(painter, node_label, safe_label_bounds, Qt::AlignTop, arrow_size); // Draw node name as smaller/lower text f.setPointSizeF(font_sz * 0.6); painter->setFont(f); DrawNodeTitle(painter, node_name, safe_label_bounds, Qt::AlignBottom, arrow_size); } if (IsOutputItem()) { auto *instance = node_->getPluginInstance(); auto *olive_instance = dynamic_cast(instance); int message_count = olive_instance ? olive_instance->persistentMessageCount() : 0; if (message_count > 0) { QString badge_text = QString::number(message_count); QFont badge_font = painter->font(); badge_font.setPointSizeF(badge_font.pointSizeF() * 0.7); painter->setFont(badge_font); QFontMetrics badge_metrics(badge_font); int text_width = badge_metrics.horizontalAdvance(badge_text); int text_height = badge_metrics.height(); int pad = text_height / 3; int badge_width = qMax(text_width + pad * 2, text_height + pad); int badge_height = text_height + pad; QRectF badge_rect( single_unit_rect.right() - badge_width - 4, single_unit_rect.top() + 4, badge_width, badge_height); painter->setPen(Qt::NoPen); painter->setBrush(QColor(220, 50, 47)); painter->drawRoundedRect(badge_rect, badge_height / 2, badge_height / 2); painter->setPen(Qt::white); painter->drawText(badge_rect, Qt::AlignCenter, badge_text); } } // Draw final border (output only) if (IsOutputItem()) { QPen border_pen; border_pen.setWidth(node_border_width_); if (option->state & QStyle::State_Selected) { border_pen.setColor(app_pal.color(QPalette::Highlight)); } else { border_pen.setColor(Qt::black); } painter->setPen(border_pen); painter->setBrush(Qt::NoBrush); painter->drawRect(rect()); } } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (last_arrow_rect_.contains(event->pos().toPoint())) { arrow_click_ = true; ToggleExpanded(); return; } event->setModifiers( QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mousePressEvent(event); } void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (arrow_click_) { return; } event->setModifiers( QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseMoveEvent(event); } void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (arrow_click_) { arrow_click_ = false; return; } event->setModifiers( QtUtils::FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseReleaseEvent(event); } QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (node_) { if (change == ItemPositionHasChanged) { ReadjustAllEdges(); UpdateContextRect(); } else if (change == ItemSelectedHasChanged) { if (value.toBool()) { qDebug() << "Selected node:" << node_; } } } return QGraphicsItem::itemChange(change, value); } void NodeViewItem::ReadjustAllEdges() { foreach (NodeViewEdge *edge, edges_) { if (NodeViewItem *to_item = edge->to_item()) { static_cast(to_item->parentItem()) ->UpdateFlowDirectionOfInputItem(to_item); } edge->Adjust(); } foreach (NodeViewItem *child, children_) { child->ReadjustAllEdges(); } } void NodeViewItem::UpdateContextRect() { QGraphicsItem *item = parentItem(); while (item) { if (NodeViewContext *ctx = dynamic_cast(item)) { ctx->UpdateRect(); break; } item = item->parentItem(); } } void NodeViewItem::DrawNodeTitle(QPainter *painter, QString text, const QRectF &rect, Qt::Alignment vertical_align, int icon_full_size) { QFontMetrics fm = painter->fontMetrics(); // Calculate how much space we have for text int item_width = this->rect().width(); int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size; int label_width = QtUtils::QFontMetricsWidth(fm, text); // Concatenate text if necessary (adds a "..." to the end and removes characters until the // string fits in the bounds) if (label_width > max_text_width) { QString concatenated; do { text.chop(1); concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(text); } while ((label_width = QtUtils::QFontMetricsWidth(fm, concatenated)) > max_text_width); text = concatenated; } // Determine X position (favors horizontal centering unless it'll overrun the arrow) QRectF text_rect = rect; Qt::Alignment text_align = Qt::AlignHCenter | vertical_align; int likely_x = item_width / 2 - label_width / 2; if (likely_x < icon_full_size) { text_rect.adjust(icon_full_size, 0, 0, 0); text_align = Qt::AlignLeft | vertical_align; } // Draw the text in a rect (the rect is sized around text already in the constructor) painter->drawText(text_rect, text_align, text); } int NodeViewItem::DrawExpandArrow(QPainter *painter) { // Draw right or down arrow based on expanded state int icon_size = painter->fontMetrics().height() / 2; int icon_padding = DefaultItemHeight() / 2 - icon_size / 2; int icon_full_size = icon_size + icon_padding * 2; painter->setRenderHint(QPainter::SmoothPixmapTransform); const QIcon &expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight; int icon_size_scaled = icon_size * painter->transform().m11(); last_arrow_rect_ = QRect(this->rect().x() + icon_padding, this->rect().y() + icon_padding, icon_size, icon_size); painter->drawPixmap( last_arrow_rect_, expand_icon.pixmap(QSize(icon_size_scaled, icon_size_scaled))); return icon_full_size; } void NodeViewItem::SetLabelAsOutput(bool e) { label_as_output_ = e; output_connector_->setVisible(!e); update(); } QPointF NodeViewItem::GetInputPoint() const { return input_connector_->scenePos(); } QPointF NodeViewItem::GetOutputPoint() const { QPointF p = output_connector_->scenePos(); QRectF r = output_connector_->polygon().boundingRect(); switch (flow_dir_) { case NodeViewCommon::kLeftToRight: default: p.setX(p.x() + r.width()); break; case NodeViewCommon::kRightToLeft: p.setX(p.x() - r.width()); break; case NodeViewCommon::kTopToBottom: p.setY(p.y() + r.height()); break; case NodeViewCommon::kBottomToTop: p.setY(p.y() - r.height()); break; } return p; } void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) { if (flow_dir_ != dir) { flow_dir_ = dir; input_connector_->SetFlowDirection(dir); output_connector_->SetFlowDirection(dir); UpdateInputConnectorPosition(); UpdateOutputConnectorPosition(); if (IsOutputItem()) { UpdateNodePosition(); } ReadjustAllEdges(); } } void NodeViewItem::UpdateNodePosition() { setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); } void NodeViewItem::UpdateInputConnectorPosition() { QRectF output_rect = input_connector_->polygon().boundingRect(); NodeViewCommon::FlowDirection using_flow_dir = flow_dir_; if (IsExpanded() && !NodeViewCommon::IsFlowHorizontal(flow_dir_)) { if (edges_.isEmpty() || edges_.first()->from_item()->x() < this->x()) { using_flow_dir = NodeViewCommon::kLeftToRight; } else { using_flow_dir = NodeViewCommon::kRightToLeft; } } // Input connector flow directions change conditionally switch (using_flow_dir) { case NodeViewCommon::kLeftToRight: input_connector_->setPos(rect().left() - output_rect.width(), 0); break; case NodeViewCommon::kRightToLeft: input_connector_->setPos(rect().right() + output_rect.width(), 0); break; case NodeViewCommon::kTopToBottom: input_connector_->setPos(rect().center().x(), rect().top() - output_rect.height()); break; case NodeViewCommon::kBottomToTop: input_connector_->setPos(rect().center().x(), rect().bottom() + output_rect.height()); break; case NodeViewCommon::kInvalidDirection: break; } } void NodeViewItem::UpdateOutputConnectorPosition() { switch (flow_dir_) { case NodeViewCommon::kLeftToRight: output_connector_->setPos(rect().right(), 0); break; case NodeViewCommon::kRightToLeft: output_connector_->setPos(rect().left(), 0); break; case NodeViewCommon::kTopToBottom: output_connector_->setPos(rect().center().x(), rect().bottom()); break; case NodeViewCommon::kBottomToTop: output_connector_->setPos(rect().center().x(), rect().top()); break; case NodeViewCommon::kInvalidDirection: break; } } bool NodeViewItem::IsInputValid(const QString &input) { return node_->IsInputConnectable(input) && !node_->IsInputHidden(input); } void NodeViewItem::SetRectSize(int height_units) { // Set rect int widget_width = DefaultItemWidth(); int widget_height = DefaultItemHeight(); setRect(QRectF(-widget_width / 2, -widget_height / 2, widget_width, widget_height * height_units)); } bool NodeViewItem::CanBeExpanded() const { if (IsOutputItem()) { return has_connectable_inputs_; } else { return node_->GetInputFlags(input_) & kInputFlagArray && element_ == -1 && !node_->IsInputConnected(input_); } } void NodeViewItem::UpdateChildrenPositions() { int y = 1; int h = DefaultItemHeight(); foreach (NodeViewItem *c, children_) { c->setPos(QPointF(0, y * h)); y += c->GetLogicalHeightWithChildren(); } SetRectSize(y); if (NodeViewItem *p = dynamic_cast(parentItem())) { p->UpdateChildrenPositions(); } } int NodeViewItem::GetLogicalHeightWithChildren() const { int h = 1; foreach (NodeViewItem *c, children_) { h += c->GetLogicalHeightWithChildren(); } return h; } void NodeViewItem::UpdateFlowDirectionOfInputItem(NodeViewItem *child) { if (!child->IsOutputItem()) { if (NodeViewCommon::IsFlowVertical(flow_dir_)) { if (!child->edges().isEmpty() && child->edges().first()->from_item()->scenePos().x() > child->scenePos().x()) { child->SetFlowDirection(NodeViewCommon::kRightToLeft); } else { child->SetFlowDirection(NodeViewCommon::kLeftToRight); } } else { child->SetFlowDirection(flow_dir_); } } } void NodeViewItem::RepopulateInputs() { if (IsOutputItem()) { has_connectable_inputs_ = false; foreach (const QString &input, node_->inputs()) { if (IsInputValid(input)) { has_connectable_inputs_ = true; break; } } input_connector_->setVisible(has_connectable_inputs_); } if (IsExpanded() && (IsOutputItem() || element_ == -1)) { // Create or remove inputs when necessary // NOTE: This is not the most efficient thing in the world, but it does work SetExpanded(false); SetExpanded(true); } } void NodeViewItem::InputArraySizeChanged(const QString &input) { if (input == input_) { RepopulateInputs(); } } void NodeViewItem::NodeAppearanceChanged() { update(); } void NodeViewItem::SetHighlighted(bool e) { highlighted_ = e; update(); } NodeViewItem *NodeViewItem::GetItemForInput(NodeInput input) { if (NodeGroup *group = dynamic_cast(node_)) { if (input.node() != group) { // Translate input to group input QString id = group->GetIDOfPassthrough(input); input.set_node(group); input.set_input(id); } } if (IsExpanded()) { if (input_.isEmpty()) { // Look for the input in our children foreach (NodeViewItem *i, children_) { if (i->input_ == input.input()) { return i->GetItemForInput(input); } } } else { // Look for element in our children if (input.element() >= 0 && input.element() < children_.size()) { return children_.at(input.element())->GetItemForInput(input); } } } // Fallback to this object return this; } }