Files
oak-editor/app/widget/nodeview/nodeviewitem.cpp
T
itsmattkc 323718bc11 moved timeline undoable commands to using the new NodeAddCommand
Previous iteration used some "magic code" that added clips automatically to the
timeline. This was functional but ultimately outside of the undo commands'
control meaning nodes could be infinitely added and abandoned. This makes the
add process part of the undo command which means it's all undoable as the user
would expect.
2019-12-14 00:04:57 +11:00

483 lines
16 KiB
C++

/***
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 <http://www.gnu.org/licenses/>.
***/
#include "nodeviewitem.h"
#include <QDebug>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include "common/qtversionabstraction.h"
#include "core.h"
#include "nodeview.h"
#include "nodeviewundo.h"
#include "ui/icons/icons.h"
#include "undo/undostack.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 = olive::core.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;i<node_->parameters().size();i++) {
NodeParam* param = node_->parameters().at(i);
// 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()) {
olive::icon::TriDown.paint(painter, expand_hitbox_.toRect(), Qt::AlignLeft | Qt::AlignVCenter);
} else {
olive::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<int>(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;i<node_->parameters().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<NodeViewItem*>(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;i<drop_item->node()->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 (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<NodeOutput*>(drag_dest_param_);
input = static_cast<NodeInput*>(drag_src_param_);
} else {
output = static_cast<NodeOutput*>(drag_src_param_);
input = static_cast<NodeInput*>(drag_dest_param_);
}
// Use a command to make Node connecting undoable
new NodeEdgeAddCommand(output,
input,
node_edge_change_command_);
}
dragging_edge_ = nullptr;
olive::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);
}