/***
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/flipmodifiers.h"
#include "common/qtutils.h"
#include "core.h"
#include "nodeview.h"
#include "nodeviewscene.h"
#include "nodeviewundo.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"
NodeViewItem::NodeViewItem(QGraphicsItem *parent) :
QGraphicsRectItem(parent),
node_(nullptr),
dragging_edge_(nullptr),
cached_drop_item_(nullptr),
cached_drop_item_expanded_(false),
expanded_(false),
standard_click_(false),
highlighted_index_(-1),
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
//
QFont default_font;
QFontMetrics font_metrics(default_font);
// Set border width
node_border_width_ = font_metrics.height() / 12;
// Set text and icon padding
int node_text_padding = font_metrics.height() / 4;
// Not particularly great way of using text scaling to set the width (DPI-awareness, etc.)
int widget_width = QFontMetricsWidth(font_metrics, "HHHHHHHHHHHHHH");
// Use the current default font height to size this widget
// Set default "collapsed" size
int widget_height = font_metrics.height() + node_text_padding * 2;
title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height);
setRect(title_bar_rect_);
}
void NodeViewItem::SetNode(Node *n)
{
node_ = n;
node_inputs_.clear();
if (node_) {
node_->Retranslate();
foreach (NodeParam* p, node_->parameters()) {
if (p->type() == NodeParam::kInput) {
NodeInput* input = static_cast(p);
if (input->IsConnectable()) {
node_inputs_.append(input);
}
}
}
setPos(node_->GetPosition());
}
update();
}
Node *NodeViewItem::GetNode() const
{
return node_;
}
bool NodeViewItem::IsExpanded() const
{
return expanded_;
}
void NodeViewItem::SetExpanded(bool e)
{
if (expanded_ == e) {
return;
}
expanded_ = e;
if (expanded_ && !node_inputs_.isEmpty()) {
// Create new rect
QRectF new_rect = title_bar_rect_;
new_rect.setHeight(new_rect.height() * node_inputs_.size());
setRect(new_rect);
} else {
setRect(title_bar_rect_);
}
update();
}
void NodeViewItem::ToggleExpanded()
{
SetExpanded(!IsExpanded());
}
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();
{
QPen border_pen;
border_pen.setWidth(node_border_width_);
QBrush bkg_color;
if (option->state & QStyle::State_Selected) {
border_pen.setColor(app_pal.color(QPalette::Highlight));
} else {
border_pen.setColor(css_proxy_.BorderColor());
}
if (IsExpanded()) {
bkg_color = app_pal.color(QPalette::Window);
} else {
bkg_color = css_proxy_.TitleBarColor();
}
painter->setPen(border_pen);
painter->setBrush(bkg_color);
painter->drawRect(rect());
}
painter->setPen(app_pal.color(QPalette::Text));
if (IsExpanded()) {
for (int i=0;ifillRect(input_rect, QColor(255, 255, 255, 64));
}
painter->drawText(input_rect, Qt::AlignCenter, node_inputs_.at(i)->name());
}
} else if (node_) {
// Draw the text in a rect (the rect is sized around text already in the constructor)
painter->drawText(title_bar_rect_, Qt::AlignCenter, 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;
// If CTRL is held, we start a new edge
if (node_ && event->modifiers() & Qt::ControlModifier) {
NodeParam* param = node_->output();
// 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_ = GetParamPoint(param);
} 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_ = static_cast(scene())->NodeToUIObject(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 = static_cast(scene())->EdgeToUIObject(edge);
QPainterPath existing_edge_line = existing_edge_ui->path();
QPointF edge_start = existing_edge_line.pointAtPercent(0);
QPointF edge_end = existing_edge_line.pointAtPercent(1);
if (existing_edge_ui->contains(edge_start)) {
dragging_edge_start_ = edge_start;
} else {
dragging_edge_start_ = edge_end;
}
// 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;
} else {
// We aren't using any override behaviors, switch back to standard click behavior
standard_click_ = true;
event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));
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;
if (!cached_drop_item_
|| !cached_drop_item_->contains(cached_drop_item_->mapFromScene(event->scenePos()))) {
// Cursor has left last entered item, we need to find a new one
if (cached_drop_item_) {
// If we expanded an item below but are no longer dragging over it, re-collapse it
if (cached_drop_item_expanded_) {
cached_drop_item_->SetExpanded(false);
}
cached_drop_item_->setZValue(0);
cached_drop_item_ = nullptr;
}
NodeViewItem* cursor_item = dynamic_cast(scene()->itemAt(event->scenePos(), sceneTransform()));
if (cursor_item
&& cursor_item != drag_source_
&& !cursor_item->node_inputs_.isEmpty()) {
cached_drop_item_ = cursor_item;
// If the item we're dragging over is collapsed, expand it
cached_drop_item_expanded_ = !cached_drop_item_->IsExpanded();
if (cached_drop_item_expanded_) {
cached_drop_item_->SetExpanded(true);
}
cached_drop_item_->setZValue(1);
} else {
cached_drop_item_ = nullptr;
}
}
if (cached_drop_item_) {
int highlight_their_index = -1;
// See if the mouse is currently inside a connector rect
for (int i=0;inode_inputs_.size();i++) {
// Get the parameter we're dragging into
NodeParam* comp_param = cached_drop_item_->node_inputs_.at(i);
// See if cursor is inside its UI
if (cached_drop_item_->GetInputRect(i).contains(cached_drop_item_->mapFromScene(event->scenePos()))) { // See if we're dragging inside the hitbox
// Attempt to prevent circular dependency - ensure the receiving node doesn't output to the outputting node
if (!cached_drop_item_->GetNode()->OutputsTo(node_)) {
drag_dest_param_ = comp_param;
highlight_their_index = i;
end_point = cached_drop_item_->mapToScene(cached_drop_item_->GetInputPoint(i));
}
break;
}
}
cached_drop_item_->SetHighlightedIndex(highlight_their_index);
}
dragging_edge_->SetConnected(drag_dest_param_ != nullptr);
dragging_edge_->SetPoints(dragging_edge_start_, end_point);
return;
}
if (standard_click_) {
event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));
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 (cached_drop_item_ != nullptr) {
cached_drop_item_->SetExpanded(false);
cached_drop_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;
}
if (standard_click_) {
event->setModifiers(FlipControlAndShiftModifiers(event->modifiers()));
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);
}
void NodeViewItem::SetHighlightedIndex(int index)
{
if (highlighted_index_ == index) {
return;
}
highlighted_index_ = index;
update();
}
QRectF NodeViewItem::GetInputRect(int index) const
{
QRectF r = title_bar_rect_;
if (IsExpanded()) {
r.translate(0, r.height() * index);
}
return r;
}
QPointF NodeViewItem::GetParamPoint(NodeParam *param) const
{
if (param->type() == NodeParam::kOutput) {
return pos() + QPointF(rect().right(), rect().center().y());
} else {
NodeInput* input = static_cast(param);
// Resolve NodeInputArray elements
while (input->parentNode() != input->parent()) {
input = static_cast(input->parent());
}
return pos() + GetInputPoint(node_inputs_.indexOf(input));
}
}
QPointF NodeViewItem::GetInputPoint(int index) const
{
QRectF input_rect = GetInputRect(index);
return QPointF(input_rect.left(), input_rect.center().y());
}