From 495b07fc5578978e9e462038c730286f40952e8c Mon Sep 17 00:00:00 2001 From: itsmattkc <34096995+itsmattkc@users.noreply.github.com> Date: Wed, 24 Nov 2021 17:58:48 -0800 Subject: [PATCH] implemented node groups --- app/dialog/CMakeLists.txt | 1 + app/dialog/nodegroup/CMakeLists.txt | 22 ++ app/dialog/nodegroup/nodegroupdialog.cpp | 99 ++++++++ .../nodegroup/nodegroupdialog.h} | 31 ++- .../nodeproperties/nodepropertiesdialog.cpp | 2 +- app/node/CMakeLists.txt | 3 +- app/node/group.cpp | 30 --- app/node/group/CMakeLists.txt | 22 ++ app/node/group/group.cpp | 177 +++++++++++++++ app/node/group/group.h | 176 ++++++++++++++ app/node/node.cpp | 19 +- app/node/node.h | 41 +--- app/node/output/viewer/viewer.h | 2 - app/node/param.cpp | 18 ++ app/node/param.h | 37 ++- app/render/previewautocacher.cpp | 3 + app/render/previewautocacher.h | 4 +- app/widget/nodeparamview/nodeparamview.cpp | 214 +++++++++++------- app/widget/nodeparamview/nodeparamview.h | 29 ++- .../nodeparamview/nodeparamviewdockarea.h | 2 + .../nodeparamview/nodeparamviewitem.cpp | 68 +++++- app/widget/nodeparamview/nodeparamviewitem.h | 29 ++- app/widget/nodeview/nodeview.cpp | 96 +++++++- app/widget/nodeview/nodeview.h | 2 +- app/widget/nodeview/nodeviewcontext.cpp | 4 +- app/widget/nodeview/nodeviewcontext.h | 5 + app/widget/nodeview/nodeviewitem.cpp | 2 +- app/window/mainwindow/mainwindow.cpp | 10 - app/window/mainwindow/mainwindow.h | 2 - 29 files changed, 947 insertions(+), 203 deletions(-) create mode 100644 app/dialog/nodegroup/CMakeLists.txt create mode 100644 app/dialog/nodegroup/nodegroupdialog.cpp rename app/{node/group.h => dialog/nodegroup/nodegroupdialog.h} (61%) delete mode 100644 app/node/group.cpp create mode 100644 app/node/group/CMakeLists.txt create mode 100644 app/node/group/group.cpp create mode 100644 app/node/group/group.h diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index ff82a6b2c..01bbfd5ec 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -23,6 +23,7 @@ add_subdirectory(diskcache) add_subdirectory(export) add_subdirectory(footagerelink) add_subdirectory(keyframeproperties) +add_subdirectory(nodegroup) add_subdirectory(nodeproperties) add_subdirectory(preferences) add_subdirectory(progress) diff --git a/app/dialog/nodegroup/CMakeLists.txt b/app/dialog/nodegroup/CMakeLists.txt new file mode 100644 index 000000000..0f48edfc9 --- /dev/null +++ b/app/dialog/nodegroup/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/nodegroup/nodegroupdialog.cpp + dialog/nodegroup/nodegroupdialog.h + PARENT_SCOPE +) diff --git a/app/dialog/nodegroup/nodegroupdialog.cpp b/app/dialog/nodegroup/nodegroupdialog.cpp new file mode 100644 index 000000000..2d07a4338 --- /dev/null +++ b/app/dialog/nodegroup/nodegroupdialog.cpp @@ -0,0 +1,99 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "nodegroupdialog.h" + +#include +#include +#include +#include + +#include "widget/nodeparamview/nodeparamview.h" +#include "widget/nodeview/nodeview.h" + +namespace olive { + +#define super QDialog + +NodeGroupDialog::NodeGroupDialog(NodeGroup *group, QWidget *parent) : + super(parent), + group_(group), + parent_undo_(nullptr) +{ + QGridLayout *layout = new QGridLayout(this); + + int row = 0; + + layout->addWidget(new QLabel(tr("Name:")), row, 0); + + name_edit_ = new QLineEdit(); + layout->addWidget(name_edit_, row, 1); + + row++; + + QSplitter *splitter = new QSplitter(Qt::Horizontal); + layout->addWidget(splitter, row, 0, 1, 2); + + NodeParamView *param_view = new NodeParamView(false); + param_view->SetCreateCheckBoxes(kCheckBoxesOnNonConnected); + splitter->addWidget(param_view); + + NodeView *node_view = new NodeView(); + node_view->SetContexts({group}); + QMetaObject::invokeMethod(node_view, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); + splitter->addWidget(node_view); + + for (auto it=group->GetInputPassthroughs().cbegin(); it!=group->GetInputPassthroughs().cend(); it++) { + param_view->SetInputChecked(it.value(), true); + } + + connect(node_view, &NodeView::NodesSelected, param_view, &NodeParamView::SelectNodes); + connect(node_view, &NodeView::NodesDeselected, param_view, &NodeParamView::DeselectNodes); + node_view->SelectAll(); + + row++; + + QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + btns->setCenterButtons(true); + connect(btns, &QDialogButtonBox::accepted, this, &NodeGroupDialog::accept); + connect(btns, &QDialogButtonBox::rejected, this, &NodeGroupDialog::reject); + layout->addWidget(btns, row, 0, 1, 2); + + setWindowTitle(tr("Group Editor")); +} + +void NodeGroupDialog::accept() +{ + MultiUndoCommand *command = new MultiUndoCommand(); + + if (name_edit_->text() != group_->GetCustomName()) { + command->add_child(new NodeGroupSetCustomNameCommand(group_, name_edit_->text())); + } + + if (parent_undo_) { + parent_undo_->add_child(command); + } else { + Core::instance()->undo_stack()->push(command); + } + + super::accept(); +} + +} diff --git a/app/node/group.h b/app/dialog/nodegroup/nodegroupdialog.h similarity index 61% rename from app/node/group.h rename to app/dialog/nodegroup/nodegroupdialog.h index 2f15bbb71..741f0ac55 100644 --- a/app/node/group.h +++ b/app/dialog/nodegroup/nodegroupdialog.h @@ -18,26 +18,41 @@ ***/ -#ifndef NODEGROUP_H -#define NODEGROUP_H +#ifndef NODEGROUPDIALOG_H +#define NODEGROUPDIALOG_H -#include "node.h" +#include +#include + +#include "node/group/group.h" namespace olive { -class NodeGroup : public Node +class NodeGroupDialog : public QDialog { Q_OBJECT public: - NodeGroup(); + explicit NodeGroupDialog(NodeGroup *group, QWidget *parent = nullptr); - void SetNodes(Node *nodes); + void SetParentUndoCommand(MultiUndoCommand *c) + { + parent_undo_ = c; + } + +public slots: + virtual void accept() override; + +signals: private: - QVector nodes_; + NodeGroup *group_; + + QLineEdit *name_edit_; + + MultiUndoCommand *parent_undo_; }; } -#endif // NODEGROUP_H +#endif // NODEGROUPDIALOG_H diff --git a/app/dialog/nodeproperties/nodepropertiesdialog.cpp b/app/dialog/nodeproperties/nodepropertiesdialog.cpp index 123b6c69b..fca1935ba 100644 --- a/app/dialog/nodeproperties/nodepropertiesdialog.cpp +++ b/app/dialog/nodeproperties/nodepropertiesdialog.cpp @@ -46,7 +46,7 @@ NodePropertiesDialog::NodePropertiesDialog(Node *node, const rational &timebase, label_edit_->setText(node->GetLabel()); label_layout->addWidget(label_edit_); - NodeParamViewItem *item = new NodeParamViewItem(node); + NodeParamViewItem *item = new NodeParamViewItem(node, kNoCheckBoxes); item->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); item->SetTimebase(timebase); item->setTitleBarWidget(new QWidget()); diff --git a/app/node/CMakeLists.txt b/app/node/CMakeLists.txt index 6cd34ca5b..508bf00bf 100644 --- a/app/node/CMakeLists.txt +++ b/app/node/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(color) add_subdirectory(distort) add_subdirectory(filter) add_subdirectory(generator) +add_subdirectory(group) add_subdirectory(input) add_subdirectory(math) add_subdirectory(output) @@ -34,8 +35,6 @@ set(OLIVE_SOURCES node/globals.h node/graph.cpp node/graph.h - node/group.cpp - node/group.h node/hashtraverser.cpp node/hashtraverser.h node/inputdragger.cpp diff --git a/app/node/group.cpp b/app/node/group.cpp deleted file mode 100644 index 7b7ab0c87..000000000 --- a/app/node/group.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2021 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 "group.h" - -namespace olive { - -NodeGroup::NodeGroup() -{ - -} - -} diff --git a/app/node/group/CMakeLists.txt b/app/node/group/CMakeLists.txt new file mode 100644 index 000000000..3c0bce4d6 --- /dev/null +++ b/app/node/group/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2021 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 . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + node/group/group.cpp + node/group/group.h + PARENT_SCOPE +) diff --git a/app/node/group/group.cpp b/app/node/group/group.cpp new file mode 100644 index 000000000..fbe785bd9 --- /dev/null +++ b/app/node/group/group.cpp @@ -0,0 +1,177 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 "group.h" + +#include "node/graph.h" + +namespace olive { + +NodeGroup::NodeGroup() : + output_passthrough_(nullptr) +{ + graph_ = new NodeGraph(); + graph_->setParent(this); +} + +QString NodeGroup::Name() const +{ + if (custom_name_.isEmpty()) { + return tr("Group"); + } else { + return custom_name_; + } +} + +QString NodeGroup::id() const +{ + return QStringLiteral("org.olivevideoeditor.Olive.group"); +} + +QVector NodeGroup::Category() const +{ + return {kCategoryGeneral}; +} + +QString NodeGroup::Description() const +{ + return tr("A group of nodes that is represented as a single node."); +} + +void NodeGroup::Retranslate() +{ + foreach (Node *n, graph_->nodes()) { + n->Retranslate(); + } +} + +void NodeGroup::AddNode(Node *node) +{ + node->setParent(graph_); +} + +void NodeGroup::RemoveNode(Node *node, QObject *new_parent) +{ + if (node->parent() == graph_) { + node->setParent(new_parent); + } +} + +void NodeGroup::AddInputPassthrough(const NodeInput &input) +{ + Q_ASSERT(graph_->nodes().contains(input.node())); + + for (auto it=input_passthroughs_.cbegin(); it!=input_passthroughs_.cend(); it++) { + if (it.value() == input) { + // Already passing this input through + return; + } + } + + // Add input + QString id = GetGroupInputIDFromInput(input); + + AddInput(id, input.GetDataType(), input.GetDefaultValue(), input.GetFlags()); + + input_passthroughs_.insert(id, input); +} + +void NodeGroup::RemoveInputPassthrough(const NodeInput &input) +{ + for (auto it=input_passthroughs_.cbegin(); it!=input_passthroughs_.cend(); it++) { + if (it.value() == input) { + RemoveInput(it.key()); + input_passthroughs_.erase(it); + break; + } + } +} + +void NodeGroup::SetOutputPassthrough(Node *node) +{ + Q_ASSERT(graph_->nodes().contains(node)); + + output_passthrough_ = node; +} + +QString NodeGroup::GetGroupInputIDFromInput(const NodeInput &input) +{ + QCryptographicHash hash(QCryptographicHash::Sha1); + + hash.addData(input.node()->GetUUID().toByteArray()); + + hash.addData(input.input().toUtf8()); + + hash.addData((const char*) &input.element(), sizeof(input.element())); + + return QString::fromLatin1(hash.result().toHex()); +} + +bool NodeGroup::ContainsInputPassthrough(const NodeInput &input) const +{ + for (auto it=input_passthroughs_.cbegin(); it!=input_passthroughs_.cend(); it++) { + if (it.value() == input) { + return true; + } + } + + return false; +} + +void NodeAddToGroupCommand::redo() +{ + previous_parent_ = node_->parent(); + group_->AddNode(node_); +} + +void NodeAddToGroupCommand::undo() +{ + group_->RemoveNode(node_, previous_parent_); +} + +void NodeGroupSetCustomNameCommand::redo() +{ + old_name_ = group_->GetCustomName(); + group_->SetCustomName(new_name_); +} + +void NodeGroupSetCustomNameCommand::undo() +{ + group_->SetCustomName(old_name_); +} + +void NodeGroupAddInputPassthrough::redo() +{ + if (!group_->ContainsInputPassthrough(input_)) { + group_->AddInputPassthrough(input_); + actually_added_ = true; + } else { + actually_added_ = false; + } +} + +void NodeGroupAddInputPassthrough::undo() +{ + if (actually_added_) { + group_->RemoveInputPassthrough(input_); + } +} + +} diff --git a/app/node/group/group.h b/app/node/group/group.h new file mode 100644 index 000000000..a0aa9cee3 --- /dev/null +++ b/app/node/group/group.h @@ -0,0 +1,176 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2021 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 . + +***/ + +#ifndef NODEGROUP_H +#define NODEGROUP_H + +#include "node/node.h" + +namespace olive { + +class NodeGroup : public Node +{ + Q_OBJECT +public: + NodeGroup(); + + NODE_DEFAULT_DESTRUCTOR(NodeGroup) + NODE_COPY_FUNCTION(NodeGroup) + + virtual QString Name() const override; + virtual QString id() const override; + virtual QVector Category() const override; + virtual QString Description() const override; + + virtual void Retranslate() override; + + void AddNode(Node *node); + + void RemoveNode(Node *node, QObject *new_parent = nullptr); + + void AddInputPassthrough(const NodeInput &input); + + void RemoveInputPassthrough(const NodeInput &input); + + void SetOutputPassthrough(Node *node); + + const QString &GetCustomName() const + { + return custom_name_; + } + + void SetCustomName(const QString &name) + { + custom_name_ = name; + + // NOTE: Not technically the right signal, but should achieve the right goal + emit LabelChanged(custom_name_); + } + + void ClearCustomName() + { + custom_name_.clear(); + } + + static QString GetGroupInputIDFromInput(const NodeInput &input); + + const QHash &GetInputPassthroughs() const + { + return input_passthroughs_; + } + + bool ContainsInputPassthrough(const NodeInput &input) const; + +private: + NodeGraph *graph_; + + QHash input_passthroughs_; + + Node *output_passthrough_; + + QString custom_name_; + +}; + +class NodeAddToGroupCommand : public UndoCommand +{ +public: + NodeAddToGroupCommand(Node *node, NodeGroup *group) : + node_(node), + group_(group) + {} + + virtual Project * GetRelevantProject() const override + { + return node_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + Node *node_; + + NodeGroup *group_; + + QObject *previous_parent_; + +}; + +class NodeGroupSetCustomNameCommand : public UndoCommand +{ +public: + NodeGroupSetCustomNameCommand(NodeGroup *group, const QString &name) : + group_(group), + new_name_(name) + {} + + virtual Project * GetRelevantProject() const override + { + return group_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeGroup *group_; + + QString old_name_; + + QString new_name_; + +}; + +class NodeGroupAddInputPassthrough : public UndoCommand +{ +public: + NodeGroupAddInputPassthrough(NodeGroup *group, const NodeInput &input) : + group_(group), + input_(input), + actually_added_(false) + {} + + virtual Project * GetRelevantProject() const override + { + return group_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeGroup *group_; + + NodeInput input_; + + bool actually_added_; + +}; + +} + +#endif // NODEGROUP_H diff --git a/app/node/node.cpp b/app/node/node.cpp index ddabc5a61..1d0316e8b 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -49,6 +49,7 @@ Node::Node() : operation_stack_(0), cache_result_(false) { + uuid_ = QUuid::createUuid(); } Node::~Node() @@ -88,6 +89,8 @@ void Node::Load(QXmlStreamReader *reader, XMLNodeData& xml_node_data, uint versi xml_node_data.node_ptrs.insert(reader->readElementText().toULongLong(), this); } else if (reader->name() == QStringLiteral("label")) { SetLabel(reader->readElementText()); + } else if (reader->name() == QStringLiteral("uuid")) { + SetUUID(QUuid::fromString(reader->readElementText())); } else if (reader->name() == QStringLiteral("color")) { override_color_ = reader->readElementText().toInt(); } else if (reader->name() == QStringLiteral("links")) { @@ -169,6 +172,7 @@ void Node::Save(QXmlStreamWriter *writer) const { writer->writeTextElement(QStringLiteral("ptr"), QString::number(reinterpret_cast(this))); + writer->writeTextElement(QStringLiteral("uuid"), uuid_.toString()); writer->writeTextElement(QStringLiteral("label"), GetLabel()); writer->writeTextElement(QStringLiteral("color"), QString::number(override_color_)); @@ -219,7 +223,16 @@ void Node::Save(QXmlStreamWriter *writer) const Project* Node::project() const { - return dynamic_cast(parent()); + QObject *t = this->parent(); + + while (t) { + if (Project *p = dynamic_cast(t)) { + return p; + } + t = t->parent(); + } + + return nullptr; } QString Node::ShortName() const @@ -1089,7 +1102,7 @@ NodeInputImmediate *Node::GetImmediate(const QString &input, int element) const return nullptr; } -Node::InputFlags Node::GetInputFlags(const QString &input) const +InputFlags Node::GetInputFlags(const QString &input) const { const Input* i = GetInternalInputData(input); @@ -1343,7 +1356,7 @@ void Node::HashAddNodeSignature(QCryptographicHash &hash) const hash.addData(id().toUtf8()); } -void Node::InsertInput(const QString &id, NodeValue::Type type, const QVariant &default_value, Node::InputFlags flags, int index) +void Node::InsertInput(const QString &id, NodeValue::Type type, const QVariant &default_value, InputFlags flags, int index) { if (id.isEmpty()) { qWarning() << "Rejected adding input with an empty ID on node" << this->id(); diff --git a/app/node/node.h b/app/node/node.h index ca3f7f43c..3a0fa5130 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "codec/frame.h" @@ -112,6 +113,9 @@ public: Project* project() const; + const QUuid &GetUUID() const {return uuid_;} + void SetUUID(const QUuid &uuid) {uuid_ = uuid;} + /** * @brief Clear current node variables and replace them with */ @@ -935,38 +939,9 @@ public: }; + InputFlags GetInputFlags(const QString& input) const; + protected: - enum InputFlag { - /// By default, inputs are keyframable, connectable, and NOT arrays - kInputFlagNormal = 0x0, - kInputFlagArray = 0x1, - kInputFlagNotKeyframable = 0x2, - kInputFlagNotConnectable = 0x4, - kInputFlagHidden = 0x8 - }; - - class InputFlags { - public: - explicit InputFlags() - { - f_ = kInputFlagNormal; - } - - explicit InputFlags(uint64_t flags) - { - f_ = flags; - } - - bool operator&(const InputFlag& f) const - { - return f_ & f; - } - - private: - uint64_t f_; - - }; - virtual void Hash(QCryptographicHash& hash, const NodeGlobals &globals, const VideoParams& video_params) const; void HashAddNodeSignature(QCryptographicHash &hash) const; @@ -1216,8 +1191,6 @@ private: return input_ids_.indexOf(input); } - InputFlags GetInputFlags(const QString& input) const; - Input* GetInternalInputData(const QString& input) { int i = GetInternalInputIndex(input); @@ -1336,6 +1309,8 @@ private: PositionMap context_positions_; + QUuid uuid_; + private slots: /** * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 56a89908c..5c516000c 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -254,8 +254,6 @@ private: AudioPlaybackCache audio_playback_cache_; - int operation_stack_; - VideoParams cached_video_params_; AudioParams cached_audio_params_; diff --git a/app/node/param.cpp b/app/node/param.cpp index f0a685abe..2e579f613 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -69,6 +69,15 @@ bool NodeInput::IsArray() const } } +InputFlags NodeInput::GetFlags() const +{ + if (IsValid()) { + return node_->GetInputFlags(input_); + } else { + return InputFlags(kInputFlagNormal); + } +} + Node *NodeInput::GetConnectedOutput() const { if (IsValid()) { @@ -87,6 +96,15 @@ NodeValue::Type NodeInput::GetDataType() const } } +QVariant NodeInput::GetDefaultValue() const +{ + if (IsValid()) { + return node_->GetDefaultValue(input_); + } else { + return QVariant(); + } +} + QStringList NodeInput::GetComboBoxStrings() const { if (IsValid()) { diff --git a/app/node/param.h b/app/node/param.h index 3de4b6727..2e2276187 100644 --- a/app/node/param.h +++ b/app/node/param.h @@ -31,6 +31,37 @@ namespace olive { class Node; class NodeKeyframe; +enum InputFlag { + /// By default, inputs are keyframable, connectable, and NOT arrays + kInputFlagNormal = 0x0, + kInputFlagArray = 0x1, + kInputFlagNotKeyframable = 0x2, + kInputFlagNotConnectable = 0x4, + kInputFlagHidden = 0x8 +}; + +class InputFlags { +public: + explicit InputFlags() + { + f_ = kInputFlagNormal; + } + + explicit InputFlags(uint64_t flags) + { + f_ = flags; + } + + bool operator&(const InputFlag& f) const + { + return f_ & f; + } + +private: + uint64_t f_; + +}; + struct NodeInputPair { bool operator==(const NodeInputPair& rhs) const { @@ -98,7 +129,7 @@ public: return input_; } - int element() const + const int &element() const { return element_; } @@ -123,10 +154,14 @@ public: bool IsArray() const; + InputFlags GetFlags() const; + Node *GetConnectedOutput() const; NodeValue::Type GetDataType() const; + QVariant GetDefaultValue() const; + QStringList GetComboBoxStrings() const; QVariant GetProperty(const QString& key) const; diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 2c121745d..75b78c3c5 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -373,6 +373,9 @@ void PreviewAutoCacher::AddNode(Node *node) // Add to project copy->setParent(&copied_project_); + // Copy UUID + copy->SetUUID(node->GetUUID()); + // Insert into map InsertIntoCopyMap(node, copy); diff --git a/app/render/previewautocacher.h b/app/render/previewautocacher.h index ad4665da3..f770c776f 100644 --- a/app/render/previewautocacher.h +++ b/app/render/previewautocacher.h @@ -24,8 +24,9 @@ #include #include "config/config.h" -#include "node/graph.h" #include "node/color/colormanager/colormanager.h" +#include "node/graph.h" +#include "node/group/group.h" #include "node/node.h" #include "node/output/viewer/viewer.h" #include "node/project/project.h" @@ -172,6 +173,7 @@ private: QVector graph_update_queue_; QHash copy_map_; + QHash graph_map_; ViewerOutput* copied_viewer_node_; ColorManager* copied_color_manager_; QVector created_nodes_; diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 9d3e77000..52b4407fc 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -32,10 +32,12 @@ namespace olive { #define super TimeBasedWidget -NodeParamView::NodeParamView(QWidget *parent) : +NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) : super(true, false, parent), last_scroll_val_(0), - focused_node_(nullptr) + focused_node_(nullptr), + create_checkboxes_(kNoCheckBoxes), + time_target_(nullptr) { // Create horizontal layout to place scroll area in (and keyframe editing eventually) QHBoxLayout* layout = new QHBoxLayout(this); @@ -46,16 +48,16 @@ NodeParamView::NodeParamView(QWidget *parent) : layout->addWidget(splitter); // Set up scroll area for params - QScrollArea* scroll_area = new QScrollArea(); - scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); - scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scroll_area->setWidgetResizable(true); - splitter->addWidget(scroll_area); + param_scroll_area_ = new QScrollArea(); + param_scroll_area_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + param_scroll_area_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + param_scroll_area_->setWidgetResizable(true); + splitter->addWidget(param_scroll_area_); // Param widget param_widget_container_ = new NodeParamViewParamContainer(); connect(param_widget_container_, &NodeParamViewParamContainer::Resized, this, &NodeParamView::UpdateGlobalScrollBar); - scroll_area->setWidget(param_widget_container_); + param_scroll_area_->setWidget(param_widget_container_); param_widget_area_ = new NodeParamViewDockArea(); @@ -74,56 +76,63 @@ NodeParamView::NodeParamView(QWidget *parent) : param_widget_container_layout->addStretch(INT_MAX); - // Set up keyframe view - QWidget* keyframe_area = new QWidget(); - QVBoxLayout* keyframe_area_layout = new QVBoxLayout(keyframe_area); - keyframe_area_layout->setSpacing(0); - keyframe_area_layout->setMargin(0); - - // Create ruler object - keyframe_area_layout->addWidget(ruler()); - - // Create keyframe view - keyframe_view_ = new KeyframeView(); - keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - ConnectTimelineView(keyframe_view_); - keyframe_area_layout->addWidget(keyframe_view_); - - // Connect ruler and keyframe view together - connect(ruler(), &TimeRuler::TimeChanged, keyframe_view_, &KeyframeView::SetTime); - connect(keyframe_view_, &KeyframeView::TimeChanged, ruler(), &TimeRuler::SetTime); - connect(keyframe_view_, &KeyframeView::TimeChanged, this, &NodeParamView::SetTime); - connect(keyframe_view_, &KeyframeView::Dragged, this, &NodeParamView::KeyframeViewDragged); - - // Connect keyframe view scaling to this - connect(keyframe_view_, &KeyframeView::ScaleChanged, this, &NodeParamView::SetScale); - - splitter->addWidget(keyframe_area); - - // Set both widgets to 50/50 - splitter->setSizes({INT_MAX, INT_MAX}); - // Disable collapsing param view (but collapsing keyframe view is permitted) splitter->setCollapsible(0, false); + if (create_keyframe_view) { + // Set up keyframe view + QWidget* keyframe_area = new QWidget(); + QVBoxLayout* keyframe_area_layout = new QVBoxLayout(keyframe_area); + keyframe_area_layout->setSpacing(0); + keyframe_area_layout->setMargin(0); + + // Create ruler object + keyframe_area_layout->addWidget(ruler()); + + // Create keyframe view + keyframe_view_ = new KeyframeView(); + keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ConnectTimelineView(keyframe_view_); + keyframe_area_layout->addWidget(keyframe_view_); + + // Connect ruler and keyframe view together + connect(ruler(), &TimeRuler::TimeChanged, keyframe_view_, &KeyframeView::SetTime); + connect(keyframe_view_, &KeyframeView::TimeChanged, ruler(), &TimeRuler::SetTime); + connect(keyframe_view_, &KeyframeView::TimeChanged, this, &NodeParamView::SetTime); + connect(keyframe_view_, &KeyframeView::Dragged, this, &NodeParamView::KeyframeViewDragged); + + // Connect keyframe view scaling to this + connect(keyframe_view_, &KeyframeView::ScaleChanged, this, &NodeParamView::SetScale); + + splitter->addWidget(keyframe_area); + + // Set both widgets to 50/50 + splitter->setSizes({INT_MAX, INT_MAX}); + } else { + keyframe_view_ = nullptr; + } + // Create global vertical scrollbar on the right vertical_scrollbar_ = new QScrollBar(); vertical_scrollbar_->setMaximum(0); layout->addWidget(vertical_scrollbar_); // Connect scrollbars together - connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); - connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, scroll_area->verticalScrollBar(), &QScrollBar::setValue); - connect(scroll_area->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); - connect(scroll_area->verticalScrollBar(), &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); - connect(vertical_scrollbar_, &QScrollBar::valueChanged, scroll_area->verticalScrollBar(), &QScrollBar::setValue); - connect(vertical_scrollbar_, &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); + connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); + connect(vertical_scrollbar_, &QScrollBar::valueChanged, param_scroll_area_->verticalScrollBar(), &QScrollBar::setValue); - // TimeBasedWidget's scrollbar has extra functionality that we can take advantage of - keyframe_view_->setHorizontalScrollBar(scrollbar()); - keyframe_view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + if (keyframe_view_) { + connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, vertical_scrollbar_, &QScrollBar::setValue); + connect(keyframe_view_->verticalScrollBar(), &QScrollBar::valueChanged, param_scroll_area_->verticalScrollBar(), &QScrollBar::setValue); + connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); + connect(vertical_scrollbar_, &QScrollBar::valueChanged, keyframe_view_->verticalScrollBar(), &QScrollBar::setValue); - connect(keyframe_view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); + // TimeBasedWidget's scrollbar has extra functionality that we can take advantage of + keyframe_view_->setHorizontalScrollBar(scrollbar()); + keyframe_view_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + connect(keyframe_view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); + } // Set a default scale - FIXME: Hardcoded SetScale(120); @@ -192,6 +201,14 @@ void NodeParamView::DeselectNodes(const QVector &nodes) } } +void NodeParamView::SetInputChecked(const NodeInput &input, bool e) +{ + input_checked_.insert(input, e); + if (NodeParamViewItem *item = items_.value(input.node())) { + item->SetInputChecked(input, e); + } +} + void NodeParamView::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); @@ -205,17 +222,21 @@ void NodeParamView::ScaleChangedEvent(const double &scale) { super::ScaleChangedEvent(scale); - keyframe_view_->SetScale(scale); + if (keyframe_view_) { + keyframe_view_->SetScale(scale); + } } void NodeParamView::TimebaseChangedEvent(const rational &timebase) { super::TimebaseChangedEvent(timebase); - keyframe_view_->SetTimebase(timebase); + if (keyframe_view_) { + keyframe_view_->SetTimebase(timebase); + } foreach (NodeParamViewItem* item, items_) { - item->SetTimebase(timebase); + item->SetTimebase(timebase); } UpdateItemTime(GetTime()); @@ -225,29 +246,37 @@ void NodeParamView::TimeChangedEvent(const rational &time) { super::TimeChangedEvent(time); - keyframe_view_->SetTime(time); + if (keyframe_view_) { + keyframe_view_->SetTime(time); + } UpdateItemTime(time); } void NodeParamView::ConnectedNodeChangeEvent(ViewerOutput *n) { - // Set viewer as a time target - keyframe_view_->SetTimeTarget(n); + if (keyframe_view_) { + // Set viewer as a time target + keyframe_view_->SetTimeTarget(n); + } foreach (NodeParamViewItem* item, items_) { item->SetTimeTarget(n); } + + time_target_ = n; } Node *NodeParamView::GetTimeTarget() const { - return keyframe_view_->GetTimeTarget(); + return time_target_; } void NodeParamView::DeleteSelected() { - keyframe_view_->DeleteSelected(); + if (keyframe_view_) { + keyframe_view_->DeleteSelected(); + } } void NodeParamView::UpdateItemTime(const rational &time) @@ -293,14 +322,16 @@ void NodeParamView::SignalNodeOrder() void NodeParamView::AddNode(Node *n) { - NodeParamViewItem* item = new NodeParamViewItem(n, param_widget_area_); + NodeParamViewItem* item = new NodeParamViewItem(n, create_checkboxes_, param_widget_area_); item->setAllowedAreas(Qt::LeftDockWidgetArea); item->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable); item->SetExpanded(node_expanded_state_.value(n, true)); - connect(n, &Node::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); - connect(n, &Node::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + if (keyframe_view_) { + connect(n, &Node::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); + connect(n, &Node::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + } connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); @@ -310,6 +341,15 @@ void NodeParamView::AddNode(Node *n) connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::InputCheckedChanged, this, &NodeParamView::SetInputChecked); + + if (create_checkboxes_) { + for (auto it=input_checked_.cbegin(); it!=input_checked_.cend(); it++) { + if (it.key().node() == n) { + item->SetInputChecked(it.key(), it.value()); + } + } + } // Set time target item->SetTimeTarget(GetTimeTarget()); @@ -327,15 +367,19 @@ void NodeParamView::AddNode(Node *n) emit FocusedNodeChanged(focused_node_); } - keyframe_view_->AddKeyframesOfNode(n); + if (keyframe_view_) { + keyframe_view_->AddKeyframesOfNode(n); + } } void NodeParamView::RemoveNode(Node *n) { - keyframe_view_->RemoveKeyframesOfNode(n); + if (keyframe_view_) { + keyframe_view_->RemoveKeyframesOfNode(n); - disconnect(n, &Node::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); - disconnect(n, &Node::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + disconnect(n, &Node::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); + disconnect(n, &Node::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); + } delete items_.take(n); @@ -358,8 +402,10 @@ void NodeParamView::UpdateGlobalScrollBar() { int height_offscreen = param_widget_container_->height() - ruler()->height() + scrollbar()->height(); - keyframe_view_->SetMaxScroll(height_offscreen); - vertical_scrollbar_->setRange(0, height_offscreen - keyframe_view_->height()); + if (keyframe_view_) { + keyframe_view_->SetMaxScroll(height_offscreen); + } + vertical_scrollbar_->setRange(0, height_offscreen - param_scroll_area_->height()); } void NodeParamView::PinNode(bool pin) @@ -390,18 +436,20 @@ void NodeParamView::FocusChanged(QWidget* old, QWidget* now) item = dynamic_cast(parent); if (item) { - // Found it! - if (item->GetNode() != focused_node_) { - if (focused_node_) { - // De-focus current node - items_.value(focused_node_)->SetHighlighted(false); + if (item->parent() == param_widget_area_) { + // Found it! + if (item->GetNode() != focused_node_) { + if (focused_node_) { + // De-focus current node + items_.value(focused_node_)->SetHighlighted(false); + } + + focused_node_ = item->GetNode(); + + item->SetHighlighted(true); + + emit FocusedNodeChanged(focused_node_); } - - focused_node_ = item->GetNode(); - - item->SetHighlighted(true); - - emit FocusedNodeChanged(focused_node_); } break; @@ -421,15 +469,17 @@ void NodeParamView::KeyframeViewDragged(int x, int y) void NodeParamView::UpdateElementY() { - for (auto it=items_.cbegin(); it!=items_.cend(); it++) { - foreach (const QString& input, it.key()->inputs()) { - int arr_sz = it.key()->InputArraySize(input); + if (keyframe_view_) { + for (auto it=items_.cbegin(); it!=items_.cend(); it++) { + foreach (const QString& input, it.key()->inputs()) { + int arr_sz = it.key()->InputArraySize(input); - for (int i=-1; iGetElementY(ic); - keyframe_view_->SetElementY(ic, y); + int y = it.value()->GetElementY(ic); + keyframe_view_->SetElementY(ic, y); + } } } } diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 1718f445e..677c4dce5 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -58,11 +58,25 @@ class NodeParamView : public TimeBasedWidget { Q_OBJECT public: - NodeParamView(QWidget* parent = nullptr); + NodeParamView(bool create_keyframe_view, QWidget* parent = nullptr); + NodeParamView(QWidget* parent = nullptr) : + NodeParamView(true, parent) + { + } void SelectNodes(const QVector &nodes); void DeselectNodes(const QVector& nodes); + void SetCreateCheckBoxes(NodeParamViewCheckBoxBehavior e) + { + create_checkboxes_ = e; + } + + bool IsInputChecked(const NodeInput &input) const + { + return input_checked_.value(input); + } + const QMap& GetItemMap() const { return items_; @@ -82,6 +96,9 @@ public: keyframe_view_->DeselectAll(); } +public slots: + void SetInputChecked(const NodeInput &input, bool e); + signals: void RequestSelectNode(const QVector& target); @@ -117,10 +134,10 @@ private: int last_scroll_val_; + QScrollArea* param_scroll_area_; + NodeParamViewParamContainer* param_widget_container_; - // This may look weird, but QMainWindow is just a QWidget with a fancy layout that allows - // docking windows NodeParamViewDockArea* param_widget_area_; QVector pinned_nodes_; @@ -131,6 +148,12 @@ private: Node* focused_node_; + NodeParamViewCheckBoxBehavior create_checkboxes_; + + Node *time_target_; + + QHash input_checked_; + private slots: void UpdateGlobalScrollBar(); diff --git a/app/widget/nodeparamview/nodeparamviewdockarea.h b/app/widget/nodeparamview/nodeparamviewdockarea.h index 90a00c1a0..c313236cd 100644 --- a/app/widget/nodeparamview/nodeparamviewdockarea.h +++ b/app/widget/nodeparamview/nodeparamviewdockarea.h @@ -25,6 +25,8 @@ namespace olive { +// This may look weird, but QMainWindow is just a QWidget with a fancy layout that allows +// for docking QDockWidgets class NodeParamViewDockArea : public QMainWindow { Q_OBJECT diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 3c2d5bbb0..3347551ef 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -38,12 +38,14 @@ const int NodeParamViewItemBody::kArrayInsertColumn = kKeyControlColumn-1; const int NodeParamViewItemBody::kArrayRemoveColumn = kArrayInsertColumn-1; const int NodeParamViewItemBody::kExtraButtonColumn = kKeyControlColumn-1; -// 0 is for the array collapse button, 1 is for the main label, widgets start at 2 -const int NodeParamViewItemBody::kWidgetStartColumn = 2; +const int NodeParamViewItemBody::kOptionalCheckBox = 0; +const int NodeParamViewItemBody::kArrayCollapseBtnColumn = 1; +const int NodeParamViewItemBody::kLabelColumn = 2; +const int NodeParamViewItemBody::kWidgetStartColumn = 3; #define super QDockWidget -NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : +NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget *parent) : super(parent), node_(node), highlighted_(false) @@ -55,10 +57,11 @@ NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : this->setTitleBarWidget(title_bar_); // Create and add contents widget - body_ = new NodeParamViewItemBody(node_); + body_ = new NodeParamViewItemBody(node_, create_checkboxes); connect(body_, &NodeParamViewItemBody::RequestSelectNode, this, &NodeParamViewItem::RequestSelectNode); connect(body_, &NodeParamViewItemBody::RequestSetTime, this, &NodeParamViewItem::RequestSetTime); connect(body_, &NodeParamViewItemBody::ArrayExpandedChanged, this, &NodeParamViewItem::ArrayExpandedChanged); + connect(body_, &NodeParamViewItemBody::InputCheckedChanged, this, &NodeParamViewItem::InputCheckedChanged); connect(title_bar_, &NodeParamViewItemTitleBar::ExpandedStateChanged, this, &NodeParamViewItem::SetExpanded); connect(title_bar_, &NodeParamViewItemTitleBar::PinToggled, this, &NodeParamViewItem::PinToggled); @@ -165,6 +168,11 @@ int NodeParamViewItem::GetElementY(const NodeInput &c) const } } +void NodeParamViewItem::SetInputChecked(const NodeInput &input, bool e) +{ + body_->SetInputChecked(input, e); +} + void NodeParamViewItem::ToggleExpanded() { SetExpanded(!IsExpanded()); @@ -222,9 +230,10 @@ void NodeParamViewItemTitleBar::mouseDoubleClickEvent(QMouseEvent *event) collapse_btn_->click(); } -NodeParamViewItemBody::NodeParamViewItemBody(Node* node, QWidget *parent) : +NodeParamViewItemBody::NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget *parent) : QWidget(parent), - node_(node) + node_(node), + create_checkboxes_(create_checkboxes) { QGridLayout* root_layout = new QGridLayout(this); @@ -277,11 +286,22 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const ui_objects.layout = layout; ui_objects.row = row; + // Create optional checkbox if requested + if (create_checkboxes_) { + ui_objects.optional_checkbox = new QCheckBox(); + connect(ui_objects.optional_checkbox, &QCheckBox::clicked, this, &NodeParamViewItemBody::OptionalCheckBoxClicked); + layout->addWidget(ui_objects.optional_checkbox, row, kOptionalCheckBox); + + if (create_checkboxes_ == kCheckBoxesOnNonConnected && input_ref.IsConnected()) { + ui_objects.optional_checkbox->setVisible(false); + } + } + // Add descriptor label ui_objects.main_label = new QLabel(); - // Label always goes into column 1 (array collapse button goes into 0 if applicable) - layout->addWidget(ui_objects.main_label, row, 1); + // Create input label + layout->addWidget(ui_objects.main_label, row, kLabelColumn); if (node->InputIsArray(input)) { if (element == -1) { @@ -292,8 +312,8 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const // Default to collapsed array_collapse_btn->setChecked(false); - // Collapse button always goes into column 0 - layout->addWidget(array_collapse_btn, row, 0); + // Add collapse button to layout + layout->addWidget(array_collapse_btn, row, kArrayCollapseBtnColumn); // Connect signal to show/hide array params when toggled connect(array_collapse_btn, &CollapseButton::toggled, this, &NodeParamViewItemBody::ArrayCollapseBtnPressed); @@ -448,6 +468,11 @@ void NodeParamViewItemBody::UpdateUIForEdgeConnection(const NodeInput& input) if (ui_objects.key_control) { ui_objects.key_control->setVisible(!input.IsConnected()); } + + // Show/hide optional checkbox if requested + if (create_checkboxes_ == kCheckBoxesOnNonConnected) { + ui_objects.optional_checkbox->setVisible(!input.IsConnected()); + } } } @@ -575,6 +600,16 @@ void NodeParamViewItemBody::SetTimebase(const rational& timebase) } } +void NodeParamViewItemBody::SetInputChecked(const NodeInput &input, bool e) +{ + if (input_ui_map_.contains(input)) { + QCheckBox *cb = input_ui_map_.value(input).optional_checkbox; + if (cb) { + cb->setChecked(e); + } + } +} + void NodeParamViewItemBody::ReplaceWidgets(const NodeInput &input) { InputUI ui = input_ui_map_.value(input); @@ -588,12 +623,25 @@ void NodeParamViewItemBody::ShowSpeedDurationDialogForNode() sdd.exec(); } +void NodeParamViewItemBody::OptionalCheckBoxClicked(bool e) +{ + QCheckBox *cb = static_cast(sender()); + + for (auto it=input_ui_map_.cbegin(); it!=input_ui_map_.cend(); it++) { + if (it.value().optional_checkbox == cb) { + emit InputCheckedChanged(it.key(), e); + break; + } + } +} + NodeParamViewItemBody::InputUI::InputUI() : main_label(nullptr), widget_bridge(nullptr), connected_label(nullptr), key_control(nullptr), extra_btn(nullptr), + optional_checkbox(nullptr), array_insert_btn(nullptr), array_remove_btn(nullptr) { diff --git a/app/widget/nodeparamview/nodeparamviewitem.h b/app/widget/nodeparamview/nodeparamviewitem.h index bbd31c9f4..55a0a3174 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -21,6 +21,7 @@ #ifndef NODEPARAMVIEWITEM_H #define NODEPARAMVIEWITEM_H +#include #include #include #include @@ -38,6 +39,12 @@ namespace olive { +enum NodeParamViewCheckBoxBehavior { + kNoCheckBoxes, + kCheckBoxesOn, + kCheckBoxesOnNonConnected +}; + class NodeParamViewItemTitleBar : public QWidget { Q_OBJECT @@ -75,7 +82,7 @@ private: class NodeParamViewItemBody : public QWidget { Q_OBJECT public: - NodeParamViewItemBody(Node* node, QWidget* parent = nullptr); + NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget* parent = nullptr); void SetTimeTarget(Node* target); @@ -86,7 +93,9 @@ public: int GetElementY(NodeInput c) const; // Set the timebase of any timebased widgets contained here - void SetTimebase(const rational& timebase); + void SetTimebase(const rational& timebase); + + void SetInputChecked(const NodeInput &input, bool e); signals: void RequestSetTime(const rational& time); @@ -95,6 +104,8 @@ signals: void ArrayExpandedChanged(bool e); + void InputCheckedChanged(const NodeInput &input, bool e); + private: void CreateWidgets(QGridLayout *layout, Node* node, const QString& input, int element, int row_index); @@ -114,6 +125,7 @@ private: QGridLayout* layout; int row; QPushButton *extra_btn; + QCheckBox *optional_checkbox; NodeParamViewArrayButton* array_insert_btn; NodeParamViewArrayButton* array_remove_btn; @@ -135,6 +147,8 @@ private: rational timebase_; + NodeParamViewCheckBoxBehavior create_checkboxes_; + /** * @brief The column to place the keyframe controls in * @@ -147,6 +161,9 @@ private: static const int kArrayRemoveColumn; static const int kExtraButtonColumn; + static const int kOptionalCheckBox; + static const int kArrayCollapseBtnColumn; + static const int kLabelColumn; static const int kWidgetStartColumn; private slots: @@ -168,13 +185,15 @@ private slots: void ShowSpeedDurationDialogForNode(); + void OptionalCheckBoxClicked(bool e); + }; class NodeParamViewItem : public QDockWidget { Q_OBJECT public: - NodeParamViewItem(Node* node, QWidget* parent = nullptr); + NodeParamViewItem(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget* parent = nullptr); void SetTimeTarget(Node* target); @@ -196,6 +215,8 @@ public: int GetElementY(const NodeInput& c) const; + void SetInputChecked(const NodeInput &input, bool e); + public slots: void SetExpanded(bool e); @@ -214,6 +235,8 @@ signals: void Moved(); + void InputCheckedChanged(const NodeInput &input, bool e); + protected: virtual void changeEvent(QEvent *e) override; diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index e805d7eda..9b3619e0e 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -21,16 +21,18 @@ #include "nodeview.h" #include +#include #include #include #include #include "core.h" +#include "dialog/nodegroup/nodegroupdialog.h" #include "nodeviewundo.h" #include "node/audio/volume/volume.h" #include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" -#include "node/group.h" +#include "node/group/group.h" #include "node/traverser.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" @@ -638,6 +640,7 @@ void NodeView::UpdateSelectionCache() 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; @@ -649,20 +652,20 @@ void NodeView::UpdateSelectionCache() } } - if (still_selected) { + if (!still_selected) { deselected.append(n); selected_nodes_.removeOne(n); } } } - if (!selected.isEmpty()) { - emit NodesSelected(selected); - } - if (!deselected.isEmpty()) { emit NodesDeselected(deselected); } + + if (!selected.isEmpty()) { + emit NodesSelected(selected); + } } void NodeView::ShowContextMenu(const QPoint &pos) @@ -1082,13 +1085,88 @@ void NodeView::PositionNewEdge(const QPoint &pos) void NodeView::GroupNodes() { - /*NodeGroup *group = new NodeGroup(); - selected_nodes_*/ + // Get items + QVector 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; iGetContext() != 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(); + + command->add_child(new NodeAddCommand(context->parent(), group)); + command->add_child(new NodeSetPositionCommand(group, context, avg_pos)); + + // Add nodes to group + QVector nodes_to_group = selected_nodes_; + DeselectAll(); + foreach (Node *n, nodes_to_group) { + for (auto it=n->input_connections().cbegin(); it!=n->input_connections().cend(); it++) { + Node *output = it->second; + const NodeInput &input = it->first; + + if (!nodes_to_group.contains(output)) { + command->add_child(new NodeEdgeRemoveCommand(output, input)); + command->add_child(new NodeEdgeAddCommand(output, NodeInput(group, input.input(), input.element()))); + } + } + + for (auto it=n->output_connections().cbegin(); it!=n->output_connections().cend(); it++) { + Node *output = it->first; + const NodeInput &input = it->second; + + if (!nodes_to_group.contains(input.node())) { + command->add_child(new NodeEdgeRemoveCommand(output, input)); + command->add_child(new NodeEdgeAddCommand(group, input)); + } + } + + command->add_child(new NodeRemovePositionFromContextCommand(n, context)); + command->add_child(new NodeAddToGroupCommand(n, group)); + command->add_child(new NodeSetPositionCommand(n, group, scene_.context_map().value(context)->GetItemFromMap(n)->GetNodePosition())); + + 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)); + } + } + } + + // Do command + command->redo_now(); + + NodeGroupDialog ngd(group, this); + if (ngd.exec() == QDialog::Accepted) { + // Push to stack so it can be undone (MultiUndoCommand will ignore the request to redo again) + Core::instance()->undo_stack()->push(command); + } else { + // Undo command and delete + command->undo_now(); + delete command; + } } void NodeView::UngroupNodes() { - //static_cast(selected_nodes_.first()); + //NodeGroup *group = static_cast(selected_nodes_.first()); } void NodeView::PasteNodesInternal(const QVector &duplicate_nodes) diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index fd5e2c0c9..5f7c802bb 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -45,7 +45,7 @@ class NodeView : public HandMovableView, public NodeCopyPasteService { Q_OBJECT public: - NodeView(QWidget* parent); + NodeView(QWidget* parent = nullptr); virtual ~NodeView() override; diff --git a/app/widget/nodeview/nodeviewcontext.cpp b/app/widget/nodeview/nodeviewcontext.cpp index b608ee58b..c2c516c60 100644 --- a/app/widget/nodeview/nodeviewcontext.cpp +++ b/app/widget/nodeview/nodeviewcontext.cpp @@ -189,7 +189,9 @@ void NodeViewContext::DeleteSelected(NodeViewDeleteCommand *command) void NodeViewContext::Select(const QVector &nodes) { foreach (Node *n, nodes) { - item_map_.value(n)->setSelected(true); + if (NodeViewItem *item = item_map_.value(n)) { + item->setSelected(true); + } } } diff --git a/app/widget/nodeview/nodeviewcontext.h b/app/widget/nodeview/nodeviewcontext.h index cf49b332d..7cdc638a7 100644 --- a/app/widget/nodeview/nodeviewcontext.h +++ b/app/widget/nodeview/nodeviewcontext.h @@ -36,6 +36,11 @@ public: QPointF MapScenePosToNodePosInContext(const QPointF &pos) const; + NodeViewItem *GetItemFromMap(Node *node) const + { + return item_map_.value(node); + } + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; public slots: diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 3ea29ff5a..1a9b6c516 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -544,7 +544,7 @@ QPointF NodeViewItem::GetInputPoint(const QString &input, int element) const int index = node_inputs_.indexOf(input); if (index < 0 || index >= int(input_connectors_.size())) { - return QPointF(); + return pos(); } return input_connectors_[index]->scenePos(); diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 47c23c3cb..db245f005 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -467,15 +467,6 @@ void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) } } -void MainWindow::ProjectPanelSelectionChanged(const QVector &nodes) -{ - ProjectPanel *panel = static_cast(sender()); - - if (PanelManager::instance()->CurrentlyFocused(false) == panel) { - node_panel_->Select(nodes, true); - } -} - void MainWindow::ShowWelcomeDialog() { if (Config::Current()[QStringLiteral("ShowWelcomeDialog")].toBool()) { @@ -572,7 +563,6 @@ ProjectPanel *MainWindow::AppendProjectPanel() connect(panel, &PanelWidget::CloseRequested, this, &MainWindow::ProjectCloseRequested); connect(panel, &ProjectPanel::ProjectNameChanged, this, &MainWindow::UpdateTitle); - connect(panel, &ProjectPanel::SelectionChanged, this, &MainWindow::ProjectPanelSelectionChanged); return panel; } diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index f8cf36d98..719652200 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -193,8 +193,6 @@ private slots: void TimelinePanelSelectionChanged(const QVector &blocks); - void ProjectPanelSelectionChanged(const QVector &nodes); - void ShowWelcomeDialog(); };