Files
oak-editor/app/widget/nodeview/nodeviewitem.cpp
T
itsmattkc 75d4cd899b use hardcoded namespace
The macro defined namespaces confused the hell out of lupdate and more or less broke translations permanently. Looks like the only way we can do it is to have a hardcoded namespace, which goes against my instinct, but honestly how likely is it that we'll change the namespace anyway (I guess forks might want to do it, but that's their problem ;) )
2020-11-17 20:24:42 +11:00

703 lines
20 KiB
C++

/***
Olive - Non-Linear Video Editor
Copyright (C) 2020 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/flipmodifiers.h"
#include "common/qtutils.h"
#include "config/config.h"
#include "core.h"
#include "nodeview.h"
#include "nodeviewscene.h"
#include "nodeviewundo.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"
namespace olive {
NodeViewItem::NodeViewItem(QGraphicsItem *parent) :
QGraphicsRectItem(parent),
node_(nullptr),
dragging_edge_(nullptr),
cached_drop_item_(nullptr),
cached_drop_item_expanded_(false),
expanded_(false),
hide_titlebar_(false),
standard_click_(false),
highlighted_index_(-1),
node_edge_change_command_(nullptr),
flow_dir_(NodeViewCommon::kLeftToRight)
{
// 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
//
// Set border width
node_border_width_ = DefaultItemBorder();
int widget_width = DefaultItemWidth();
int widget_height = DefaultItemHeight();
title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height);
setRect(title_bar_rect_);
}
QPointF NodeViewItem::GetNodePosition() const
{
QPointF node_pos;
qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding();
qreal adjusted_y = pos().y() / DefaultItemVerticalPadding();
switch (flow_dir_) {
case NodeViewCommon::kLeftToRight:
node_pos.setX(adjusted_x);
node_pos.setY(adjusted_y);
break;
case NodeViewCommon::kRightToLeft:
node_pos.setX(-adjusted_x);
node_pos.setY(adjusted_y);
break;
case NodeViewCommon::kTopToBottom:
node_pos.setX(adjusted_y);
node_pos.setY(adjusted_x);
break;
case NodeViewCommon::kBottomToTop:
node_pos.setX(-adjusted_y);
node_pos.setY(adjusted_x);
break;
}
return node_pos;
}
void NodeViewItem::SetNodePosition(const QPointF &pos)
{
switch (flow_dir_) {
case NodeViewCommon::kLeftToRight:
setPos(pos.x() * DefaultItemHorizontalPadding(),
pos.y() * DefaultItemVerticalPadding());
break;
case NodeViewCommon::kRightToLeft:
setPos(-pos.x() * DefaultItemHorizontalPadding(),
pos.y() * DefaultItemVerticalPadding());
break;
case NodeViewCommon::kTopToBottom:
setPos(pos.y() * DefaultItemHorizontalPadding(),
pos.x() * DefaultItemVerticalPadding());
break;
case NodeViewCommon::kBottomToTop:
setPos(pos.y() * DefaultItemHorizontalPadding(),
-pos.x() * DefaultItemVerticalPadding());
break;
}
}
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()), "HHHHHHHHHHHH");;
}
int NodeViewItem::DefaultItemBorder()
{
return QFontMetrics(QFont()).height() / 12;
}
qreal NodeViewItem::DefaultItemHorizontalPadding() const
{
if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) {
return DefaultItemWidth() * 1.5;
} else {
return DefaultItemWidth() * 1.25;
}
}
qreal NodeViewItem::DefaultItemVerticalPadding() const
{
if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) {
return DefaultItemHeight() * 1.5;
} else {
return DefaultItemHeight() * 2.0;
}
}
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<NodeInput*>(p);
if (input->is_connectable()) {
node_inputs_.append(input);
}
}
}
SetNodePosition(node_->GetPosition());
}
update();
}
Node *NodeViewItem::GetNode() const
{
return node_;
}
bool NodeViewItem::IsExpanded() const
{
return expanded_;
}
void NodeViewItem::SetExpanded(bool e, bool hide_titlebar)
{
if (node_inputs_.isEmpty()
|| (expanded_ == e && hide_titlebar_ == hide_titlebar)) {
return;
}
expanded_ = e;
hide_titlebar_ = hide_titlebar;
if (expanded_ && !node_inputs_.isEmpty()) {
// Create new rect
QRectF new_rect = title_bar_rect_;
if (hide_titlebar_) {
new_rect.setHeight(new_rect.height() * node_inputs_.size());
} else {
new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1));
}
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();
// Draw background rect if expanded
if (IsExpanded()) {
painter->setPen(Qt::NoPen);
painter->setBrush(app_pal.color(QPalette::Window));
painter->drawRect(rect());
painter->setPen(app_pal.color(QPalette::Text));
for (int i=0;i<node_inputs_.size();i++) {
QRectF input_rect = GetInputRect(i);
if (highlighted_index_ == i) {
QColor highlight_col = app_pal.color(QPalette::Text);
highlight_col.setAlpha(64);
painter->fillRect(input_rect, highlight_col);
}
painter->drawText(input_rect, Qt::AlignCenter, node_inputs_.at(i)->name());
}
}
// Draw the titlebar
if (!hide_titlebar_ && node_) {
Color node_color = Config::Current()[QStringLiteral("NodeCatColor%1")
.arg(node_->Category().first())].value<Color>();
QLinearGradient grad;
grad.setStart(0, rect().top());
grad.setFinalStop(0, rect().bottom());
QColor node_color_as_qcolor = node_color.toQColor();
{
// Generate lighter color for gradient
qreal hue, sat, lightness;
node_color_as_qcolor.getHslF(&hue, &sat, &lightness);
lightness = qMin(1.0, lightness + 0.2);
grad.setColorAt(0.0, QColor::fromHslF(hue, sat, lightness));
}
grad.setColorAt(1.0, node_color_as_qcolor);
painter->setPen(Qt::black);
painter->setBrush(grad);
painter->drawRect(title_bar_rect_);
painter->setPen(app_pal.color(QPalette::Text));
QString node_label;
if (!node_->GetLabel().isEmpty()) {
// Use label directly if node has one
node_label = node_->GetLabel();
} else if (node_->IsTrack()) {
// If node is a track, use a special track name to help users identify better
// Exception for tracks
TrackOutput* track = static_cast<TrackOutput*>(node_);
node_label = TrackOutput::GetDefaultTrackName(track->track_type(), track->Index());
} else {
// Otherwise, just use the node's short name
node_label = node_->ShortName();
}
QFont f;
QFontMetrics fm(f);
// Draw right or down arrow based on expanded state
int icon_size = fm.height() / 2;
int icon_padding = title_bar_rect_.height() / 2 - icon_size / 2;
int icon_full_size = icon_size + icon_padding * 2;
const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight;
expand_icon.paint(painter, QRect(title_bar_rect_.x() + icon_padding,
title_bar_rect_.y() + icon_padding,
icon_size,
icon_size));
// Calculate how much space we have for text
int item_width = title_bar_rect_.width();
int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size;
int label_width = QtUtils::QFontMetricsWidth(fm, node_label);
// 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 {
node_label.chop(1);
concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(node_label);
} while ((label_width = QtUtils::QFontMetricsWidth(fm, concatenated)) > max_text_width);
node_label = concatenated;
}
// Determine the text color (automatically calculate from node background color)
if (node_color.GetRoughLuminance() > 0.5) {
painter->setPen(Qt::black);
} else {
painter->setPen(Qt::white);
}
// Determine X position (favors horizontal centering unless it'll overrun the arrow)
QRectF text_rect = title_bar_rect_;
Qt::Alignment text_align = Qt::AlignCenter;
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 | Qt::AlignVCenter;
}
// Draw the text in a rect (the rect is sized around text already in the constructor)
painter->drawText(text_rect,
text_align,
node_label);
}
// Draw final border
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)
{
// 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();
dragging_edge_->SetFlowDirection(flow_dir_);
// 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, QPointF());
} 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<NodeViewScene*>(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<NodeViewScene*>(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_->SetHighlightedIndex(-1);
cached_drop_item_->setZValue(0);
cached_drop_item_ = nullptr;
}
NodeViewItem* cursor_item = dynamic_cast<NodeViewItem*>(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, 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;i<cached_drop_item_->node_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_, true)) {
drag_dest_param_ = comp_param;
highlight_their_index = i;
QPointF end_point_local = cached_drop_item_->GetInputPoint(i, pos());
end_point = cached_drop_item_->mapToScene(end_point_local);
}
break;
}
}
cached_drop_item_->SetHighlightedIndex(highlight_their_index);
}
dragging_edge_->SetConnected(drag_dest_param_);
dragging_edge_->SetPoints(dragging_edge_start_,
end_point,
cached_drop_item_ ? cached_drop_item_->IsExpanded() : false);
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_) {
if (cached_drop_item_expanded_) {
cached_drop_item_->SetExpanded(false);
}
cached_drop_item_->SetHighlightedIndex(-1);
cached_drop_item_->setZValue(0);
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<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;
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);
}
}
void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsRectItem::mouseDoubleClickEvent(event);
if (!(event->modifiers() & Qt::ControlModifier)) {
SetExpanded(!IsExpanded());
}
}
QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionHasChanged && node_) {
node_->blockSignals(true);
node_->SetPosition(GetNodePosition());
node_->blockSignals(false);
}
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 (!hide_titlebar_) {
index++;
}
if (IsExpanded()) {
r.translate(0, r.height() * index);
}
return r;
}
QPointF NodeViewItem::GetParamPoint(NodeParam *param, const QPointF& source_pos) const
{
if (param->type() == NodeParam::kOutput) {
switch (flow_dir_) {
case NodeViewCommon::kLeftToRight:
default:
return pos() + QPointF(rect().right(), rect().center().y());
case NodeViewCommon::kRightToLeft:
return pos() + QPointF(rect().left(), rect().center().y());
case NodeViewCommon::kTopToBottom:
return pos() + QPointF(rect().center().x(), rect().bottom());
case NodeViewCommon::kBottomToTop:
return pos() + QPointF(rect().center().x(), rect().top());
}
} else {
NodeInput* input = static_cast<NodeInput*>(param);
// Resolve NodeInputArray elements
while (input->parentNode() != input->parent()) {
input = static_cast<NodeInput*>(input->parent());
}
return pos() + GetInputPoint(node_inputs_.indexOf(input), source_pos);
}
}
void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir)
{
flow_dir_ = dir;
}
QPointF NodeViewItem::GetInputPoint(int index, const QPointF& source_pos) const
{
QRectF input_rect = GetInputRect(index);
Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_);
if (flow_orientation == Qt::Horizontal || IsExpanded()) {
if (flow_dir_ == NodeViewCommon::kLeftToRight
|| (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) {
return QPointF(input_rect.left(), input_rect.center().y());
} else {
return QPointF(input_rect.right(), input_rect.center().y());
}
} else {
if (flow_dir_ == NodeViewCommon::kTopToBottom) {
return QPointF(input_rect.center().x(), input_rect.top());
} else {
return QPointF(input_rect.center().x(), input_rect.bottom());
}
}
}
}