- Move Behavior-General options (hover focus, slider ladder, scroll zooms) into the General preferences tab. - Move Behavior-Audio option (audio scrubbing) into the Audio preferences tab. - Promote remaining Behavior categories (Timeline, Playback, Project, Nodes, Rendering) to top-level sidebar entries without the 'Behavior - ' prefix. - Update Chinese translations for the new sidebar titles and fill unfinished Behavior tab strings. NodeView: - Remove double-click jump-to-parameter-editor behavior; keep expand/collapse. - Add right-click context menu item 'Show in Parameter Editor'. - Add Shift+P shortcut bound to the same action. Render: - Fix crash in PreviewAutoCacher::ClearSingleFrameRenders when proxy playback causes a render ticket to finish synchronously before the watcher pointer is returned. Defer the watcher Finished signal via queued connection so the caller can safely register the watcher before it is deleted.
1945 lines
49 KiB
C++
1945 lines
49 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2022 Olive Team
|
|
Modifications Copyright (C) 2025 mikesolar
|
|
|
|
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 "nodeview.h"
|
|
|
|
#include <QInputDialog>
|
|
#include <QMessageBox>
|
|
#include <QMimeData>
|
|
#include <QMouseEvent>
|
|
#include <QPushButton>
|
|
#include <QScrollBar>
|
|
#include <QToolTip>
|
|
|
|
#include "node/audio/volume/volume.h"
|
|
#include "node/distort/transform/transformdistortnode.h"
|
|
#include "node/factory.h"
|
|
#include "node/group/group.h"
|
|
#include "node/nodeundo.h"
|
|
#include "node/project/serializer/serializer.h"
|
|
#include "panel/panelmanager.h"
|
|
#include "node/traverser.h"
|
|
#include "ui/icons/icons.h"
|
|
#include "widget/menu/menushared.h"
|
|
#include "widget/timebased/timebasedview.h"
|
|
|
|
#define super HandMovableView
|
|
|
|
namespace olive
|
|
{
|
|
|
|
const double NodeView::kMinimumScale = 0.1;
|
|
const int NodeView::kMaximumContexts = 10;
|
|
|
|
NodeView::NodeView(QWidget *parent)
|
|
: HandMovableView(parent)
|
|
, drop_edge_(nullptr)
|
|
, create_edge_(nullptr)
|
|
, create_edge_output_item_(nullptr)
|
|
, create_edge_input_item_(nullptr)
|
|
, overlay_view_(nullptr)
|
|
, scale_(1.0)
|
|
, dont_emit_selection_signals_(false)
|
|
, show_in_param_editor_action_(nullptr)
|
|
{
|
|
setScene(&scene_);
|
|
SetDefaultDragMode(RubberBandDrag);
|
|
setContextMenuPolicy(Qt::CustomContextMenu);
|
|
setMouseTracking(true);
|
|
setRenderHint(QPainter::Antialiasing);
|
|
setViewportUpdateMode(FullViewportUpdate);
|
|
|
|
connect(this, &NodeView::customContextMenuRequested, this,
|
|
&NodeView::ShowContextMenu);
|
|
|
|
ConnectSelectionChangedSignal();
|
|
|
|
SetFlowDirection(NodeViewCommon::kLeftToRight);
|
|
|
|
show_in_param_editor_action_ = new QAction(tr("Show in Parameter Editor"), this);
|
|
Menu::ConformItem(show_in_param_editor_action_, QStringLiteral("shownodeparams"), QKeySequence(tr("Shift+P")));
|
|
show_in_param_editor_action_->setShortcutContext(Qt::WindowShortcut);
|
|
addAction(show_in_param_editor_action_);
|
|
connect(show_in_param_editor_action_, &QAction::triggered, this, &NodeView::ShowSelectedNodeInParamEditor);
|
|
|
|
UpdateSceneBoundingRect();
|
|
connect(&scene_, &QGraphicsScene::changed, this,
|
|
&NodeView::UpdateSceneBoundingRect);
|
|
|
|
minimap_ = new NodeViewMiniMap(&scene_, this);
|
|
minimap_->show();
|
|
connect(minimap_, &NodeViewMiniMap::Resized, this,
|
|
&NodeView::RepositionMiniMap);
|
|
connect(minimap_, &NodeViewMiniMap::MoveToScenePoint, this,
|
|
&NodeView::MoveToScenePoint);
|
|
connect(horizontalScrollBar(), &QScrollBar::valueChanged, this,
|
|
&NodeView::UpdateViewportOnMiniMap);
|
|
connect(verticalScrollBar(), &QScrollBar::valueChanged, this,
|
|
&NodeView::UpdateViewportOnMiniMap);
|
|
|
|
viewport()->installEventFilter(this);
|
|
}
|
|
|
|
NodeView::~NodeView()
|
|
{
|
|
// Unset the current graph
|
|
ClearGraph();
|
|
}
|
|
|
|
void NodeView::SetContexts(const QVector<Node *> &nodes)
|
|
{
|
|
if (overlay_view_) {
|
|
CloseOverlay();
|
|
}
|
|
|
|
// Remove contexts that are no longer in the list
|
|
foreach (Node *n, contexts_) {
|
|
if (!nodes.contains(n)) {
|
|
RemoveContext(n);
|
|
}
|
|
}
|
|
|
|
// Add contexts that are now in the list
|
|
foreach (Node *n, nodes) {
|
|
if (scene_.context_map().size() >= kMaximumContexts) {
|
|
break;
|
|
}
|
|
|
|
if (!contexts_.contains(n)) {
|
|
AddContext(n);
|
|
}
|
|
}
|
|
|
|
contexts_ = nodes;
|
|
|
|
CenterOnItemsBoundingRect();
|
|
}
|
|
|
|
void NodeView::CloseContextsBelongingToProject(Project *project)
|
|
{
|
|
QVector<Node *> new_contexts = contexts_;
|
|
|
|
for (auto it = new_contexts.begin(); it != new_contexts.end();) {
|
|
if ((*it)->project() == project) {
|
|
it = new_contexts.erase(it);
|
|
} else {
|
|
it++;
|
|
}
|
|
}
|
|
|
|
SetContexts(new_contexts);
|
|
}
|
|
|
|
void NodeView::ClearGraph()
|
|
{
|
|
SetContexts(QVector<Node *>());
|
|
}
|
|
|
|
void NodeView::DeleteSelected()
|
|
{
|
|
NodeViewDeleteCommand *command = new NodeViewDeleteCommand();
|
|
|
|
int count = 0;
|
|
|
|
foreach (NodeViewContext *ctx, scene_.context_map()) {
|
|
count += ctx->DeleteSelected(command);
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(command,
|
|
tr("Deleted %1 Node(s)").arg(count));
|
|
}
|
|
|
|
void NodeView::SelectAll()
|
|
{
|
|
// Optimization: rather than respond to every single item being selected, ignore the signal and
|
|
// then handle them all at the end.
|
|
DisconnectSelectionChangedSignal();
|
|
|
|
scene_.SelectAll();
|
|
|
|
ConnectSelectionChangedSignal();
|
|
|
|
UpdateSelectionCache();
|
|
}
|
|
|
|
void NodeView::DeselectAll()
|
|
{
|
|
if (selected_nodes_.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Optimization: rather than respond to every single item being selected, ignore the signal and
|
|
// then handle them all at the end.
|
|
DisconnectSelectionChangedSignal();
|
|
|
|
scene_.DeselectAll();
|
|
|
|
ConnectSelectionChangedSignal();
|
|
|
|
// Just emit all the nodes that are currently selected as no longer selected
|
|
emit NodesDeselected(selected_nodes_);
|
|
selected_nodes_.clear();
|
|
emit NodeSelectionChanged(selected_nodes_);
|
|
emit NodeSelectionChangedWithContexts(QVector<Node::ContextPair>());
|
|
}
|
|
|
|
void NodeView::Select(const QVector<Node::ContextPair> &nodes,
|
|
bool center_view_on_item)
|
|
{
|
|
// Optimization: rather than respond to every single item being selected, ignore the signal and
|
|
// then handle them all at the end.
|
|
DisconnectSelectionChangedSignal();
|
|
|
|
QVector<Node *> deselections = selected_nodes_;
|
|
QVector<Node *> new_selections;
|
|
|
|
scene_.DeselectAll();
|
|
|
|
foreach (const Node::ContextPair &p, nodes) {
|
|
NodeViewContext *ctx = scene_.context_map().value(p.context);
|
|
if (ctx) {
|
|
NodeViewItem *item = ctx->GetItemFromMap(p.node);
|
|
if (item) {
|
|
item->setSelected(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Center on something
|
|
if (center_view_on_item && !nodes.isEmpty()) {
|
|
QMetaObject::invokeMethod(this, "CenterOnNode", Qt::QueuedConnection,
|
|
OLIVE_NS_ARG(Node *, nodes.first().node));
|
|
}
|
|
|
|
ConnectSelectionChangedSignal();
|
|
|
|
// Don't signal when this function was likely triggered from another widget's signal anyway
|
|
dont_emit_selection_signals_ = true;
|
|
UpdateSelectionCache();
|
|
dont_emit_selection_signals_ = false;
|
|
}
|
|
|
|
void NodeView::CopySelected(bool cut)
|
|
{
|
|
if (selected_nodes_.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QString copy_str;
|
|
QXmlStreamWriter writer(©_str);
|
|
|
|
ProjectSerializer::SaveData sdata(ProjectSerializer::kOnlyNodes);
|
|
sdata.SetOnlySerializeNodesAndResolveGroups(selected_nodes_);
|
|
|
|
ProjectSerializer::SerializedProperties properties;
|
|
|
|
for (Node *n : selected_nodes_) {
|
|
NodeViewItem *item = GetAssumedItemForSelectedNode(n);
|
|
|
|
if (item) {
|
|
Node::Position pos = item->GetNodePositionData();
|
|
|
|
properties[n][QStringLiteral("x")] =
|
|
QString::number(pos.position.x());
|
|
properties[n][QStringLiteral("y")] =
|
|
QString::number(pos.position.y());
|
|
properties[n][QStringLiteral("expanded")] =
|
|
QString::number(pos.expanded);
|
|
}
|
|
}
|
|
|
|
sdata.SetProperties(properties);
|
|
|
|
ProjectSerializer::Save(&writer, sdata);
|
|
|
|
Core::CopyStringToClipboard(copy_str);
|
|
|
|
if (cut) {
|
|
DeleteSelected();
|
|
}
|
|
}
|
|
|
|
void NodeView::Paste()
|
|
{
|
|
if (contexts_.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
ProjectSerializer::Result res =
|
|
ProjectSerializer::Paste(ProjectSerializer::kOnlyNodes);
|
|
if (res.GetLoadData().nodes.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Node::PositionMap map;
|
|
|
|
for (auto it = res.GetLoadData().properties.cbegin();
|
|
it != res.GetLoadData().properties.cend(); it++) {
|
|
Node::Position pos;
|
|
|
|
const QMap<QString, QString> &node_props = it.value();
|
|
pos.position.setX(node_props.value(QStringLiteral("x")).toDouble());
|
|
pos.position.setY(node_props.value(QStringLiteral("y")).toDouble());
|
|
pos.expanded = node_props.value(QStringLiteral("expanded")).toDouble();
|
|
|
|
map.insert(it.key(), pos);
|
|
}
|
|
|
|
PostPaste(res.GetLoadData().nodes, map);
|
|
}
|
|
|
|
void NodeView::Duplicate()
|
|
{
|
|
if (!selected_nodes_.isEmpty()) {
|
|
QVector<Node *> selected = selected_nodes_;
|
|
QVector<Node *> new_nodes;
|
|
Node::PositionMap map;
|
|
|
|
new_nodes.resize(selected.size());
|
|
|
|
// Create copies of each selected node, checking for groups and adding children if necessary
|
|
for (int i = 0; i < selected.size(); i++) {
|
|
new_nodes[i] = selected.at(i)->copy();
|
|
|
|
if (NodeGroup *g = dynamic_cast<NodeGroup *>(selected.at(i))) {
|
|
for (auto it = g->GetContextPositions().cbegin();
|
|
it != g->GetContextPositions().cend(); it++) {
|
|
if (!selected.contains(it.key())) {
|
|
// This should automatically recurse if this is a group inside a group
|
|
selected.append(it.key());
|
|
}
|
|
}
|
|
new_nodes.resize(selected.size());
|
|
}
|
|
}
|
|
|
|
// Get positions in contexts, add input passthroughs, and copy input values/keyframes
|
|
for (int i = 0; i < new_nodes.size(); i++) {
|
|
Node *og = selected.at(i);
|
|
Node *copy = new_nodes.at(i);
|
|
|
|
Node::Position pos;
|
|
if (GetAssumedPositionForSelectedNode(og, &pos)) {
|
|
map.insert(copy, pos);
|
|
}
|
|
|
|
for (auto it = og->GetContextPositions().cbegin();
|
|
it != og->GetContextPositions().cend(); it++) {
|
|
Node *child_og = it.key();
|
|
int child_index = selected.indexOf(child_og);
|
|
|
|
if (child_index != -1) {
|
|
Node *child_copy = new_nodes.at(child_index);
|
|
|
|
copy->SetNodePositionInContext(child_copy, it.value());
|
|
}
|
|
}
|
|
|
|
if (NodeGroup *src_group = dynamic_cast<NodeGroup *>(og)) {
|
|
NodeGroup *dst_group = static_cast<NodeGroup *>(copy);
|
|
|
|
for (auto it = src_group->GetInputPassthroughs().cbegin();
|
|
it != src_group->GetInputPassthroughs().cend(); it++) {
|
|
NodeInput input = it->second;
|
|
input.set_node(
|
|
new_nodes.at(selected.indexOf(input.node())));
|
|
dst_group->AddInputPassthrough(input, it->first);
|
|
}
|
|
|
|
dst_group->SetOutputPassthrough(new_nodes.at(
|
|
selected.indexOf(src_group->GetOutputPassthrough())));
|
|
}
|
|
|
|
Node::CopyInputs(selected.at(i), new_nodes.at(i), false);
|
|
}
|
|
|
|
// Copy connections
|
|
Node::CopyDependencyGraph(selected, new_nodes, nullptr);
|
|
|
|
// Set root level context positions and attach to
|
|
PostPaste(new_nodes, map);
|
|
}
|
|
}
|
|
|
|
void NodeView::SetColorLabel(int index)
|
|
{
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
for (Node *node : qAsConst(selected_nodes_)) {
|
|
command->add_child(new NodeOverrideColorCommand(node, index));
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(
|
|
command, tr("Set Color of %1 Node(s)").arg(selected_nodes_.size()));
|
|
}
|
|
|
|
void NodeView::ZoomIn()
|
|
{
|
|
ZoomFromKeyboard(1.25);
|
|
}
|
|
|
|
void NodeView::ZoomOut()
|
|
{
|
|
ZoomFromKeyboard(0.8);
|
|
}
|
|
|
|
void NodeView::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
switch (event->key()) {
|
|
case Qt::Key_Left:
|
|
case Qt::Key_Right:
|
|
case Qt::Key_Up:
|
|
case Qt::Key_Down: {
|
|
MultiUndoCommand *pos_command = new MultiUndoCommand();
|
|
for (Node *n : qAsConst(selected_nodes_)) {
|
|
for (Node *context : qAsConst(contexts_)) {
|
|
if (context->ContextContainsNode(n)) {
|
|
Node::Position old_pos =
|
|
context->GetNodePositionInContext(n);
|
|
|
|
// Determine one pixel in scene units
|
|
double movement_amt = 1.0 / scale_;
|
|
|
|
// Translate to 2D movement
|
|
QPointF node_movement;
|
|
switch (event->key()) {
|
|
case Qt::Key_Left:
|
|
node_movement.setX(-movement_amt);
|
|
break;
|
|
case Qt::Key_Right:
|
|
node_movement.setX(movement_amt);
|
|
break;
|
|
case Qt::Key_Up:
|
|
node_movement.setY(-movement_amt);
|
|
break;
|
|
case Qt::Key_Down:
|
|
node_movement.setY(movement_amt);
|
|
break;
|
|
}
|
|
|
|
// Translate from screen units into node units
|
|
node_movement = NodeViewItem::ScreenToNodePoint(
|
|
node_movement, scene_.GetFlowDirection());
|
|
|
|
// Move command
|
|
pos_command->add_child(new NodeSetPositionCommand(
|
|
n, context, old_pos + node_movement));
|
|
}
|
|
}
|
|
}
|
|
Core::instance()->undo_stack()->push(
|
|
pos_command, tr("Moved %1 Node(s)").arg(selected_nodes_.size()));
|
|
break;
|
|
}
|
|
case Qt::Key_Escape:
|
|
if (!attached_items_.isEmpty()) {
|
|
DetachItemsFromCursor();
|
|
break;
|
|
}
|
|
|
|
emit EscPressed();
|
|
|
|
/* fall through */
|
|
default:
|
|
super::keyPressEvent(event);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void NodeView::mousePressEvent(QMouseEvent *event)
|
|
{
|
|
// Handle mouse press event
|
|
if (HandPress(event))
|
|
return;
|
|
|
|
// Get the item that the user clicked on, if any
|
|
QGraphicsItem *item = itemAt(event->pos());
|
|
|
|
if (event->button() == Qt::LeftButton) {
|
|
// Sane defaults
|
|
create_edge_already_exists_ = false;
|
|
create_edge_from_output_ = true;
|
|
create_edge_input_.Reset();
|
|
|
|
if (event->modifiers() & Qt::ControlModifier) {
|
|
NodeViewItem *mouse_item = dynamic_cast<NodeViewItem *>(item);
|
|
|
|
if (mouse_item) {
|
|
if (mouse_item->IsOutputItem()) {
|
|
create_edge_output_item_ = mouse_item;
|
|
} else {
|
|
create_edge_input_item_ = mouse_item;
|
|
create_edge_input_ = mouse_item->GetInput();
|
|
create_edge_from_output_ = false;
|
|
}
|
|
|
|
// Highlight start item for better user experience
|
|
mouse_item->SetHighlighted(true);
|
|
}
|
|
}
|
|
|
|
if (!create_edge_output_item_ && !create_edge_input_item_) {
|
|
// Determine if user clicked on a connector
|
|
if (NodeViewItemConnector *connector =
|
|
dynamic_cast<NodeViewItemConnector *>(item)) {
|
|
NodeViewItem *attached =
|
|
static_cast<NodeViewItem *>(connector->parentItem());
|
|
|
|
if (connector->IsOutput()) {
|
|
create_edge_output_item_ = attached;
|
|
} else {
|
|
create_edge_input_item_ = attached;
|
|
|
|
if (!create_edge_input_item_->edges().isEmpty()) {
|
|
// Drag existing edge instead
|
|
create_edge_ = create_edge_input_item_->edges().first();
|
|
create_edge_input_item_ = nullptr;
|
|
create_edge_output_item_ = create_edge_->from_item();
|
|
create_edge_already_exists_ = true;
|
|
} else {
|
|
create_edge_from_output_ = false;
|
|
create_edge_input_ =
|
|
create_edge_input_item_->GetInput();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((create_edge_output_item_ || create_edge_input_item_) &&
|
|
!create_edge_already_exists_) {
|
|
// Create a new edge from this output
|
|
create_edge_ = new NodeViewEdge();
|
|
create_edge_->SetCurved(scene_.GetEdgesAreCurved());
|
|
|
|
// Add edge to scene
|
|
scene_.addItem(create_edge_);
|
|
|
|
// Position edge to mouse cursor
|
|
PositionNewEdge(event->pos());
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Handle selections with the right mouse button
|
|
if (event->button() == Qt::RightButton) {
|
|
if (!item || !item->isSelected()) {
|
|
// Qt doesn't do this by default for some reason
|
|
if (!(event->modifiers() & Qt::ShiftModifier)) {
|
|
scene_.clearSelection();
|
|
}
|
|
|
|
// If there's an item here, select it
|
|
if (item) {
|
|
item->setSelected(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attached_items_.isEmpty()) {
|
|
// Default QGraphicsView functionality (selecting, dragging, etc.)
|
|
super::mousePressEvent(event);
|
|
}
|
|
|
|
// For any selected item, store its position in case the user is dragging it somewhere else
|
|
auto selected_items = scene_.GetSelectedItems();
|
|
foreach (NodeViewItem *i, selected_items) {
|
|
// Ignore items attached to the cursor
|
|
if (!IsItemAttachedToCursor(i)) {
|
|
dragging_items_.insert(i, i->GetNodePosition());
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::mouseMoveEvent(QMouseEvent *event)
|
|
{
|
|
if (HandMove(event))
|
|
return;
|
|
|
|
if (create_edge_) {
|
|
PositionNewEdge(event->pos());
|
|
return;
|
|
}
|
|
|
|
if (attached_items_.isEmpty()) {
|
|
super::mouseMoveEvent(event);
|
|
}
|
|
|
|
// See if there are any items attached
|
|
if (!attached_items_.isEmpty()) {
|
|
ProcessMovingAttachedNodes(event->pos());
|
|
}
|
|
}
|
|
|
|
void NodeView::mouseReleaseEvent(QMouseEvent *event)
|
|
{
|
|
if (HandRelease(event))
|
|
return;
|
|
|
|
if (create_edge_) {
|
|
EndEdgeDrag();
|
|
}
|
|
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
Node *select_context = nullptr;
|
|
QVector<Node *> select_nodes;
|
|
|
|
bool had_attached_items = !attached_items_.isEmpty();
|
|
|
|
if (!attached_items_.isEmpty()) {
|
|
select_context = GetContextAtMousePos(event->pos());
|
|
|
|
if (select_context) {
|
|
select_nodes = ProcessDroppingAttachedNodes(command, select_context,
|
|
event->pos());
|
|
} else {
|
|
QToolTip::showText(QCursor::pos(),
|
|
tr("Nodes must be placed inside a context."));
|
|
}
|
|
}
|
|
|
|
for (auto it = dragging_items_.cbegin(); it != dragging_items_.cend();
|
|
it++) {
|
|
NodeViewItem *i = it.key();
|
|
QPointF current_pos = i->GetNodePosition();
|
|
if (it.value() != current_pos) {
|
|
command->add_child(new NodeSetPositionCommand(
|
|
i->GetNode(), i->GetContext(), current_pos));
|
|
}
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(
|
|
command, tr("Moved %1 Node(s)").arg(dragging_items_.size()));
|
|
|
|
dragging_items_.clear();
|
|
|
|
if (!had_attached_items) {
|
|
super::mouseReleaseEvent(event);
|
|
}
|
|
|
|
if (select_context) {
|
|
DeselectAll();
|
|
scene_.context_map().value(select_context)->Select(select_nodes);
|
|
}
|
|
}
|
|
|
|
void NodeView::mouseDoubleClickEvent(QMouseEvent *event)
|
|
{
|
|
super::mouseDoubleClickEvent(event);
|
|
|
|
if (!(event->modifiers() & Qt::ControlModifier)) {
|
|
NodeViewItem *item_at_cursor =
|
|
dynamic_cast<NodeViewItem *>(itemAt(event->pos()));
|
|
if (item_at_cursor) {
|
|
item_at_cursor->ToggleExpanded();
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::dragEnterEvent(QDragEnterEvent *event)
|
|
{
|
|
if (contexts_.empty()) {
|
|
event->ignore();
|
|
return;
|
|
}
|
|
|
|
QStringList mime_fmts = event->mimeData()->formats();
|
|
|
|
if (mime_fmts.contains(Project::kItemMimeType)) {
|
|
QByteArray model_data = event->mimeData()->data(Project::kItemMimeType);
|
|
QDataStream stream(&model_data, QIODevice::ReadOnly);
|
|
|
|
// Variables to deserialize into
|
|
quintptr item_ptr;
|
|
QVector<Track::Reference> enabled_streams;
|
|
QVector<AttachedItem> new_attached;
|
|
|
|
int y = 0;
|
|
|
|
while (!stream.atEnd()) {
|
|
stream >> enabled_streams >> item_ptr;
|
|
|
|
// Get Item object
|
|
Node *item = reinterpret_cast<Node *>(item_ptr);
|
|
|
|
if (ViewerOutput *f = dynamic_cast<ViewerOutput *>(item)) {
|
|
NodeViewItem *new_item;
|
|
|
|
new_item = new NodeViewItem(f, nullptr);
|
|
new_item->SetFlowDirection(scene_.GetFlowDirection());
|
|
new_item->SetNodePosition(QPointF(0, y));
|
|
y++;
|
|
scene_.addItem(new_item);
|
|
|
|
new_attached.append({ new_item, f, new_item->pos() });
|
|
}
|
|
}
|
|
|
|
if (new_attached.empty()) {
|
|
event->ignore();
|
|
} else {
|
|
SetAttachedItems(new_attached);
|
|
|
|
event->accept();
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::dragMoveEvent(QDragMoveEvent *event)
|
|
{
|
|
if (attached_items_.empty()) {
|
|
event->ignore();
|
|
} else {
|
|
ProcessMovingAttachedNodes(event->pos());
|
|
|
|
if (GetContextAtMousePos(event->pos())) {
|
|
event->accept();
|
|
} else {
|
|
event->ignore();
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::dropEvent(QDropEvent *event)
|
|
{
|
|
if (Node *drop_ctx = GetContextAtMousePos(event->pos())) {
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
QVector<Node *> select_nodes =
|
|
ProcessDroppingAttachedNodes(command, drop_ctx, event->pos());
|
|
Core::instance()->undo_stack()->push(
|
|
command, tr("Dropped %1 Node(s)").arg(select_nodes.size()));
|
|
|
|
DeselectAll();
|
|
scene_.context_map().value(drop_ctx)->Select(select_nodes);
|
|
|
|
event->accept();
|
|
} else {
|
|
DetachItemsFromCursor(false);
|
|
event->ignore();
|
|
}
|
|
}
|
|
|
|
void NodeView::dragLeaveEvent(QDragLeaveEvent *event)
|
|
{
|
|
if (attached_items_.empty()) {
|
|
event->ignore();
|
|
} else {
|
|
DetachItemsFromCursor(false);
|
|
|
|
event->accept();
|
|
}
|
|
}
|
|
|
|
void NodeView::resizeEvent(QResizeEvent *event)
|
|
{
|
|
super::resizeEvent(event);
|
|
|
|
RepositionMiniMap();
|
|
|
|
if (overlay_view_) {
|
|
ResizeOverlay();
|
|
}
|
|
}
|
|
|
|
void NodeView::UpdateSelectionCache()
|
|
{
|
|
QVector<NodeViewItem *> current_selection = scene_.GetSelectedItems();
|
|
|
|
QVector<Node *> selected;
|
|
QVector<Node *> deselected;
|
|
|
|
QVector<Node::ContextPair> sel_with_ctx(current_selection.size());
|
|
|
|
// Determine which nodes are newly selected
|
|
for (int j = 0; j < current_selection.size(); j++) {
|
|
NodeViewItem *i = current_selection.at(j);
|
|
Node *n = i->GetNode();
|
|
if (!selected_nodes_.contains(n)) {
|
|
selected.append(n);
|
|
selected_nodes_.append(n);
|
|
}
|
|
|
|
sel_with_ctx[j] = { n, i->GetContext() };
|
|
}
|
|
|
|
// Determine which nodes are newly deselected
|
|
if (current_selection.isEmpty()) {
|
|
// All nodes that were selected have been deselected, so we'll just set them all to `deselected`
|
|
deselected = selected_nodes_;
|
|
selected_nodes_.clear();
|
|
} else {
|
|
foreach (Node *n, selected_nodes_) {
|
|
bool still_selected = false;
|
|
|
|
foreach (NodeViewItem *i, current_selection) {
|
|
if (i->GetNode() == n) {
|
|
still_selected = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!still_selected) {
|
|
deselected.append(n);
|
|
selected_nodes_.removeOne(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!deselected.isEmpty()) {
|
|
emit NodesDeselected(deselected);
|
|
}
|
|
|
|
if (!selected.isEmpty()) {
|
|
emit NodesSelected(selected);
|
|
}
|
|
|
|
if (!dont_emit_selection_signals_) {
|
|
emit NodeSelectionChanged(selected_nodes_);
|
|
emit NodeSelectionChangedWithContexts(sel_with_ctx);
|
|
}
|
|
}
|
|
|
|
void NodeView::ShowContextMenu(const QPoint &pos)
|
|
{
|
|
if (contexts_.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Menu m;
|
|
|
|
MenuShared::instance()->AddItemsForEditMenu(&m, false);
|
|
|
|
m.addSeparator();
|
|
|
|
QVector<NodeViewItem *> selected = scene_.GetSelectedItems();
|
|
|
|
if (itemAt(pos) && !selected.isEmpty()) {
|
|
// Grouping
|
|
if (selected.size() == 1 &&
|
|
dynamic_cast<NodeGroup *>(selected.first()->GetNode())) {
|
|
QAction *ungroup_action = m.addAction(tr("Ungroup"));
|
|
connect(ungroup_action, &QAction::triggered, this,
|
|
&NodeView::UngroupNodes);
|
|
} else {
|
|
QAction *group_action = m.addAction(tr("Group"));
|
|
connect(group_action, &QAction::triggered, this,
|
|
&NodeView::GroupNodes);
|
|
}
|
|
|
|
// Color menu
|
|
MenuShared::instance()->AddColorCodingMenu(&m);
|
|
|
|
// Show in Viewer option for nodes based on Viewer
|
|
if (ViewerOutput *viewer =
|
|
dynamic_cast<ViewerOutput *>(selected.first()->GetNode())) {
|
|
Q_UNUSED(viewer)
|
|
m.addSeparator();
|
|
QAction *open_in_viewer_action = m.addAction(tr("Open in Viewer"));
|
|
connect(open_in_viewer_action, &QAction::triggered, this,
|
|
&NodeView::OpenSelectedNodeInViewer);
|
|
}
|
|
|
|
m.addSeparator();
|
|
|
|
// Show in Parameter Editor
|
|
QAction *show_in_param_editor_action = m.addAction(
|
|
tr("Show in Parameter Editor"));
|
|
show_in_param_editor_action->setShortcut(
|
|
show_in_param_editor_action_->shortcut());
|
|
connect(show_in_param_editor_action, &QAction::triggered, this,
|
|
&NodeView::ShowSelectedNodeInParamEditor);
|
|
|
|
// Properties
|
|
QAction *properties_action = m.addAction(tr("P&roperties"));
|
|
connect(properties_action, &QAction::triggered, this,
|
|
&NodeView::ShowNodeProperties);
|
|
|
|
} else {
|
|
QAction *curved_action = m.addAction(tr("Smooth Edges"));
|
|
curved_action->setCheckable(true);
|
|
curved_action->setChecked(scene_.GetEdgesAreCurved());
|
|
connect(curved_action, &QAction::triggered, &scene_,
|
|
&NodeViewScene::SetEdgesAreCurved);
|
|
|
|
m.addSeparator();
|
|
|
|
Menu *direction_menu = new Menu(tr("Direction"), &m);
|
|
m.addMenu(direction_menu);
|
|
|
|
direction_menu->AddActionWithData(tr("Top to Bottom"),
|
|
NodeViewCommon::kTopToBottom,
|
|
scene_.GetFlowDirection());
|
|
|
|
direction_menu->AddActionWithData(tr("Bottom to Top"),
|
|
NodeViewCommon::kBottomToTop,
|
|
scene_.GetFlowDirection());
|
|
|
|
direction_menu->AddActionWithData(tr("Left to Right"),
|
|
NodeViewCommon::kLeftToRight,
|
|
scene_.GetFlowDirection());
|
|
|
|
direction_menu->AddActionWithData(tr("Right to Left"),
|
|
NodeViewCommon::kRightToLeft,
|
|
scene_.GetFlowDirection());
|
|
|
|
connect(direction_menu, &Menu::triggered, this,
|
|
&NodeView::ContextMenuSetDirection);
|
|
|
|
m.addSeparator();
|
|
|
|
Menu *add_menu = CreateAddMenu(&m);
|
|
m.addMenu(add_menu);
|
|
}
|
|
|
|
m.exec(mapToGlobal(pos));
|
|
}
|
|
|
|
void NodeView::CreateNodeSlot(QAction *action)
|
|
{
|
|
Node *new_node = NodeFactory::CreateFromMenuAction(action);
|
|
|
|
if (new_node) {
|
|
NodeViewItem *new_item = new NodeViewItem(new_node, nullptr);
|
|
new_item->SetFlowDirection(scene_.GetFlowDirection());
|
|
scene_.addItem(new_item);
|
|
|
|
QVector<AttachedItem> new_attached;
|
|
|
|
new_attached.append({ new_item, new_node, QPointF(0, 0) });
|
|
|
|
if (NodeGroup *new_group = dynamic_cast<NodeGroup *>(new_node)) {
|
|
for (auto it = new_group->GetContextPositions().cbegin();
|
|
it != new_group->GetContextPositions().cend(); it++) {
|
|
new_attached.append({ nullptr, it.key(), QPointF(0, 0) });
|
|
}
|
|
}
|
|
|
|
SetAttachedItems(new_attached);
|
|
}
|
|
}
|
|
|
|
void NodeView::ContextMenuSetDirection(QAction *action)
|
|
{
|
|
SetFlowDirection(
|
|
static_cast<NodeViewCommon::FlowDirection>(action->data().toInt()));
|
|
}
|
|
|
|
void NodeView::OpenSelectedNodeInViewer()
|
|
{
|
|
// Find first viewer in list of selected nodes and open it
|
|
foreach (Node *n, selected_nodes_) {
|
|
if (ViewerOutput *viewer = dynamic_cast<ViewerOutput *>(n)) {
|
|
Core::instance()->OpenNodeInViewer(viewer);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::UpdateSceneBoundingRect()
|
|
{
|
|
// Get current items bounding rect
|
|
QRectF r = scene_.itemsBoundingRect();
|
|
|
|
// Adjust so that it fills the view
|
|
r.adjust(-width(), -height(), width(), height());
|
|
|
|
// Set it
|
|
scene_.setSceneRect(r);
|
|
}
|
|
|
|
void NodeView::CenterOnItemsBoundingRect()
|
|
{
|
|
centerOn(scene_.itemsBoundingRect().center());
|
|
}
|
|
|
|
void NodeView::CenterOnNode(Node *n)
|
|
{
|
|
foreach (NodeViewContext *ctx, scene_.context_map()) {
|
|
if (NodeViewItem *item = ctx->GetItemFromMap(n)) {
|
|
centerOn(item);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::RepositionMiniMap()
|
|
{
|
|
if (minimap_->isVisible()) {
|
|
int margin = fontMetrics().height();
|
|
|
|
int w = width() - minimap_->width() - margin;
|
|
int h = height() - minimap_->height() - margin;
|
|
|
|
if (verticalScrollBar()->isVisible()) {
|
|
w -= verticalScrollBar()->width();
|
|
}
|
|
|
|
if (horizontalScrollBar()->isVisible()) {
|
|
h -= horizontalScrollBar()->height();
|
|
}
|
|
|
|
minimap_->move(w, h);
|
|
|
|
UpdateViewportOnMiniMap();
|
|
}
|
|
}
|
|
|
|
void NodeView::UpdateViewportOnMiniMap()
|
|
{
|
|
if (minimap_->isVisible()) {
|
|
minimap_->SetViewportRect(mapToScene(viewport()->rect()));
|
|
}
|
|
}
|
|
|
|
void NodeView::MoveToScenePoint(const QPointF &pos)
|
|
{
|
|
centerOn(pos);
|
|
}
|
|
|
|
void NodeView::NodeRemovedFromGraph()
|
|
{
|
|
Node *context = static_cast<Node *>(sender());
|
|
|
|
RemoveContext(context);
|
|
|
|
contexts_.removeOne(context);
|
|
}
|
|
|
|
void NodeView::DetachItemsFromCursor(bool delete_nodes_too)
|
|
{
|
|
foreach (const AttachedItem &ai, attached_items_) {
|
|
delete ai.item;
|
|
|
|
if (delete_nodes_too) {
|
|
qDebug() << "deleting" << ai.node;
|
|
delete ai.node;
|
|
}
|
|
}
|
|
|
|
attached_items_.clear();
|
|
}
|
|
|
|
void NodeView::SetFlowDirection(NodeViewCommon::FlowDirection dir)
|
|
{
|
|
scene_.SetFlowDirection(dir);
|
|
}
|
|
|
|
void NodeView::MoveAttachedNodesToCursor(const QPoint &p)
|
|
{
|
|
QPointF item_pos = mapToScene(p);
|
|
|
|
for (const AttachedItem &i : qAsConst(attached_items_)) {
|
|
if (i.item) {
|
|
i.item->setPos(item_pos + i.original_pos);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::ProcessMovingAttachedNodes(const QPoint &pos)
|
|
{
|
|
// Move those items to the cursor
|
|
MoveAttachedNodesToCursor(pos);
|
|
|
|
// See if the user clicked on an edge (only when dropping single nodes)
|
|
if (attached_items_.size() == 1) {
|
|
Node *attached_node = attached_items_.first().item->GetNode();
|
|
|
|
QRect edge_detect_rect(pos, pos);
|
|
|
|
int edge_detect_radius = fontMetrics().height();
|
|
edge_detect_rect.adjust(-edge_detect_radius, -edge_detect_radius,
|
|
edge_detect_radius, edge_detect_radius);
|
|
|
|
QList<QGraphicsItem *> items = this->items(edge_detect_rect);
|
|
|
|
NodeViewEdge *new_drop_edge = nullptr;
|
|
|
|
// See if there is an edge here
|
|
for (QGraphicsItem *item : qAsConst(items)) {
|
|
new_drop_edge = dynamic_cast<NodeViewEdge *>(item);
|
|
|
|
if (new_drop_edge) {
|
|
drop_input_.Reset();
|
|
|
|
NodeValue::Type drop_edge_data_type =
|
|
new_drop_edge->input().GetDataType();
|
|
|
|
// Determine best input to connect to our new node
|
|
if (attached_node->GetEffectInput().IsValid()) {
|
|
// If node specifies an effect input, use that immediately
|
|
drop_input_ = attached_node->GetEffectInput();
|
|
} else {
|
|
// Otherwise, we may have to iterate to find a valid one
|
|
for (const QString &input : attached_node->inputs()) {
|
|
if (input == Node::kEnabledInput) {
|
|
// Ignore enabled input
|
|
continue;
|
|
}
|
|
|
|
NodeInput i(attached_node, input);
|
|
|
|
if (attached_node->IsInputConnectable(input)) {
|
|
if (attached_node->GetInputDataType(input) ==
|
|
drop_edge_data_type) {
|
|
// Found exactly the type we're looking for, set and break this loop
|
|
drop_input_ = i;
|
|
break;
|
|
} else if (!drop_input_.IsValid()) {
|
|
// Default to first connectable input
|
|
drop_input_ = i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (attached_node->InputsFrom(new_drop_edge->input().node(),
|
|
true)) {
|
|
drop_input_.Reset();
|
|
}
|
|
|
|
if (drop_input_.IsValid()) {
|
|
break;
|
|
} else {
|
|
new_drop_edge = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (drop_edge_ != new_drop_edge) {
|
|
if (drop_edge_) {
|
|
drop_edge_->SetHighlighted(false);
|
|
}
|
|
|
|
drop_edge_ = new_drop_edge;
|
|
|
|
if (drop_edge_) {
|
|
drop_edge_->SetHighlighted(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector<Node *>
|
|
NodeView::ProcessDroppingAttachedNodes(MultiUndoCommand *command,
|
|
Node *select_context, const QPoint &pos)
|
|
{
|
|
QVector<Node *> select_nodes;
|
|
|
|
// Make a copy
|
|
QVector<AttachedItem> attached = attached_items_;
|
|
|
|
for (int i = 0; i < attached.size(); i++) {
|
|
const AttachedItem &ai = attached.at(i);
|
|
|
|
if (ai.node->InputsFrom(select_context, true)) {
|
|
attached.removeAt(i);
|
|
} else if (select_context->ContextContainsNode(ai.node)) {
|
|
select_nodes.append(ai.node);
|
|
attached.removeAt(i);
|
|
}
|
|
}
|
|
|
|
{
|
|
MultiUndoCommand *add_command = new MultiUndoCommand();
|
|
|
|
foreach (const AttachedItem &ai, attached) {
|
|
// Add node to the same graph that the context is in
|
|
if (ai.node->parent() != select_context->parent()) {
|
|
add_command->add_child(
|
|
new NodeAddCommand(select_context->parent(), ai.node));
|
|
if (ai.node->IsItem() && !ai.node->folder()) {
|
|
add_command->add_child(new FolderAddChild(
|
|
select_context->parent()->root(), ai.node));
|
|
}
|
|
}
|
|
|
|
// Add node to the context
|
|
if (ai.item) {
|
|
select_nodes.append(ai.node);
|
|
add_command->add_child(new NodeSetPositionCommand(
|
|
ai.node, select_context,
|
|
scene_.context_map()
|
|
.value(select_context)
|
|
->MapScenePosToNodePosInContext(ai.item->pos())));
|
|
}
|
|
}
|
|
|
|
if (add_command->child_count()) {
|
|
add_command->redo_now();
|
|
command->add_child(add_command);
|
|
} else {
|
|
delete add_command;
|
|
}
|
|
}
|
|
|
|
{
|
|
// Dropped attached item onto an edge, connect it between them
|
|
MultiUndoCommand *drop_edge_command = new MultiUndoCommand();
|
|
if (attached.size() == 1) {
|
|
Node *dropping_node = nullptr;
|
|
|
|
foreach (const AttachedItem &ai, attached) {
|
|
if (ai.item && !ai.node->InputsFrom(select_context, true)) {
|
|
dropping_node = ai.node;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (dropping_node && drop_edge_) {
|
|
// Remove old edge
|
|
drop_edge_command->add_child(new NodeEdgeRemoveCommand(
|
|
drop_edge_->output(), drop_edge_->input()));
|
|
|
|
// Place new edges
|
|
drop_edge_command->add_child(
|
|
new NodeEdgeAddCommand(drop_edge_->output(), drop_input_));
|
|
drop_edge_command->add_child(
|
|
new NodeEdgeAddCommand(dropping_node, drop_edge_->input()));
|
|
}
|
|
|
|
drop_edge_ = nullptr;
|
|
}
|
|
if (drop_edge_command->child_count()) {
|
|
drop_edge_command->redo_now();
|
|
command->add_child(drop_edge_command);
|
|
} else {
|
|
delete drop_edge_command;
|
|
}
|
|
}
|
|
|
|
DetachItemsFromCursor(false);
|
|
|
|
return select_nodes;
|
|
}
|
|
|
|
Node *NodeView::GetContextAtMousePos(const QPoint &p)
|
|
{
|
|
QList<QGraphicsItem *> items_at_cursor = this->items(p);
|
|
foreach (QGraphicsItem *i, items_at_cursor) {
|
|
if (NodeViewContext *context_item =
|
|
dynamic_cast<NodeViewContext *>(i)) {
|
|
return context_item->GetContext();
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void NodeView::ConnectSelectionChangedSignal()
|
|
{
|
|
connect(&scene_, &QGraphicsScene::selectionChanged, this,
|
|
&NodeView::UpdateSelectionCache);
|
|
}
|
|
|
|
void NodeView::DisconnectSelectionChangedSignal()
|
|
{
|
|
disconnect(&scene_, &QGraphicsScene::selectionChanged, this,
|
|
&NodeView::UpdateSelectionCache);
|
|
}
|
|
|
|
void NodeView::ZoomIntoCursorPosition(QWheelEvent *event, double multiplier,
|
|
const QPointF &cursor_pos)
|
|
{
|
|
Q_UNUSED(event)
|
|
|
|
double test_scale = scale_ * multiplier;
|
|
|
|
if (test_scale > kMinimumScale) {
|
|
int anchor_x =
|
|
qRound(double(cursor_pos.x() + horizontalScrollBar()->value()) /
|
|
scale_ * test_scale -
|
|
cursor_pos.x());
|
|
int anchor_y =
|
|
qRound(double(cursor_pos.y() + verticalScrollBar()->value()) /
|
|
scale_ * test_scale -
|
|
cursor_pos.y());
|
|
|
|
scale(multiplier, multiplier);
|
|
|
|
this->horizontalScrollBar()->setValue(anchor_x);
|
|
this->verticalScrollBar()->setValue(anchor_y);
|
|
|
|
scale_ = test_scale;
|
|
}
|
|
}
|
|
|
|
bool NodeView::event(QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::ShortcutOverride) {
|
|
QKeyEvent *se = static_cast<QKeyEvent *>(event);
|
|
if (se->key() == Qt::Key_Left || se->key() == Qt::Key_Right ||
|
|
se->key() == Qt::Key_Up || se->key() == Qt::Key_Down) {
|
|
se->accept();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return super::event(event);
|
|
}
|
|
|
|
bool NodeView::eventFilter(QObject *object, QEvent *event)
|
|
{
|
|
return super::eventFilter(object, event);
|
|
}
|
|
|
|
void NodeView::changeEvent(QEvent *e)
|
|
{
|
|
// Add translation code
|
|
|
|
super::changeEvent(e);
|
|
}
|
|
|
|
void NodeView::ZoomFromKeyboard(double multiplier)
|
|
{
|
|
QPoint cursor_pos = mapFromGlobal(QCursor::pos());
|
|
|
|
// If the cursor is not currently within the widget, zoom into the center
|
|
if (!rect().contains(cursor_pos)) {
|
|
cursor_pos = QPoint(width() / 2, height() / 2);
|
|
}
|
|
|
|
ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos);
|
|
}
|
|
|
|
void NodeView::ClearCreateEdgeInputIfNecessary()
|
|
{
|
|
if (create_edge_from_output_ && create_edge_input_.IsValid()) {
|
|
create_edge_input_.Reset();
|
|
}
|
|
}
|
|
|
|
QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item,
|
|
Node *context) const
|
|
{
|
|
return item->GetNodePosition() - context_offsets_.value(context);
|
|
}
|
|
|
|
NodeViewItem *NodeView::GetAssumedItemForSelectedNode(Node *node)
|
|
{
|
|
// Try to find corresponding selected item
|
|
foreach (NodeViewContext *ctx, scene_.context_map()) {
|
|
NodeViewItem *item = ctx->GetItemFromMap(node);
|
|
if (item && item->GetNode() == node && item->isSelected()) {
|
|
// Good enough
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool NodeView::GetAssumedPositionForSelectedNode(Node *node,
|
|
Node::Position *pos)
|
|
{
|
|
if (NodeViewItem *item = GetAssumedItemForSelectedNode(node)) {
|
|
*pos = item->GetNodePositionData();
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Menu *NodeView::CreateAddMenu(Menu *parent)
|
|
{
|
|
Menu *add_menu = NodeFactory::CreateMenu(parent);
|
|
add_menu->setTitle(tr("Add"));
|
|
connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot);
|
|
return add_menu;
|
|
}
|
|
|
|
void NodeView::PositionNewEdge(const QPoint &pos)
|
|
{
|
|
// Determine scene coordinate
|
|
QPointF scene_pt = mapToScene(pos);
|
|
|
|
// Find if the cursor is currently inside an item
|
|
NodeViewItem *item_at_cursor = dynamic_cast<NodeViewItem *>(itemAt(pos));
|
|
|
|
NodeViewItem *source_item = create_edge_from_output_ ?
|
|
create_edge_output_item_ :
|
|
create_edge_input_item_;
|
|
NodeViewItem *&opposing_item = create_edge_from_output_ ?
|
|
create_edge_input_item_ :
|
|
create_edge_output_item_;
|
|
|
|
// Filter out connecting to self
|
|
if (item_at_cursor && item_at_cursor->GetNode() == source_item->GetNode()) {
|
|
item_at_cursor = nullptr;
|
|
}
|
|
|
|
// Collapse any items that the cursor is no longer inside
|
|
int i = create_edge_expanded_items_.size() - 1;
|
|
for (; i >= 0; i--) {
|
|
NodeViewItem *nvi = create_edge_expanded_items_.at(i);
|
|
QPointF local_pt = nvi->mapFromScene(scene_pt);
|
|
|
|
if (nvi->scene() == &scene_ &&
|
|
(nvi->contains(local_pt) ||
|
|
(!nvi->IsOutputItem() &&
|
|
nvi->parentItem()->contains(
|
|
nvi->parentItem()->mapFromScene(scene_pt)) &&
|
|
local_pt.y() > nvi->rect().bottom()))) {
|
|
break;
|
|
} else {
|
|
// Collapsing an item will destroy its children, so if the cursor item happens to be a child
|
|
// of the item we're about to collapse, set it to null
|
|
if (item_at_cursor && item_at_cursor->parentItem() == nvi) {
|
|
item_at_cursor = nullptr;
|
|
}
|
|
|
|
if (opposing_item && opposing_item->parentItem() == nvi) {
|
|
opposing_item = nullptr;
|
|
ClearCreateEdgeInputIfNecessary();
|
|
}
|
|
|
|
CollapseItem(nvi);
|
|
}
|
|
}
|
|
create_edge_expanded_items_.resize(i + 1);
|
|
|
|
// Expand item if possible
|
|
if (item_at_cursor && item_at_cursor->CanBeExpanded() &&
|
|
!item_at_cursor->IsExpanded() && create_edge_from_output_) {
|
|
ExpandItem(item_at_cursor);
|
|
create_edge_expanded_items_.append(item_at_cursor);
|
|
}
|
|
|
|
// Filter out connecting to a node that connects to us or an item of the same type
|
|
if (item_at_cursor &&
|
|
((create_edge_from_output_ && source_item->GetNode()->InputsFrom(
|
|
item_at_cursor->GetNode(), true)) ||
|
|
(!create_edge_from_output_ && item_at_cursor->GetNode()->InputsFrom(
|
|
source_item->GetNode(), true)) ||
|
|
(create_edge_from_output_ == item_at_cursor->IsOutputItem()))) {
|
|
item_at_cursor = nullptr;
|
|
}
|
|
|
|
// Filter out "output node" of the context, we assume users won't want to fetch the output of this
|
|
if (item_at_cursor && !create_edge_from_output_ &&
|
|
item_at_cursor->IsLabelledAsOutputOfContext()) {
|
|
item_at_cursor = nullptr;
|
|
}
|
|
|
|
// If the item has changed
|
|
if (item_at_cursor != opposing_item) {
|
|
// If we had a destination active, disconnect from it since the item has changed
|
|
if (opposing_item) {
|
|
opposing_item->SetHighlighted(false);
|
|
opposing_item = nullptr;
|
|
}
|
|
|
|
// Clear cached input
|
|
ClearCreateEdgeInputIfNecessary();
|
|
|
|
// If this is an input and we're
|
|
opposing_item = item_at_cursor;
|
|
|
|
if (opposing_item) {
|
|
opposing_item->SetHighlighted(true);
|
|
if (!opposing_item->IsOutputItem()) {
|
|
create_edge_input_ = opposing_item->GetInput();
|
|
}
|
|
}
|
|
}
|
|
|
|
QPointF output_point = create_edge_output_item_ ?
|
|
create_edge_output_item_->GetOutputPoint() :
|
|
scene_pt;
|
|
QPointF input_point = create_edge_input_.IsValid() ?
|
|
create_edge_input_item_->GetInputPoint() :
|
|
scene_pt;
|
|
|
|
create_edge_->SetPoints(output_point, input_point);
|
|
create_edge_->SetConnected(create_edge_output_item_ &&
|
|
create_edge_input_.IsValid());
|
|
}
|
|
|
|
void NodeView::GroupNodes()
|
|
{
|
|
// Get items
|
|
QVector<NodeViewItem *> items = scene_.GetSelectedItems();
|
|
if (items.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// Get node context
|
|
Node *context = items.first()->GetContext();
|
|
QPointF avg_pos = items.first()->GetNodePosition();
|
|
for (int i = 1; i < items.size(); i++) {
|
|
if (items.at(i)->GetContext() != context) {
|
|
QMessageBox::critical(
|
|
this, tr("Failed to group nodes"),
|
|
tr("Nodes can only be grouped if they're in the same context."));
|
|
return;
|
|
}
|
|
|
|
avg_pos += items.at(i)->GetNodePosition();
|
|
}
|
|
avg_pos /= items.size();
|
|
|
|
// Create group
|
|
NodeGroup *group = new NodeGroup();
|
|
|
|
// Add group to graph and context
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
// Add nodes to group
|
|
Node *output_passthrough = nullptr;
|
|
QVector<Node *> nodes_to_group = selected_nodes_;
|
|
DeselectAll();
|
|
foreach (Node *n, nodes_to_group) {
|
|
command->add_child(
|
|
new NodeRemovePositionFromContextCommand(n, context));
|
|
command->add_child(new NodeSetPositionCommand(
|
|
n, group, context->GetNodePositionDataInContext(n)));
|
|
|
|
for (auto it = n->inputs().cbegin(); it != n->inputs().cend(); it++) {
|
|
NodeInput input(n, *it, -1);
|
|
|
|
if (!input.IsConnected() ||
|
|
!nodes_to_group.contains(input.GetConnectedOutput())) {
|
|
command->add_child(
|
|
new NodeGroupAddInputPassthrough(group, input));
|
|
}
|
|
}
|
|
|
|
if (!output_passthrough) {
|
|
// Default to the first node we find that doesn't output to a node inside the group
|
|
output_passthrough = nodes_to_group.first();
|
|
foreach (Node *potential_in, nodes_to_group) {
|
|
if (potential_in != n && !potential_in->InputsFrom(n, false)) {
|
|
output_passthrough = n;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set output passthrough
|
|
command->add_child(
|
|
new NodeGroupSetOutputPassthrough(group, output_passthrough));
|
|
|
|
// Add group to graph
|
|
command->add_child(new NodeAddCommand(context->parent(), group));
|
|
command->add_child(new NodeSetPositionCommand(group, context, avg_pos));
|
|
|
|
// Do command
|
|
Core::instance()->LabelNodes({ group }, command);
|
|
|
|
Core::instance()->undo_stack()->push(command, tr("Grouped Nodes"));
|
|
}
|
|
|
|
void NodeView::UngroupNodes()
|
|
{
|
|
NodeViewItem *group_item = nullptr;
|
|
QVector<NodeViewItem *> items = scene_.GetSelectedItems();
|
|
if (items.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
NodeGroup *group = nullptr;
|
|
foreach (NodeViewItem *i, items) {
|
|
if ((group = dynamic_cast<NodeGroup *>(i->GetNode()))) {
|
|
group_item = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!group_item) {
|
|
return;
|
|
}
|
|
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
Node *context = group_item->GetContext();
|
|
|
|
command->add_child(
|
|
new NodeRemovePositionFromContextCommand(group, context));
|
|
command->add_child(new NodeRemoveAndDisconnectCommand(group));
|
|
|
|
for (auto it = group->GetContextPositions().cbegin();
|
|
it != group->GetContextPositions().cend(); it++) {
|
|
command->add_child(
|
|
new NodeRemovePositionFromContextCommand(it.key(), group));
|
|
command->add_child(new NodeSetPositionCommand(
|
|
it.key(), context, group->GetNodePositionDataInContext(it.key())));
|
|
}
|
|
|
|
Core::instance()->undo_stack()->push(command, tr("Ungrouped Nodes"));
|
|
}
|
|
|
|
void NodeView::ShowNodeProperties()
|
|
{
|
|
Node *first_node = selected_nodes_.first();
|
|
|
|
if (NodeGroup *group = dynamic_cast<NodeGroup *>(first_node)) {
|
|
if (!overlay_view_) {
|
|
overlay_view_ = new NodeView(this);
|
|
overlay_view_->show();
|
|
|
|
QPushButton *overlay_close_btn = new QPushButton(overlay_view_);
|
|
overlay_close_btn->setIcon(icon::Error);
|
|
int offset = overlay_close_btn->sizeHint().width() / 2;
|
|
overlay_close_btn->move(offset, offset);
|
|
overlay_close_btn->show();
|
|
|
|
connect(overlay_view_, &NodeView::NodesSelected, this,
|
|
&NodeView::NodesSelected);
|
|
connect(overlay_view_, &NodeView::NodesDeselected, this,
|
|
&NodeView::NodesDeselected);
|
|
connect(overlay_view_, &NodeView::NodeGroupOpened, this,
|
|
&NodeView::NodeGroupOpened);
|
|
connect(overlay_view_, &NodeView::NodeGroupClosed, this,
|
|
&NodeView::NodeGroupClosed);
|
|
connect(overlay_view_, &NodeView::EscPressed, this,
|
|
&NodeView::CloseOverlay);
|
|
connect(overlay_close_btn, &QPushButton::clicked, this,
|
|
&NodeView::CloseOverlay);
|
|
|
|
const QColor &bgcol = overlay_view_->palette().base().color();
|
|
overlay_view_->setStyleSheet(
|
|
QStringLiteral(
|
|
"QGraphicsView { background: rgba(%1, %2, %3, 0.8); }")
|
|
.arg(QString::number(bgcol.red()),
|
|
QString::number(bgcol.green()),
|
|
QString::number(bgcol.blue())));
|
|
|
|
overlay_close_btn->setStyleSheet(
|
|
QStringLiteral("background: transparent; border: none;"));
|
|
}
|
|
overlay_view_->SetContexts({ group });
|
|
ResizeOverlay();
|
|
QMetaObject::invokeMethod(overlay_view_,
|
|
&NodeView::CenterOnItemsBoundingRect,
|
|
Qt::QueuedConnection);
|
|
overlay_view_->setFocus();
|
|
|
|
emit NodesDeselected(selected_nodes_);
|
|
emit NodeSelectionChanged(QVector<Node *>());
|
|
emit NodeSelectionChangedWithContexts(QVector<Node::ContextPair>());
|
|
overlay_view_->SelectAll();
|
|
|
|
emit NodeGroupOpened(group);
|
|
} else {
|
|
LabelSelectedNodes();
|
|
}
|
|
}
|
|
|
|
void NodeView::ShowSelectedNodeInParamEditor()
|
|
{
|
|
if (selected_nodes_.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
NodeViewItem *item = scene_.GetSelectedItems().first();
|
|
if (!item || !item->GetNode()) {
|
|
return;
|
|
}
|
|
|
|
if (PanelManager::instance()) {
|
|
if (PanelWidget *panel = PanelManager::instance()->GetPanelWithName(
|
|
QStringLiteral("ParamPanel"))) {
|
|
panel->show();
|
|
panel->raise();
|
|
panel->setFocus(Qt::OtherFocusReason);
|
|
}
|
|
}
|
|
|
|
emit NodeSelectionChangedWithContexts(
|
|
{ { item->GetNode(), item->GetContext() } });
|
|
}
|
|
|
|
void NodeView::LabelSelectedNodes()
|
|
{
|
|
Core::instance()->LabelNodes(selected_nodes_);
|
|
}
|
|
|
|
void NodeView::ItemAboutToBeDeleted(NodeViewItem *item)
|
|
{
|
|
dragging_items_.remove(item);
|
|
|
|
if (create_edge_) {
|
|
// Item should be removed from scene, but not yet deleted, allowing a safe PositionNewEdge call
|
|
// to disconnect
|
|
PositionNewEdge(mapFromGlobal(QCursor::pos()));
|
|
|
|
QGraphicsItem *test = item;
|
|
do {
|
|
if (test == item) {
|
|
break;
|
|
}
|
|
|
|
test = test->parentItem();
|
|
} while (test);
|
|
|
|
if (test == item) {
|
|
// Cancel edge function
|
|
EndEdgeDrag(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void NodeView::CloseOverlay()
|
|
{
|
|
if (overlay_view_->overlay_view_) {
|
|
overlay_view_->CloseOverlay();
|
|
}
|
|
|
|
overlay_view_->deleteLater();
|
|
overlay_view_ = nullptr;
|
|
emit NodeGroupClosed();
|
|
}
|
|
|
|
void NodeView::AddContext(Node *n)
|
|
{
|
|
NodeViewContext *ctx = scene_.AddContext(n);
|
|
|
|
connect(ctx, &NodeViewContext::ItemAboutToBeDeleted, this,
|
|
&NodeView::ItemAboutToBeDeleted);
|
|
|
|
connect(n, &Node::RemovedFromGraph, this, &NodeView::NodeRemovedFromGraph);
|
|
}
|
|
|
|
void NodeView::RemoveContext(Node *n)
|
|
{
|
|
scene_.RemoveContext(n);
|
|
disconnect(n, &Node::RemovedFromGraph, this,
|
|
&NodeView::NodeRemovedFromGraph);
|
|
}
|
|
|
|
bool NodeView::IsItemAttachedToCursor(NodeViewItem *item) const
|
|
{
|
|
foreach (const AttachedItem &ai, attached_items_) {
|
|
if (ai.item == item) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void NodeView::ExpandItem(NodeViewItem *item)
|
|
{
|
|
item->SetExpanded(true);
|
|
item->setZValue(100);
|
|
}
|
|
|
|
void NodeView::CollapseItem(NodeViewItem *item)
|
|
{
|
|
item->SetExpanded(false);
|
|
item->setZValue(0);
|
|
}
|
|
|
|
void NodeView::EndEdgeDrag(bool cancel)
|
|
{
|
|
// Check if the edge was reconnected to the same place as before
|
|
MultiUndoCommand *command = new MultiUndoCommand();
|
|
|
|
bool reconnected_to_itself = false;
|
|
|
|
if (create_edge_already_exists_) {
|
|
if (!cancel) {
|
|
if (create_edge_output_item_ == create_edge_->from_item() &&
|
|
create_edge_->input() == create_edge_input_) {
|
|
reconnected_to_itself = true;
|
|
} else {
|
|
// We are moving (or removing) an existing edge
|
|
command->add_child(new NodeEdgeRemoveCommand(
|
|
create_edge_->output(), create_edge_->input()));
|
|
}
|
|
}
|
|
} else {
|
|
// We're creating a new edge, which means this UI object is only temporary
|
|
delete create_edge_;
|
|
}
|
|
|
|
create_edge_ = nullptr;
|
|
|
|
// Clear highlight if we set one
|
|
if (create_edge_output_item_) {
|
|
create_edge_output_item_->SetHighlighted(false);
|
|
}
|
|
if (create_edge_input_item_) {
|
|
create_edge_input_item_->SetHighlighted(false);
|
|
}
|
|
|
|
QString command_name;
|
|
|
|
NodeInput &creating_input = create_edge_input_;
|
|
if (create_edge_output_item_ && create_edge_input_item_ && !cancel) {
|
|
if (creating_input.IsValid()) {
|
|
// Make connection
|
|
if (!reconnected_to_itself) {
|
|
Node *creating_output = create_edge_output_item_->GetNode();
|
|
|
|
while (NodeGroup *output_group =
|
|
dynamic_cast<NodeGroup *>(creating_output)) {
|
|
creating_output = output_group->GetOutputPassthrough();
|
|
}
|
|
|
|
while (NodeGroup *input_group =
|
|
dynamic_cast<NodeGroup *>(creating_input.node())) {
|
|
creating_input =
|
|
input_group->GetInputFromID(creating_input.input());
|
|
}
|
|
|
|
if (creating_input.IsConnected()) {
|
|
Node::OutputConnection existing_edge_to_remove = {
|
|
creating_input.GetConnectedOutput(), creating_input
|
|
};
|
|
|
|
Node *already_connected_output =
|
|
creating_input.GetConnectedOutput();
|
|
NodeViewContext *ctx =
|
|
GetContextItemFromNodeItem(create_edge_input_item_);
|
|
if (ctx && !ctx->GetItemFromMap(already_connected_output)) {
|
|
if (QMessageBox::warning(
|
|
this, QString(),
|
|
tr("Input \"%1\" is currently connected to node \"%2\", which is not visible in this context. "
|
|
"By connecting this, that connection will be removed. Do you wish to continue?")
|
|
.arg(creating_input.name(),
|
|
already_connected_output
|
|
->GetLabelAndName()),
|
|
QMessageBox::Yes | QMessageBox::No) ==
|
|
QMessageBox::No) {
|
|
cancel = true;
|
|
}
|
|
}
|
|
|
|
if (!cancel) {
|
|
command->add_child(new NodeEdgeRemoveCommand(
|
|
existing_edge_to_remove.first,
|
|
existing_edge_to_remove.second));
|
|
}
|
|
}
|
|
|
|
if (!cancel) {
|
|
command->add_child(new NodeEdgeAddCommand(creating_output,
|
|
creating_input));
|
|
|
|
command_name = Node::GetConnectCommandString(
|
|
creating_output, creating_input);
|
|
|
|
// If the output is not in the input's context, add it now. We check the item rather than
|
|
// the node itself, because sometimes a node may not be in the context but another node
|
|
// representing it will be (e.g. groups)
|
|
if (!scene_.context_map()
|
|
.value(create_edge_input_item_->GetContext())
|
|
->GetItemFromMap(creating_output)) {
|
|
command->add_child(new NodeSetPositionCommand(
|
|
creating_output,
|
|
create_edge_input_item_->GetContext(),
|
|
scene_.context_map()
|
|
.value(create_edge_input_item_->GetContext())
|
|
->MapScenePosToNodePosInContext(
|
|
create_edge_output_item_->scenePos())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
creating_input.Reset();
|
|
create_edge_output_item_ = nullptr;
|
|
create_edge_input_item_ = nullptr;
|
|
|
|
// Collapse any items we expanded
|
|
for (auto it = create_edge_expanded_items_.crbegin();
|
|
it != create_edge_expanded_items_.crend(); it++) {
|
|
CollapseItem(*it);
|
|
}
|
|
create_edge_expanded_items_.clear();
|
|
|
|
Core::instance()->undo_stack()->push(command, command_name);
|
|
}
|
|
|
|
void NodeView::PostPaste(const QVector<Node *> &new_nodes,
|
|
const Node::PositionMap &map)
|
|
{
|
|
QVector<AttachedItem> new_attached;
|
|
|
|
NodeViewItem *first_item = nullptr;
|
|
|
|
for (int i = 0; i < new_nodes.size(); i++) {
|
|
Node *node = new_nodes.at(i);
|
|
|
|
// Determine if item had a position, if not don't create an item for it
|
|
NodeViewItem *new_item;
|
|
|
|
if (map.contains(node)) {
|
|
new_item = new NodeViewItem(node, nullptr);
|
|
new_item->SetFlowDirection(scene_.GetFlowDirection());
|
|
new_item->SetNodePosition(map.value(node));
|
|
scene_.addItem(new_item);
|
|
|
|
if (!first_item) {
|
|
first_item = new_item;
|
|
}
|
|
} else {
|
|
new_item = nullptr;
|
|
}
|
|
|
|
new_attached.append({ new_item, node, QPointF(0, 0) });
|
|
}
|
|
|
|
// Correct positions
|
|
if (first_item) {
|
|
for (int i = 0; i < new_attached.size(); i++) {
|
|
AttachedItem &ai = new_attached[i];
|
|
|
|
if (ai.item) {
|
|
ai.original_pos = ai.item->pos() - first_item->pos();
|
|
}
|
|
}
|
|
}
|
|
|
|
SetAttachedItems(new_attached);
|
|
}
|
|
|
|
void NodeView::ResizeOverlay()
|
|
{
|
|
overlay_view_->resize(this->size());
|
|
}
|
|
|
|
NodeViewContext *NodeView::GetContextItemFromNodeItem(NodeViewItem *item)
|
|
{
|
|
QGraphicsItem *i = item;
|
|
while ((i = i->parentItem())) {
|
|
if (NodeViewContext *nvc = dynamic_cast<NodeViewContext *>(i)) {
|
|
return nvc;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void NodeView::SetAttachedItems(const QVector<AttachedItem> &items)
|
|
{
|
|
// Detach anything currently attached
|
|
DetachItemsFromCursor();
|
|
|
|
attached_items_ = items;
|
|
|
|
// Move to cursor
|
|
MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos()));
|
|
}
|
|
|
|
}
|