/*** Olive - Non-Linear Video Editor Copyright (C) 2019 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 "nodeviewitem.h" #include #include #include #include #include #include "common/qtversionabstraction.h" #include "core.h" #include "nodeview.h" #include "nodeviewundo.h" #include "ui/icons/icons.h" #include "window/mainwindow/mainwindow.h" NodeViewItem::NodeViewItem(QGraphicsItem *parent) : QGraphicsRectItem(parent), node_(nullptr), font_metrics(font), dragging_edge_(nullptr), drag_expanded_item_(nullptr), expanded_(false), standard_click_(false), node_edge_change_command_(nullptr) { // Set flags for this widget setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemIsSelectable); setFlag(QGraphicsItem::ItemSendsGeometryChanges); // // We use font metrics to set all the UI measurements for DPI-awareness // // Not particularly great way of using text scaling to set the width (DPI-awareness, etc.) int widget_width = QFontMetricsWidth(font_metrics, "HHHHHHHHHHHHHHHH"); // Set border width node_border_width_ = font_metrics.height() / 12; // Set default node connector size node_connector_size_ = font_metrics.height() / 3; // Set text and icon padding node_text_padding_ = font_metrics.height() / 6; // FIXME: Revise icon sizing algorithm (share with NodeParamViewItem) node_icon_padding_ = node_text_padding_ * 3; // Use the current default font height to size this widget // Set default "collapsed" size title_bar_rect_ = QRectF(0, 0, widget_width, font_metrics.height() + node_text_padding_ * 2); setRect(title_bar_rect_); } void NodeViewItem::SetNode(Node *n) { node_ = n; setPos(node_->GetPosition()); update(); } Node *NodeViewItem::node() { return node_; } bool NodeViewItem::IsExpanded() { return expanded_; } void NodeViewItem::SetExpanded(bool e) { if (expanded_ == e) { return; } expanded_ = e; QRectF new_rect; if (expanded_) { QRectF full_size_rect = title_bar_rect_; // If a node is connected, use its parameter count to set the height if (node_ != nullptr) { full_size_rect.adjust(0, 0, 0, node_text_padding_*2 + font_metrics.height() * node_->parameters().size()); } // Store content_rect (the rect without the titlebar) content_rect_ = full_size_rect.adjusted(0, title_bar_rect_.height(), 0, 0); new_rect = full_size_rect; } else { new_rect = title_bar_rect_; } update(); setRect(new_rect); } QRectF NodeViewItem::GetParameterConnectorRect(int index) { if (node_ == nullptr) { return QRectF(); } NodeParam* param = node_->parameters().at(index); QRectF connector_rect(rect().x(), content_rect_.y() + node_text_padding_ + font_metrics.height() / 2 - node_connector_size_ / 2, node_connector_size_, node_connector_size_); if (index > 0) { connector_rect.translate(0, font_metrics.height() * index); } if (param->type() == NodeParam::kOutput) { connector_rect.translate(rect().width() - node_connector_size_, 0); } return connector_rect; } QPointF NodeViewItem::GetParameterTextPoint(int index) { if (node_ == nullptr) { return QPointF(); } NodeParam* param = node_->parameters().at(index); if (param->type() == NodeParam::kOutput) { return content_rect_.topRight() + QPointF(-(node_connector_size_ + node_text_padding_), node_text_padding_ + font_metrics.ascent() + font_metrics.height()*index); } else { return content_rect_.topLeft() + QPointF(node_connector_size_ + node_text_padding_, node_text_padding_ + font_metrics.ascent() + font_metrics.height()*index); } } void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { // HACK for getting the main QWidget palette color (the `widget`'s palette uses the NodeView color instead which we // don't want here) QPalette app_pal = Core::instance()->main_window()->palette(); // Set up border, which will change color if selected QPen border_pen(css_proxy_.BorderColor(), node_border_width_); QPen text_pen(app_pal.color(QPalette::Text)); QBrush connector_brush(app_pal.color(QPalette::Text)); painter->setPen(border_pen); if (expanded_ && node_ != nullptr) { // Use main widget color for node contents painter->setBrush(app_pal.window()); // Draw background rect painter->drawRect(rect()); // Set pen to draw text painter->setPen(text_pen); // Draw text and a connector rectangle for each parameter // Store the text points which will steadily increase sa we loop // Loop through all the parameters for (int i=0;iparameters().size();i++) { NodeParam* param = node_->parameters().at(i); // If parameter is not connectable, don't draw its slot if (param->IsConnectable()) { // Draw connector square painter->fillRect(GetParameterConnectorRect(i), connector_brush); } // Draw text QPointF text_pt = GetParameterTextPoint(i); if (param->type() == NodeParam::kOutput) { text_pt -= QPointF(QFontMetricsWidth(font_metrics, param->name()), 0); } painter->drawText(text_pt, param->name()); } painter->setPen(border_pen); } // Draw rect painter->setBrush(css_proxy_.TitleBarColor()); painter->drawRect(title_bar_rect_); // If selected, draw selection outline if (option->state & QStyle::State_Selected) { QPen pen = painter->pen(); pen.setColor(app_pal.color(QPalette::Highlight)); painter->setPen(pen); painter->setBrush(Qt::transparent); painter->drawRect(rect()); } // Draw text if (node_ != nullptr) { painter->setPen(text_pen); // Draw the expand icon expand_hitbox_ = title_bar_rect_.adjusted(node_icon_padding_, node_icon_padding_, -node_icon_padding_, -node_icon_padding_); // Make the icon rect a square expand_hitbox_.setWidth(expand_hitbox_.height()); // Draw the icon if (IsExpanded()) { icon::TriDown.paint(painter, expand_hitbox_.toRect(), Qt::AlignLeft | Qt::AlignVCenter); } else { icon::TriRight.paint(painter, expand_hitbox_.toRect(), Qt::AlignLeft | Qt::AlignVCenter); } // Draw the text in a rect (the rect is sized around text already in the constructor) QRectF text_rect = title_bar_rect_.adjusted(node_icon_padding_ + expand_hitbox_.width() + node_text_padding_, node_text_padding_, -node_text_padding_, -node_text_padding_); painter->drawText(text_rect, static_cast(Qt::AlignVCenter | Qt::AlignLeft), node_->Name()); } } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { // We override standard mouse behavior in some cases. In these cases, we don't want the standard "move" and "release" // events to trigger if we haven't already triggered the "press" event. We use this variable to determine whether // base class behavior is valid here. standard_click_ = false; // Don't initiate a drag if we clicked the expand hitbox if (expand_hitbox_.contains(event->pos())) { return; } // See if the mouse click was on a parameter connector if (IsExpanded() // This is only possible if the node is expanded && node_ != nullptr) { // We can only loop through a node's parameters if a valid node is attached for (int i=0;iparameters().size();i++) { if (GetParameterConnectorRect(i).contains(event->pos())) { // See if the cursor is in the rect NodeParam* param = node_->parameters().at(i); // Create draggable object dragging_edge_ = new NodeViewEdge(); // Set up a QUndoCommand to make this action undoable node_edge_change_command_ = new QUndoCommand(); if (param->type() == NodeParam::kOutput || param->edges().isEmpty()) { // For an output param (or an input param with no connections), we default to creating a new edge drag_source_ = this; drag_src_param_ = param; // Set the starting position to the current param's connector dragging_edge_start_ = mapToScene(GetParameterConnectorRect(i).center()); } else if (param->type() == NodeParam::kInput) { // For an input param, we default to moving an existing edge // (here we use the last one, which will usually also be the first) NodeEdgePtr edge = param->edges().last(); // The starting position will be the OPPOSING parameter's rectangle // Get the opposing parameter drag_src_param_ = edge->output(); // Get its Node UI object drag_source_ = NodeView::NodeToUIObject(scene(), drag_src_param_->parentNode()); // Get the opposing parameter's rect center using the line's current coordinates // (we use the current coordinates because a complex formula is used for the line's coords if the opposing // node is collapsed, therefore it's easier to just retrieve it from line itself) NodeViewEdge* existing_edge_ui = NodeView::EdgeToUIObject(scene(), edge); QLineF existing_edge_line = existing_edge_ui->line(); if (existing_edge_ui->contains(existing_edge_line.p1())) { dragging_edge_start_ = existing_edge_line.p1(); } else { dragging_edge_start_ = existing_edge_line.p2(); } // Remove old edge NodeEdgeRemoveCommand* remove_command = new NodeEdgeRemoveCommand(edge->output(), edge->input(), node_edge_change_command_); remove_command->redo(); } // Add it to the scene scene()->addItem(dragging_edge_); // Trigger initial line setting mouseMoveEvent(event); return; } } } // We aren't using any override behaviors, switch back to standard click behavior standard_click_ = true; QGraphicsRectItem::mousePressEvent(event); } void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { // Check if an edge drag was initiated if (dragging_edge_ != nullptr) { QPointF end_point = event->scenePos(); drag_dest_param_ = nullptr; // See if the mouse is currently inside a node NodeViewItem* drop_item = dynamic_cast(scene()->itemAt(event->scenePos(), sceneTransform())); // If we expanded an item below but are no longer dragging over it, re-collapse it if (drag_expanded_item_ != nullptr && drop_item != drag_expanded_item_) { drag_expanded_item_->SetExpanded(false); drag_expanded_item_ = nullptr; } if (drop_item != nullptr && drop_item != drag_source_) { // If the item we're dragging over is collapsed, expand it if (!drop_item->IsExpanded()) { drag_expanded_item_ = drop_item; drop_item->SetExpanded(true); } // See if the mouse is currently inside a connector rect for (int i=0;inode()->parameters().size();i++) { // Make a larger "hitbox" rect to make it easier to drag into QRectF param_hitbox = drop_item->GetParameterConnectorRect(i).adjusted(-node_connector_size_, -node_connector_size_, node_connector_size_, node_connector_size_); // Get the parameter we're dragging into NodeParam* comp_param = drop_item->node()->parameters().at(i); if (comp_param->IsConnectable() && param_hitbox.contains(drop_item->mapFromScene(event->scenePos()))) { // See if we're dragging inside the hitbox // Prevent circular dependency - check if the Node we'll be outputting to already outputs to this Node Node* outputting_node; Node* receiving_node; // Determine which Node will be "submitting output" and which node will be "receiving input" if (drag_src_param_->type() == NodeParam::kInput) { receiving_node = drag_src_param_->parentNode(); outputting_node = drop_item->node(); } else { receiving_node = drop_item->node(); outputting_node = drag_src_param_->parentNode(); } // Ensure the receiving node doesn't output to the outputting node if (!receiving_node->OutputsTo(outputting_node)) { drag_dest_param_ = comp_param; end_point = drop_item->mapToScene(drop_item->GetParameterConnectorRect(i).center()); } break; } } } dragging_edge_->SetConnected(drag_dest_param_ != nullptr); dragging_edge_->setLine(QLineF(dragging_edge_start_, end_point)); return; } if (standard_click_) { QGraphicsRectItem::mouseMoveEvent(event); } } void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { // Check if an edge drag was initiated if (dragging_edge_ != nullptr) { // Remove the drag object scene()->removeItem(dragging_edge_); // If we expanded an item in the drag, re-collapse it now if (drag_expanded_item_ != nullptr) { drag_expanded_item_->SetExpanded(false); drag_expanded_item_ = nullptr; } if (drag_dest_param_ != nullptr) { // We dragged to somewhere, so we'll make a new connection NodeEdgePtr new_edge; NodeOutput* output; NodeInput* input; // Connecting will automatically add an edge UI object through the signal/slot system if (drag_dest_param_->type() == NodeParam::kOutput) { output = static_cast(drag_dest_param_); input = static_cast(drag_src_param_); } else { output = static_cast(drag_src_param_); input = static_cast(drag_dest_param_); } // Use a command to make Node connecting undoable new NodeEdgeAddCommand(output, input, node_edge_change_command_); } dragging_edge_ = nullptr; Core::instance()->undo_stack()->pushIfHasChildren(node_edge_change_command_); node_edge_change_command_ = nullptr; return; } // Check if we clicked the Expand/Collapse icon if (expand_hitbox_.contains(event->pos())) { SetExpanded(!IsExpanded()); } if (standard_click_) { QGraphicsRectItem::mouseReleaseEvent(event); } } QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { node_->SetPosition(value.toPointF()); } return QGraphicsItem::itemChange(change, value); }