diff --git a/CMakeLists.txt b/CMakeLists.txt index 67de32ebf..27d98682d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,9 @@ else() endif() set(OLIVE_DEFINITIONS -DQT_DEPRECATED_WARNINGS) +if (WIN32) + list(APPEND OLIVE_DEFINITIONS -DUNICODE -D_UNICODE) +endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") diff --git a/app/common/bezier.cpp b/app/common/bezier.cpp index 10fd88a2a..a1d900d57 100644 --- a/app/common/bezier.cpp +++ b/app/common/bezier.cpp @@ -58,6 +58,10 @@ double Bezier::CalculateTFromX(bool cubic, double x, double a, double b, double double top = 1.0; while (true) { + if (bottom == top) { + return bottom; + } + double mid = (bottom + top) * 0.5; double test = cubic ? CubicTtoY(a, b, c, d, mid) : QuadraticTtoY(a, b, c, mid); diff --git a/app/common/xmlutils.cpp b/app/common/xmlutils.cpp index f778b6440..1b7820e26 100644 --- a/app/common/xmlutils.cpp +++ b/app/common/xmlutils.cpp @@ -24,37 +24,10 @@ #include "node/factory.h" #include "widget/nodeparamview/nodeparamviewundo.h" #include "widget/nodeview/nodeviewundo.h" +//#include "widget/timelinewidget/undo/timelineundogeneral.h" namespace olive { -void XMLConnectNodes(const XMLNodeData &xml_node_data, uint version, MultiUndoCommand *command) -{ - foreach (const XMLNodeData::SerializedConnection& con, xml_node_data.desired_connections) { - Node *out = xml_node_data.node_ptrs.value(con.output_node); - - if (out) { - // Use output param as hint tag since we grandfathered those in - Node::ValueHint hint(con.output_param); - - if (command) { - command->add_child(new NodeEdgeAddCommand(out, con.input)); - - if (version < 210907) { - /// Deprecated: backwards compatibility only - command->add_child(new NodeSetValueHintCommand(con.input, hint)); - } - } else { - Node::ConnectEdge(out, con.input); - - if (version < 210907) { - /// Deprecated: backwards compatibility only - con.input.node()->SetValueHintForInput(con.input.input(), hint, con.input.element()); - } - } - } - } -} - bool XMLReadNextStartElement(QXmlStreamReader *reader) { QXmlStreamReader::TokenType token; @@ -71,10 +44,59 @@ bool XMLReadNextStartElement(QXmlStreamReader *reader) return false; } -void XMLLinkBlocks(const XMLNodeData &xml_node_data) +void XMLNodeData::PostConnect(uint version, MultiUndoCommand *command) const { - foreach (const XMLNodeData::BlockLink& l, xml_node_data.block_links) { - Block::Link(l.block, static_cast(xml_node_data.node_ptrs.value(l.link))); + foreach (const XMLNodeData::SerializedConnection& con, desired_connections) { + if (Node *out = node_ptrs.value(con.output_node)) { + // Use output param as hint tag since we grandfathered those in + Node::ValueHint hint(con.output_param); + + if (command) { + command->add_child(new NodeEdgeAddCommand(out, con.input)); + } else { + Node::ConnectEdge(out, con.input); + } + + if (version < 210907) { + /// Deprecated: backwards compatibility only + if (command) { + command->add_child(new NodeSetValueHintCommand(con.input, hint)); + } else { + con.input.node()->SetValueHintForInput(con.input.input(), hint, con.input.element()); + } + } + } + } + + foreach (const XMLNodeData::BlockLink& l, block_links) { + Node *a = l.block; + Node *b = node_ptrs.value(l.link); + if (command) { + command->add_child(new NodeLinkCommand(a, b, true)); + } else { + Node::Link(a, b); + } + } + + foreach (const XMLNodeData::GroupLink &l, group_input_links) { + if (Node *input_node = node_ptrs.value(l.input_node)) { + NodeInput resolved(input_node, l.input_id, l.input_element); + if (command) { + command->add_child(new NodeGroupAddInputPassthrough(l.group, resolved)); + } else { + l.group->AddInputPassthrough(resolved); + } + } + } + + for (auto it=group_output_links.cbegin(); it!=group_output_links.cend(); it++) { + if (Node *output_node = node_ptrs.value(it.value())) { + if (command) { + command->add_child(new NodeGroupSetOutputPassthrough(it.key(), output_node)); + } else { + it.key()->SetOutputPassthrough(output_node); + } + } } } diff --git a/app/common/xmlutils.h b/app/common/xmlutils.h index e8b88d6a3..d0488ccd0 100644 --- a/app/common/xmlutils.h +++ b/app/common/xmlutils.h @@ -31,6 +31,7 @@ namespace olive { class Block; class Node; class NodeInput; +class NodeGroup; #define XMLAttributeLoop(reader, item) \ foreach (const QXmlStreamAttribute& item, reader->attributes()) @@ -49,14 +50,23 @@ struct XMLNodeData { quintptr link; }; + struct GroupLink { + NodeGroup *group; + quintptr input_node; + QString input_id; + int input_element; + }; + QHash node_ptrs; QList desired_connections; QList block_links; + QVector group_input_links; + QHash group_output_links; + + void PostConnect(uint version, MultiUndoCommand *command = nullptr) const; }; -void XMLConnectNodes(const XMLNodeData& xml_node_data, uint version, MultiUndoCommand *command = nullptr); - /** * @brief Workaround for QXmlStreamReader::readNextStartElement not detecting the end of a document * @@ -68,8 +78,6 @@ void XMLConnectNodes(const XMLNodeData& xml_node_data, uint version, MultiUndoCo */ bool XMLReadNextStartElement(QXmlStreamReader* reader); -void XMLLinkBlocks(const XMLNodeData& xml_node_data); - } #endif // XMLREADLOOP_H diff --git a/app/core.cpp b/app/core.cpp index 31eed8bbc..79aadb211 100644 --- a/app/core.cpp +++ b/app/core.cpp @@ -75,7 +75,7 @@ namespace olive { Core* Core::instance_ = nullptr; -const uint Core::kProjectVersion = 210907; +const uint Core::kProjectVersion = 211228; Core::Core(const CoreParams& params) : main_window_(nullptr), @@ -434,7 +434,7 @@ void Core::CreateNewSequence() command->add_child(new NodeAddCommand(active_project, new_sequence)); command->add_child(new FolderAddChild(GetSelectedFolderInActiveProject(), new_sequence)); - command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, Node::Position())); // Create and connect default nodes to new sequence new_sequence->add_default_nodes(command); @@ -1354,10 +1354,10 @@ void Core::SetPreferenceForRenderMode(RenderMode::Mode mode, const QString &pref Config::Current()[GetRenderModePreferencePrefix(mode, preference)] = value; } -void Core::LabelNodes(const QVector &nodes) +bool Core::LabelNodes(const QVector &nodes, MultiUndoCommand *parent) { if (nodes.isEmpty()) { - return; + return false; } bool ok; @@ -1386,8 +1386,16 @@ void Core::LabelNodes(const QVector &nodes) rename_command->AddNode(n, s); } - undo_stack_.push(rename_command); + if (parent) { + parent->add_child(rename_command); + } else { + undo_stack_.push(rename_command); + } + + return true; } + + return false; } Sequence *Core::CreateNewSequenceForProject(Project* project) const diff --git a/app/core.h b/app/core.h index 3ac9b1e70..003e9e3f3 100644 --- a/app/core.h +++ b/app/core.h @@ -253,7 +253,7 @@ public: /** * @brief Show a dialog to the user to rename a set of nodes */ - void LabelNodes(const QVector &nodes); + bool LabelNodes(const QVector &nodes, MultiUndoCommand *parent = nullptr); /** * @brief Create a new sequence named appropriately for the active project diff --git a/app/dialog/CMakeLists.txt b/app/dialog/CMakeLists.txt index ff82a6b2c..64c718d2a 100644 --- a/app/dialog/CMakeLists.txt +++ b/app/dialog/CMakeLists.txt @@ -21,9 +21,9 @@ add_subdirectory(color) add_subdirectory(configbase) add_subdirectory(diskcache) add_subdirectory(export) +add_subdirectory(footageproperties) add_subdirectory(footagerelink) add_subdirectory(keyframeproperties) -add_subdirectory(nodeproperties) add_subdirectory(preferences) add_subdirectory(progress) add_subdirectory(rendercancel) diff --git a/app/dialog/nodeproperties/CMakeLists.txt b/app/dialog/footageproperties/CMakeLists.txt similarity index 81% rename from app/dialog/nodeproperties/CMakeLists.txt rename to app/dialog/footageproperties/CMakeLists.txt index f1c44b5af..25287c233 100644 --- a/app/dialog/nodeproperties/CMakeLists.txt +++ b/app/dialog/footageproperties/CMakeLists.txt @@ -1,5 +1,5 @@ # Olive - Non-Linear Video Editor -# Copyright (C) 2021 Olive Team +# Copyright (C) 2020 Olive Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -14,9 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +add_subdirectory(streamproperties) + set(OLIVE_SOURCES ${OLIVE_SOURCES} - dialog/nodeproperties/nodepropertiesdialog.cpp - dialog/nodeproperties/nodepropertiesdialog.h + dialog/footageproperties/footageproperties.cpp + dialog/footageproperties/footageproperties.h PARENT_SCOPE ) diff --git a/app/dialog/footageproperties/footageproperties.cpp b/app/dialog/footageproperties/footageproperties.cpp new file mode 100644 index 000000000..7cb56c63a --- /dev/null +++ b/app/dialog/footageproperties/footageproperties.cpp @@ -0,0 +1,251 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "footageproperties.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "streamproperties/audiostreamproperties.h" +#include "streamproperties/videostreamproperties.h" +#include "widget/nodeview/nodeviewundo.h" + +namespace olive { + +FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent, Footage *footage) : + QDialog(parent), + footage_(footage) +{ + QGridLayout* layout = new QGridLayout(this); + + setWindowTitle(tr("\"%1\" Properties").arg(footage_->GetLabelOrName())); + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + int row = 0; + + layout->addWidget(new QLabel(tr("Name:")), row, 0); + + footage_name_field_ = new QLineEdit(footage_->GetLabel()); + layout->addWidget(footage_name_field_, row, 1); + row++; + + layout->addWidget(new QLabel(tr("Tracks:")), row, 0, 1, 2); + row++; + + track_list = new QListWidget(); + layout->addWidget(track_list, row, 0, 1, 2); + + row++; + + stacked_widget_ = new QStackedWidget(); + layout->addWidget(stacked_widget_, row, 0, 1, 2); + + int first_usable_stream = -1; + + for (int i=0; iGetTotalStreamCount(); i++) { + Track::Reference reference = footage_->GetReferenceFromRealIndex(i); + + QString description; + bool is_enabled = false; + + switch (reference.type()) { + case Track::kVideo: + { + stacked_widget_->addWidget(new VideoStreamProperties(footage_, reference.index())); + + VideoParams vp = footage_->GetVideoParams(reference.index()); + is_enabled = vp.enabled(); + description = tr("%1x%2 %3 FPS").arg(QString::number(vp.width()), QString::number(vp.height()), QString::number(vp.frame_rate().toDouble())); + break; + } + case Track::kAudio: + { + stacked_widget_->addWidget(new AudioStreamProperties(footage_, reference.index())); + + AudioParams ap = footage_->GetAudioParams(reference.index()); + is_enabled = ap.enabled(); + description = tr("%1 Hz %2 channels").arg(QString::number(ap.sample_rate()), QString::number(ap.channel_count())); + break; + } + default: + stacked_widget_->addWidget(new StreamProperties()); + description = tr("Unknown"); + break; + } + + QListWidgetItem* item = new QListWidgetItem(description, track_list); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(is_enabled ? Qt::Checked : Qt::Unchecked); + track_list->addItem(item); + + if (first_usable_stream == -1 + && (reference.type() == Track::kVideo + || reference.type() == Track::kAudio)) { + first_usable_stream = i; + } + } + + row++; + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->setCenterButtons(true); + layout->addWidget(buttons, row, 0, 1, 2); + + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(track_list, &QListWidget::currentRowChanged, stacked_widget_, &QStackedWidget::setCurrentIndex); + + // Auto-select first item that actually has properties + if (first_usable_stream >= 0) { + track_list->setCurrentRow(first_usable_stream); + } + track_list->setFocus(); +} + +void FootagePropertiesDialog::accept() +{ + // Perform sanity check on all pages + for (int i=0;icount();i++) { + if (!static_cast(stacked_widget_->widget(i))->SanityCheck()) { + // Switch to the failed panel in question + stacked_widget_->setCurrentIndex(i); + + // Do nothing (it's up to the property panel itself to throw the error message) + return; + } + } + + MultiUndoCommand* command = new MultiUndoCommand(); + + if (footage_->GetLabel() != footage_name_field_->text()) { + NodeRenameCommand *nrc = new NodeRenameCommand(); + nrc->AddNode(footage_, footage_name_field_->text()); + command->add_child(nrc); + } + + for (int i=0; iGetTotalStreamCount(); i++) { + Track::Reference reference = footage_->GetReferenceFromRealIndex(i); + bool new_stream_enabled = (track_list->item(i)->checkState() == Qt::Checked); + bool old_stream_enabled = new_stream_enabled; + + switch (reference.type()) { + case Track::kVideo: + old_stream_enabled = footage_->GetVideoParams(reference.index()).enabled(); + break; + case Track::kAudio: + old_stream_enabled = footage_->GetAudioParams(reference.index()).enabled(); + break; + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; + } + + if (old_stream_enabled != new_stream_enabled) { + command->add_child(new StreamEnableChangeCommand(footage_, + reference.type(), + reference.index(), + new_stream_enabled)); + } + } + + for (int i=0;icount();i++) { + static_cast(stacked_widget_->widget(i))->Accept(command); + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + QDialog::accept(); +} + +FootagePropertiesDialog::StreamEnableChangeCommand::StreamEnableChangeCommand(Footage *footage, Track::Type type, int index_in_type, bool enabled) : + footage_(footage), + type_(type), + index_(index_in_type), + new_enabled_(enabled) +{ +} + +Project *FootagePropertiesDialog::StreamEnableChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + +void FootagePropertiesDialog::StreamEnableChangeCommand::redo() +{ + switch (type_) { + case Track::kVideo: + { + VideoParams vp = footage_->GetVideoParams(index_); + old_enabled_ = vp.enabled(); + vp.set_enabled(new_enabled_); + footage_->SetVideoParams(vp, index_); + break; + } + case Track::kAudio: + { + AudioParams ap = footage_->GetAudioParams(index_); + old_enabled_ = ap.enabled(); + ap.set_enabled(new_enabled_); + footage_->SetAudioParams(ap, index_); + break; + } + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; + } +} + +void FootagePropertiesDialog::StreamEnableChangeCommand::undo() +{ + switch (type_) { + case Track::kVideo: + { + VideoParams vp = footage_->GetVideoParams(index_); + vp.set_enabled(old_enabled_); + footage_->SetVideoParams(vp, index_); + break; + } + case Track::kAudio: + { + AudioParams ap = footage_->GetAudioParams(index_); + ap.set_enabled(old_enabled_); + footage_->SetAudioParams(ap, index_); + break; + } + case Track::kSubtitle: + case Track::kNone: + case Track::kCount: + break; + } +} + +} diff --git a/app/dialog/footageproperties/footageproperties.h b/app/dialog/footageproperties/footageproperties.h new file mode 100644 index 000000000..7fd58a14e --- /dev/null +++ b/app/dialog/footageproperties/footageproperties.h @@ -0,0 +1,120 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef MEDIAPROPERTIESDIALOG_H +#define MEDIAPROPERTIESDIALOG_H + +#include +#include +#include +#include +#include +#include +#include + +#include "node/project/footage/footage.h" +#include "undo/undocommand.h" + +namespace olive { + +/** + * @brief The MediaPropertiesDialog class + * + * A dialog for setting properties on Media. This can be loaded from any part of the application provided it's given + * a valid Media object. + */ +class FootagePropertiesDialog : public QDialog { + Q_OBJECT +public: + /** + * @brief MediaPropertiesDialog Constructor + * + * @param parent + * + * QWidget parent. Usually MainWindow or Project panel. + * + * @param i + * + * Media object to set properties for. + */ + FootagePropertiesDialog(QWidget *parent, Footage* footage); +private: + class StreamEnableChangeCommand : public UndoCommand { + public: + StreamEnableChangeCommand(Footage *footage, + Track::Type type, + int index_in_type, + bool enabled); + + virtual Project* GetRelevantProject() const override; + + virtual void redo() override; + virtual void undo() override; + + private: + Footage *footage_; + Track::Type type_; + int index_; + + bool old_enabled_; + bool new_enabled_; + }; + + /** + * @brief Stack of widgets that changes based on whether the stream is a video or audio stream + */ + QStackedWidget* stacked_widget_; + + /** + * @brief ComboBox for interlacing setting + */ + QComboBox* interlacing_box; + + /** + * @brief Media name text field + */ + QLineEdit* footage_name_field_; + + /** + * @brief Internal pointer to Media object (set in constructor) + */ + Footage* footage_; + + /** + * @brief A list widget for listing the tracks in Media + */ + QListWidget* track_list; + + /** + * @brief Frame rate to conform to + */ + QDoubleSpinBox* conform_fr; + +private slots: + /** + * @brief Overridden accept function for saving the properties back to the Media class + */ + void accept(); + +}; + +} + +#endif // MEDIAPROPERTIESDIALOG_H diff --git a/app/dialog/footageproperties/streamproperties/CMakeLists.txt b/app/dialog/footageproperties/streamproperties/CMakeLists.txt new file mode 100644 index 000000000..3228e9520 --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/CMakeLists.txt @@ -0,0 +1,26 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2020 Olive Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(OLIVE_SOURCES + ${OLIVE_SOURCES} + dialog/footageproperties/streamproperties/streamproperties.h + dialog/footageproperties/streamproperties/streamproperties.cpp + dialog/footageproperties/streamproperties/audiostreamproperties.h + dialog/footageproperties/streamproperties/audiostreamproperties.cpp + dialog/footageproperties/streamproperties/videostreamproperties.h + dialog/footageproperties/streamproperties/videostreamproperties.cpp + PARENT_SCOPE +) diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp new file mode 100644 index 000000000..0359e7577 --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.cpp @@ -0,0 +1,37 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "audiostreamproperties.h" + +namespace olive { + +AudioStreamProperties::AudioStreamProperties(Footage *footage, int audio_index) : + footage_(footage), + audio_index_(audio_index) +{ +} + +void AudioStreamProperties::Accept(MultiUndoCommand*) +{ + Q_UNUSED(footage_) + Q_UNUSED(audio_index_) +} + +} diff --git a/app/dialog/footageproperties/streamproperties/audiostreamproperties.h b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h new file mode 100644 index 000000000..058ff2bfc --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/audiostreamproperties.h @@ -0,0 +1,45 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef AUDIOSTREAMPROPERTIES_H +#define AUDIOSTREAMPROPERTIES_H + +#include "node/project/footage/footage.h" +#include "streamproperties.h" + +namespace olive { + +class AudioStreamProperties : public StreamProperties +{ +public: + AudioStreamProperties(Footage *footage, int audio_index); + + virtual void Accept(MultiUndoCommand* parent) override; + +private: + Footage *footage_; + + int audio_index_; + +}; + +} + +#endif // AUDIOSTREAMPROPERTIES_H diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.cpp b/app/dialog/footageproperties/streamproperties/streamproperties.cpp new file mode 100644 index 000000000..96f3bbd5a --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/streamproperties.cpp @@ -0,0 +1,30 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "streamproperties.h" + +namespace olive { + +StreamProperties::StreamProperties(QWidget *parent) : + QWidget(parent) +{ +} + +} diff --git a/app/dialog/footageproperties/streamproperties/streamproperties.h b/app/dialog/footageproperties/streamproperties/streamproperties.h new file mode 100644 index 000000000..c8457216b --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/streamproperties.h @@ -0,0 +1,44 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef STREAMPROPERTIES_H +#define STREAMPROPERTIES_H + +#include + +#include "common/define.h" +#include "undo/undocommand.h" + +namespace olive { + +class StreamProperties : public QWidget +{ +public: + StreamProperties(QWidget* parent = nullptr); + + virtual void Accept(MultiUndoCommand*){} + + virtual bool SanityCheck(){return true;} + +}; + +} + +#endif // STREAMPROPERTIES_H diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp new file mode 100644 index 000000000..6fcbdd914 --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.cpp @@ -0,0 +1,271 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "videostreamproperties.h" + +#include +#include +#include +#include +#include + +#include "common/ocioutils.h" +#include "core.h" +#include "undo/undostack.h" + +namespace olive { + +VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index) : + footage_(footage), + video_index_(video_index), + video_premultiply_alpha_(nullptr) +{ + QGridLayout* video_layout = new QGridLayout(this); + video_layout->setMargin(0); + + int row = 0; + + video_layout->addWidget(new QLabel(tr("Pixel Aspect:")), row, 0); + + VideoParams vp = footage_->GetVideoParams(video_index_); + + pixel_aspect_combo_ = new PixelAspectRatioComboBox(); + pixel_aspect_combo_->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + video_layout->addWidget(pixel_aspect_combo_, row, 1); + + row++; + + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + + video_interlace_combo_ = new InterlacedComboBox(); + video_interlace_combo_->SetInterlaceMode(vp.interlacing()); + + video_layout->addWidget(video_interlace_combo_, row, 1); + + row++; + + video_layout->addWidget(new QLabel(tr("Color Space:")), row, 0); + + video_color_space_ = new QComboBox(); + OCIO::ConstConfigRcPtr config = footage_->project()->color_manager()->GetConfig(); + int number_of_colorspaces = config->getNumColorSpaces(); + + video_color_space_->addItem(tr("Default (%1)").arg(footage_->project()->color_manager()->GetDefaultInputColorSpace())); + + for (int i=0;igetColorSpaceNameByIndex(i); + + video_color_space_->addItem(colorspace); + } + + video_color_space_->setCurrentText(vp.colorspace()); + + video_layout->addWidget(video_color_space_, row, 1); + + if (vp.channel_count() == VideoParams::kRGBAChannelCount) { + row++; + + video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha")); + video_premultiply_alpha_->setChecked(vp.premultiplied_alpha()); + video_layout->addWidget(video_premultiply_alpha_, row, 0, 1, 2); + } + + row++; + + if (vp.video_type() == VideoParams::kVideoTypeImageSequence) { + QGroupBox* imgseq_group = new QGroupBox(tr("Image Sequence")); + QGridLayout* imgseq_layout = new QGridLayout(imgseq_group); + + int imgseq_row = 0; + + imgseq_layout->addWidget(new QLabel(tr("Start Index:")), imgseq_row, 0); + + imgseq_start_time_ = new IntegerSlider(); + imgseq_start_time_->SetMinimum(0); + imgseq_start_time_->SetValue(vp.start_time()); + imgseq_layout->addWidget(imgseq_start_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("End Index:")), imgseq_row, 0); + + imgseq_end_time_ = new IntegerSlider(); + imgseq_end_time_->SetMinimum(0); + imgseq_end_time_->SetValue(vp.start_time() + vp.duration() - 1); + imgseq_layout->addWidget(imgseq_end_time_, imgseq_row, 1); + + imgseq_row++; + + imgseq_layout->addWidget(new QLabel(tr("Frame Rate:")), imgseq_row, 0); + + imgseq_frame_rate_ = new FrameRateComboBox(); + imgseq_frame_rate_->SetFrameRate(vp.frame_rate()); + imgseq_layout->addWidget(imgseq_frame_rate_, imgseq_row, 1); + + video_layout->addWidget(imgseq_group, row, 0, 1, 2); + } +} + +void VideoStreamProperties::Accept(MultiUndoCommand *parent) +{ + QString set_colorspace; + + if (video_color_space_->currentIndex() > 0) { + set_colorspace = video_color_space_->currentText(); + } + + VideoParams vp = footage_->GetVideoParams(video_index_); + + if ((video_premultiply_alpha_ && video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) + || set_colorspace != vp.colorspace() + || static_cast(video_interlace_combo_->currentIndex()) != vp.interlacing() + || pixel_aspect_combo_->GetPixelAspectRatio() != vp.pixel_aspect_ratio()) { + + parent->add_child(new VideoStreamChangeCommand(footage_, + video_index_, + video_premultiply_alpha_ ? video_premultiply_alpha_->isChecked() : vp.premultiplied_alpha(), + set_colorspace, + static_cast(video_interlace_combo_->currentIndex()), + pixel_aspect_combo_->GetPixelAspectRatio())); + } + + if (vp.video_type() == VideoParams::kVideoTypeImageSequence) { + int64_t new_dur = imgseq_end_time_->GetValue() - imgseq_start_time_->GetValue() + 1; + + if (vp.start_time() != imgseq_start_time_->GetValue() + || vp.duration() != new_dur + || vp.frame_rate() != imgseq_frame_rate_->GetFrameRate()) { + parent->add_child(new ImageSequenceChangeCommand(footage_, + video_index_, + imgseq_start_time_->GetValue(), + new_dur, + imgseq_frame_rate_->GetFrameRate())); + } + } +} + +bool VideoStreamProperties::SanityCheck() +{ + if (footage_->GetVideoParams(video_index_).video_type() == VideoParams::kVideoTypeImageSequence) { + if (imgseq_start_time_->GetValue() >= imgseq_end_time_->GetValue()) { + QMessageBox::critical(this, + tr("Invalid Configuration"), + tr("Image sequence end index must be a value higher than the start index."), + QMessageBox::Ok); + return false; + } + } + + return true; +} + +VideoStreamProperties::VideoStreamChangeCommand::VideoStreamChangeCommand(Footage *footage, + int video_index, + bool premultiplied, + QString colorspace, + VideoParams::Interlacing interlacing, + const rational &pixel_ar) : + footage_(footage), + video_index_(video_index), + new_premultiplied_(premultiplied), + new_colorspace_(colorspace), + new_interlacing_(interlacing), + new_pixel_ar_(pixel_ar) +{ +} + +Project *VideoStreamProperties::VideoStreamChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + +void VideoStreamProperties::VideoStreamChangeCommand::redo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + old_premultiplied_ = vp.premultiplied_alpha(); + old_colorspace_ = vp.colorspace(); + old_interlacing_ = vp.interlacing(); + old_pixel_ar_ = vp.pixel_aspect_ratio(); + + vp.set_premultiplied_alpha(new_premultiplied_); + vp.set_colorspace(new_colorspace_); + vp.set_interlacing(new_interlacing_); + vp.set_pixel_aspect_ratio(new_pixel_ar_); + + footage_->SetVideoParams(vp, video_index_); +} + +void VideoStreamProperties::VideoStreamChangeCommand::undo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + vp.set_premultiplied_alpha(old_premultiplied_); + vp.set_colorspace(old_colorspace_); + vp.set_interlacing(old_interlacing_); + vp.set_pixel_aspect_ratio(old_pixel_ar_); + + footage_->SetVideoParams(vp, video_index_); +} + +VideoStreamProperties::ImageSequenceChangeCommand::ImageSequenceChangeCommand(Footage *footage, int video_index, int64_t start_index, int64_t duration, const rational &frame_rate) : + footage_(footage), + video_index_(video_index), + new_start_index_(start_index), + new_duration_(duration), + new_frame_rate_(frame_rate) +{ +} + +Project *VideoStreamProperties::ImageSequenceChangeCommand::GetRelevantProject() const +{ + return footage_->project(); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::redo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + old_start_index_ = vp.start_time(); + vp.set_start_time(new_start_index_); + + old_duration_ = vp.duration(); + vp.set_duration(new_duration_); + + old_frame_rate_ = vp.frame_rate(); + vp.set_frame_rate(new_frame_rate_); + vp.set_time_base(new_frame_rate_.flipped()); + + footage_->SetVideoParams(vp, video_index_); +} + +void VideoStreamProperties::ImageSequenceChangeCommand::undo() +{ + VideoParams vp = footage_->GetVideoParams(video_index_); + + vp.set_start_time(old_start_index_); + vp.set_duration(old_duration_); + vp.set_frame_rate(old_frame_rate_); + vp.set_time_base(old_frame_rate_.flipped()); + + footage_->SetVideoParams(vp, video_index_); +} + +} diff --git a/app/dialog/footageproperties/streamproperties/videostreamproperties.h b/app/dialog/footageproperties/streamproperties/videostreamproperties.h new file mode 100644 index 000000000..3d858b2c1 --- /dev/null +++ b/app/dialog/footageproperties/streamproperties/videostreamproperties.h @@ -0,0 +1,146 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2020 Olive Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef VIDEOSTREAMPROPERTIES_H +#define VIDEOSTREAMPROPERTIES_H + +#include +#include + +#include "node/project/footage/footage.h" +#include "streamproperties.h" +#include "widget/slider/integerslider.h" +#include "widget/standardcombos/standardcombos.h" + +namespace olive { + +class VideoStreamProperties : public StreamProperties +{ + Q_OBJECT +public: + VideoStreamProperties(Footage *footage, int video_index); + + virtual void Accept(MultiUndoCommand *parent) override; + + virtual bool SanityCheck() override; + +private: + Footage *footage_; + + int video_index_; + + /** + * @brief Setting for associated/premultiplied alpha + */ + QCheckBox* video_premultiply_alpha_; + + /** + * @brief Setting for this media's color space + */ + QComboBox* video_color_space_; + + /** + * @brief Setting for video interlacing + */ + InterlacedComboBox* video_interlace_combo_; + + /** + * @brief Sets the start index for image sequences + */ + IntegerSlider* imgseq_start_time_; + + /** + * @brief Sets the end index for image sequences + */ + IntegerSlider* imgseq_end_time_; + + /** + * @brief Sets the frame rate for image sequences + */ + FrameRateComboBox* imgseq_frame_rate_; + + /** + * @brief Sets the pixel aspect ratio of the stream + */ + PixelAspectRatioComboBox* pixel_aspect_combo_; + + class VideoStreamChangeCommand : public UndoCommand { + public: + VideoStreamChangeCommand(Footage *footage, + int video_index, + bool premultiplied, + QString colorspace, + VideoParams::Interlacing interlacing, + const rational& pixel_ar); + + virtual Project* GetRelevantProject() const override; + + virtual void redo() override; + virtual void undo() override; + + private: + Footage *footage_; + int video_index_; + + bool new_premultiplied_; + QString new_colorspace_; + VideoParams::Interlacing new_interlacing_; + rational new_pixel_ar_; + + bool old_premultiplied_; + QString old_colorspace_; + VideoParams::Interlacing old_interlacing_; + rational old_pixel_ar_; + + }; + + class ImageSequenceChangeCommand : public UndoCommand { + public: + ImageSequenceChangeCommand(Footage *footage, + int video_index, + int64_t start_index, + int64_t duration, + const rational& frame_rate); + + virtual Project* GetRelevantProject() const override; + + virtual void redo() override; + virtual void undo() override; + + private: + Footage *footage_; + int video_index_; + + int64_t new_start_index_; + int64_t old_start_index_; + + int64_t new_duration_; + int64_t old_duration_; + + rational new_frame_rate_; + rational old_frame_rate_; + + }; + +}; + +} + +#endif // VIDEOSTREAMPROPERTIES_H diff --git a/app/dialog/nodeproperties/nodepropertiesdialog.cpp b/app/dialog/nodeproperties/nodepropertiesdialog.cpp deleted file mode 100644 index 123b6c69b..000000000 --- a/app/dialog/nodeproperties/nodepropertiesdialog.cpp +++ /dev/null @@ -1,74 +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 "nodepropertiesdialog.h" - -#include -#include - -#include "core.h" -#include "widget/nodeview/nodeviewundo.h" - -namespace olive { - -NodePropertiesDialog::NodePropertiesDialog(Node *node, const rational &timebase, QWidget *parent) : - QDialog(parent), - node_(node) -{ - setWindowTitle(tr("Node Properties")); - - QVBoxLayout *layout = new QVBoxLayout(this); - - QHBoxLayout *label_layout = new QHBoxLayout(); - label_layout->setMargin(0); - layout->addLayout(label_layout); - - label_layout->addWidget(new QLabel(tr("Name:"))); - - label_edit_ = new QLineEdit(); - label_edit_->setText(node->GetLabel()); - label_layout->addWidget(label_edit_); - - NodeParamViewItem *item = new NodeParamViewItem(node); - item->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - item->SetTimebase(timebase); - item->setTitleBarWidget(new QWidget()); - layout->addWidget(item); - - layout->addStretch(); - - QDialogButtonBox *btns = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - connect(btns, &QDialogButtonBox::accepted, this, &NodePropertiesDialog::accept); - connect(btns, &QDialogButtonBox::rejected, this, &NodePropertiesDialog::reject); - layout->addWidget(btns); -} - -void NodePropertiesDialog::accept() -{ - if (label_edit_->text() != node_->GetLabel()) { - NodeRenameCommand* rename_command = new NodeRenameCommand(); - rename_command->AddNode(node_, label_edit_->text()); - Core::instance()->undo_stack()->push(rename_command); - } - - QDialog::accept(); -} - -} diff --git a/app/dialog/sequence/sequencedialogparametertab.cpp b/app/dialog/sequence/sequencedialogparametertab.cpp index 9d77d8bc7..00f89e002 100644 --- a/app/dialog/sequence/sequencedialogparametertab.cpp +++ b/app/dialog/sequence/sequencedialogparametertab.cpp @@ -19,11 +19,28 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg // Set up video section QGroupBox* video_group = new QGroupBox(); video_group->setTitle(tr("Video")); - QHBoxLayout* video_layout = new QHBoxLayout(video_group); - video_section_ = new VideoParamEdit(); - video_section_->SetParameterMask(Sequence::kVideoParamEditMask); - connect(video_section_, &VideoParamEdit::Changed, this, &SequenceDialogParameterTab::UpdatePreviewResolutionLabel); - video_layout->addWidget(video_section_); + QGridLayout *video_layout = new QGridLayout(video_group); + video_layout->addWidget(new QLabel(tr("Width:")), row, 0); + width_slider_ = new IntegerSlider(); + width_slider_->SetMinimum(0); + video_layout->addWidget(width_slider_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Height:")), row, 0); + height_slider_ = new IntegerSlider(); + height_slider_->SetMinimum(0); + video_layout->addWidget(height_slider_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Frame Rate:")), row, 0); + framerate_combo_ = new FrameRateComboBox(); + video_layout->addWidget(framerate_combo_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Pixel Aspect Ratio:")), row, 0); + pixelaspect_combo_ = new PixelAspectRatioComboBox(); + video_layout->addWidget(pixelaspect_combo_, row, 1); + row++; + video_layout->addWidget(new QLabel(tr("Interlacing:")), row, 0); + interlacing_combo_ = new InterlacedComboBox(); + video_layout->addWidget(interlacing_combo_, row, 1); layout->addWidget(video_group); row = 0; @@ -65,7 +82,11 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg // Set values based on input sequence VideoParams vp = sequence->GetVideoParams(); AudioParams ap = sequence->GetAudioParams(); - video_section_->SetVideoParams(vp); + width_slider_->SetValue(vp.width()); + height_slider_->SetValue(vp.height()); + framerate_combo_->SetFrameRate(vp.time_base().flipped()); + pixelaspect_combo_->SetPixelAspectRatio(vp.pixel_aspect_ratio()); + interlacing_combo_->SetInterlaceMode(vp.interlacing()); preview_resolution_field_->SetDivider(vp.divider()); preview_format_field_->SetPixelFormat(vp.format()); preview_autocache_field_->setChecked(sequence->GetVideoAutoCacheEnabled()); @@ -86,11 +107,11 @@ SequenceDialogParameterTab::SequenceDialogParameterTab(Sequence* sequence, QWidg void SequenceDialogParameterTab::PresetChanged(const SequencePreset &preset) { - video_section_->SetWidth(preset.width()); - video_section_->SetHeight(preset.height()); - video_section_->SetFrameRate(preset.frame_rate()); - video_section_->SetPixelAspectRatio(preset.pixel_aspect()); - video_section_->SetInterlaceMode(preset.interlacing()); + width_slider_->SetValue(preset.width()); + height_slider_->SetValue(preset.height()); + framerate_combo_->SetFrameRate(preset.frame_rate()); + pixelaspect_combo_->SetPixelAspectRatio(preset.pixel_aspect()); + interlacing_combo_->SetInterlaceMode(preset.interlacing()); audio_sample_rate_field_->SetSampleRate(preset.sample_rate()); audio_channels_field_->SetChannelLayout(preset.channel_layout()); preview_resolution_field_->SetDivider(preset.preview_divider()); @@ -115,8 +136,8 @@ void SequenceDialogParameterTab::SavePresetClicked() void SequenceDialogParameterTab::UpdatePreviewResolutionLabel() { - VideoParams test_param(video_section_->GetWidth(), - video_section_->GetHeight(), + VideoParams test_param(GetSelectedVideoWidth(), + GetSelectedVideoHeight(), VideoParams::kFormatInvalid, VideoParams::kInternalChannelCount, rational(1), diff --git a/app/dialog/sequence/sequencedialogparametertab.h b/app/dialog/sequence/sequencedialogparametertab.h index 6d4d3bf08..e11561b2f 100644 --- a/app/dialog/sequence/sequencedialogparametertab.h +++ b/app/dialog/sequence/sequencedialogparametertab.h @@ -1,6 +1,7 @@ #ifndef SEQUENCEDIALOGPARAMETERTAB_H #define SEQUENCEDIALOGPARAMETERTAB_H +#include #include #include #include @@ -9,7 +10,6 @@ #include "sequencepreset.h" #include "widget/slider/integerslider.h" #include "widget/standardcombos/standardcombos.h" -#include "widget/videoparamedit/videoparamedit.h" namespace olive { @@ -21,27 +21,27 @@ public: int GetSelectedVideoWidth() const { - return video_section_->GetWidth(); + return width_slider_->GetValue(); } int GetSelectedVideoHeight() const { - return video_section_->GetHeight(); + return height_slider_->GetValue(); } rational GetSelectedVideoFrameRate() const { - return video_section_->GetFrameRate(); + return framerate_combo_->GetFrameRate(); } rational GetSelectedVideoPixelAspect() const { - return video_section_->GetPixelAspectRatio(); + return pixelaspect_combo_->GetPixelAspectRatio(); } VideoParams::Interlacing GetSelectedVideoInterlacingMode() const { - return video_section_->GetInterlaceMode(); + return interlacing_combo_->GetInterlaceMode(); } int GetSelectedAudioSampleRate() const @@ -76,7 +76,15 @@ signals: void SaveParametersAsPreset(const SequencePreset& preset); private: - VideoParamEdit* video_section_; + IntegerSlider *width_slider_; + + IntegerSlider *height_slider_; + + FrameRateComboBox *framerate_combo_; + + PixelAspectRatioComboBox *pixelaspect_combo_; + + InterlacedComboBox *interlacing_combo_; SampleRateComboBox* audio_sample_rate_field_; diff --git a/app/node/CMakeLists.txt b/app/node/CMakeLists.txt index d10d5746f..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) diff --git a/app/node/block/block.cpp b/app/node/block/block.cpp index 34f87b2e1..a726d6b0f 100644 --- a/app/node/block/block.cpp +++ b/app/node/block/block.cpp @@ -47,6 +47,8 @@ Block::Block() : IgnoreHashingFrom(kLengthInput); AddInput(kEnabledInput, NodeValue::kBoolean, true, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable)); + + SetFlags(kDontShowInParamView); } QVector Block::Category() const diff --git a/app/node/distort/crop/cropdistortnode.cpp b/app/node/distort/crop/cropdistortnode.cpp index 5018a8f5a..009e05310 100644 --- a/app/node/distort/crop/cropdistortnode.cpp +++ b/app/node/distort/crop/cropdistortnode.cpp @@ -233,13 +233,11 @@ void CropDistortNode::GizmoMove(const QPointF &p, const rational &time, const Qt } } -void CropDistortNode::GizmoRelease() +void CropDistortNode::GizmoRelease(MultiUndoCommand *command) { - MultiUndoCommand *command = new MultiUndoCommand(); for (NodeInputDragger& i : gizmo_dragger_) { i.End(command); } - Core::instance()->undo_stack()->push(command); gizmo_dragger_.clear(); gizmo_start_.clear(); diff --git a/app/node/distort/crop/cropdistortnode.h b/app/node/distort/crop/cropdistortnode.h index a20998a7d..24f2c0b50 100644 --- a/app/node/distort/crop/cropdistortnode.h +++ b/app/node/distort/crop/cropdistortnode.h @@ -76,7 +76,7 @@ public: virtual bool GizmoPress(const NodeValueRow& row, const NodeGlobals &globals, const QPointF &p) override; virtual void GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) override; - virtual void GizmoRelease() override; + virtual void GizmoRelease(MultiUndoCommand *command) override; static const QString kTextureInput; static const QString kLeftInput; diff --git a/app/node/distort/transform/transformdistortnode.cpp b/app/node/distort/transform/transformdistortnode.cpp index df6266a7a..483b9f001 100644 --- a/app/node/distort/transform/transformdistortnode.cpp +++ b/app/node/distort/transform/transformdistortnode.cpp @@ -301,13 +301,11 @@ void TransformDistortNode::GizmoMove(const QPointF &p, const rational &time, con } } -void TransformDistortNode::GizmoRelease() +void TransformDistortNode::GizmoRelease(MultiUndoCommand *command) { - MultiUndoCommand *command = new MultiUndoCommand(); for (NodeInputDragger& i : gizmo_dragger_) { i.End(command); } - Core::instance()->undo_stack()->push(command); gizmo_dragger_.clear(); gizmo_start_.clear(); diff --git a/app/node/distort/transform/transformdistortnode.h b/app/node/distort/transform/transformdistortnode.h index 17bf541a0..6bfe5e2f3 100644 --- a/app/node/distort/transform/transformdistortnode.h +++ b/app/node/distort/transform/transformdistortnode.h @@ -78,7 +78,7 @@ public: virtual bool GizmoPress(const NodeValueRow &row, const NodeGlobals &globals, const QPointF &p) override; virtual void GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) override; - virtual void GizmoRelease() override; + virtual void GizmoRelease(MultiUndoCommand *command) override; enum AutoScaleType { kAutoScaleNone, diff --git a/app/node/factory.cpp b/app/node/factory.cpp index f4368d898..447ba0dbd 100644 --- a/app/node/factory.cpp +++ b/app/node/factory.cpp @@ -54,6 +54,7 @@ namespace olive { QList NodeFactory::library_; +QVector NodeFactory::hidden_; void NodeFactory::Initialize() { @@ -65,6 +66,9 @@ void NodeFactory::Initialize() library_.append(created_node); } + + hidden_.append(kTextGeneratorLegacy); + hidden_.append(kGroupNode); } void NodeFactory::Destroy() @@ -86,6 +90,11 @@ Menu *NodeFactory::CreateMenu(QWidget* parent, bool create_none_item, Node::Cate continue; } + if (hidden_.contains(i)) { + // Skip this node + continue; + } + // Make sure nodes are up-to-date with the current translation n->Retranslate(); @@ -241,6 +250,8 @@ Node *NodeFactory::CreateFromFactoryIndex(const NodeFactory::InternalID &id) return new SubtitleBlock(); case kShapeGenerator: return new ShapeNode(); + case kGroupNode: + return new NodeGroup(); case kInternalNodeCount: break; diff --git a/app/node/factory.h b/app/node/factory.h index 1d8cd72f2..10101e648 100644 --- a/app/node/factory.h +++ b/app/node/factory.h @@ -61,6 +61,7 @@ public: kTimeRemapNode, kSubtitleBlock, kShapeGenerator, + kGroupNode, // Count value kInternalNodeCount @@ -87,6 +88,8 @@ public: private: static QList library_; + static QVector hidden_; + }; } diff --git a/app/node/generator/shape/shapenodebase.cpp b/app/node/generator/shape/shapenodebase.cpp index 8211bb180..d5c03b0ac 100644 --- a/app/node/generator/shape/shapenodebase.cpp +++ b/app/node/generator/shape/shapenodebase.cpp @@ -297,13 +297,11 @@ void ShapeNodeBase::GizmoMove(const QPointF &p, const rational &time, const Qt:: } } -void ShapeNodeBase::GizmoRelease() +void ShapeNodeBase::GizmoRelease(MultiUndoCommand *command) { - MultiUndoCommand *command = new MultiUndoCommand(); for (NodeInputDragger& i : gizmo_dragger_) { i.End(command); } - Core::instance()->undo_stack()->push(command); gizmo_dragger_.clear(); } diff --git a/app/node/generator/shape/shapenodebase.h b/app/node/generator/shape/shapenodebase.h index 819d4cb94..7337d5b6c 100644 --- a/app/node/generator/shape/shapenodebase.h +++ b/app/node/generator/shape/shapenodebase.h @@ -49,7 +49,7 @@ public: virtual bool GizmoPress(const NodeValueRow& row, const NodeGlobals &globals, const QPointF &p) override; virtual void GizmoMove(const QPointF &p, const rational &time, const Qt::KeyboardModifiers &modifiers) override; - virtual void GizmoRelease() override; + virtual void GizmoRelease(MultiUndoCommand *command) override; private: static QVector2D GenerateGizmoAnchor(const QVector2D &pos, const QVector2D &size, int drag, QVector2D *pt); diff --git a/app/node/graph.cpp b/app/node/graph.cpp index a2d04b233..f9a68c500 100644 --- a/app/node/graph.cpp +++ b/app/node/graph.cpp @@ -44,26 +44,12 @@ void NodeGraph::Clear() } } -qreal NodeGraph::GetNodeContextHeight(Node *context) -{ - const PositionMap &map = position_map_.value(context); - - qreal top = 0, bottom = 0; - - foreach (const QPointF &pt, map) { - top = qMin(pt.y(), top); - bottom = qMax(pt.y(), bottom); - } - - return bottom - top; -} - -int NodeGraph::GetNumberOfContextsNodeIsIn(Node *node) const +int NodeGraph::GetNumberOfContextsNodeIsIn(Node *node, bool except_itself) const { int count = 0; - for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { - if (it.value().contains(node)) { + foreach (Node *ctx, node_children_) { + if (ctx->ContextContainsNode(node) && (!except_itself || ctx != node)) { count++; } } @@ -71,18 +57,6 @@ int NodeGraph::GetNumberOfContextsNodeIsIn(Node *node) const return count; } -bool NodeGraph::NodeOutputsToContext(Node *node) const -{ - for (auto it=position_map_.cbegin(); it!=position_map_.cend(); it++) { - const PositionMap &pm = it.value(); - if (pm.contains(node) && node->OutputsTo(it.key(), true)) { - return true; - } - } - - return false; -} - void NodeGraph::childEvent(QChildEvent *event) { super::childEvent(event); @@ -100,6 +74,12 @@ void NodeGraph::childEvent(QChildEvent *event) connect(node, &Node::ValueChanged, this, &NodeGraph::ValueChanged, Qt::DirectConnection); connect(node, &Node::InputValueHintChanged, this, &NodeGraph::InputValueHintChanged, Qt::DirectConnection); + if (NodeGroup *group = dynamic_cast(node)) { + connect(group, &NodeGroup::InputPassthroughAdded, this, &NodeGraph::GroupAddedInputPassthrough, Qt::DirectConnection); + connect(group, &NodeGroup::InputPassthroughRemoved, this, &NodeGraph::GroupRemovedInputPassthrough, Qt::DirectConnection); + connect(group, &NodeGroup::OutputPassthroughChanged, this, &NodeGraph::GroupChangedOutputPassthrough, Qt::DirectConnection); + } + emit NodeAdded(node); emit node->AddedToGraph(this); @@ -113,21 +93,19 @@ void NodeGraph::childEvent(QChildEvent *event) disconnect(node, &Node::ValueChanged, this, &NodeGraph::ValueChanged); disconnect(node, &Node::InputValueHintChanged, this, &NodeGraph::InputValueHintChanged); + if (NodeGroup *group = dynamic_cast(node)) { + disconnect(group, &NodeGroup::InputPassthroughAdded, this, &NodeGraph::GroupAddedInputPassthrough); + disconnect(group, &NodeGroup::InputPassthroughRemoved, this, &NodeGraph::GroupRemovedInputPassthrough); + disconnect(group, &NodeGroup::OutputPassthroughChanged, this, &NodeGraph::GroupChangedOutputPassthrough); + } + emit NodeRemoved(node); emit node->RemovedFromGraph(this); - for (auto it=position_map_.begin(); it!=position_map_.end(); it++) { - PositionMap &map = it.value(); - for (auto jt=map.begin(); jt!=map.end(); ) { - if (jt.key() == node) { - jt = map.erase(jt); - emit NodePositionRemoved(node, it.key()); - } else { - jt++; - } - } + // Remove from any contexts + foreach (Node *context, node_children_) { + context->RemoveNodeFromContext(node); } - } } } diff --git a/app/node/graph.h b/app/node/graph.h index 5e0aa5be2..443e20748 100644 --- a/app/node/graph.h +++ b/app/node/graph.h @@ -21,6 +21,7 @@ #ifndef NODEGRAPH_H #define NODEGRAPH_H +#include "node/group/group.h" #include "node/node.h" namespace olive { @@ -63,54 +64,7 @@ public: return default_nodes_; } - bool NodeMapContainsNode(Node* node, Node* context) const - { - return position_map_.value(context).contains(node); - } - - QPointF GetNodePosition(Node* node, Node* context) - { - return position_map_.value(context).value(node); - } - - void SetNodePosition(Node* node, Node* context, const QPointF& pos) - { - position_map_[context].insert(node, pos); - emit NodePositionAdded(node, context, pos); - } - - void RemoveNodePosition(Node* node, Node* context) - { - PositionMap& map = position_map_[context]; - map.remove(node); - if (map.isEmpty()) { - position_map_.remove(context); - } - emit NodePositionRemoved(node, context); - } - - bool ContextContainsNode(Node *node, Node *context) - { - return position_map_[context].contains(node); - } - - qreal GetNodeContextHeight(Node *context); - - using PositionMap = QHash; - - const PositionMap &GetNodesForContext(Node *context) - { - return position_map_[context]; - } - - const QMap &GetPositionMap() const - { - return position_map_; - } - - int GetNumberOfContextsNodeIsIn(Node *node) const; - - bool NodeOutputsToContext(Node *node) const; + int GetNumberOfContextsNodeIsIn(Node *node, bool except_itself = false) const; signals: /** @@ -131,9 +85,11 @@ signals: void InputValueHintChanged(const NodeInput& input); - void NodePositionAdded(Node *node, Node *relative, const QPointF &position); + void GroupAddedInputPassthrough(NodeGroup *group, const NodeInput &input); - void NodePositionRemoved(Node *node, Node *relative); + void GroupRemovedInputPassthrough(NodeGroup *group, const NodeInput &input); + + void GroupChangedOutputPassthrough(NodeGroup *group, Node *output); protected: void AddDefaultNode(Node* n) @@ -148,10 +104,6 @@ private: QVector default_nodes_; - QMap position_map_; - - PositionMap root_position_map_; - }; } diff --git a/app/widget/videoparamedit/CMakeLists.txt b/app/node/group/CMakeLists.txt similarity index 90% rename from app/widget/videoparamedit/CMakeLists.txt rename to app/node/group/CMakeLists.txt index bd799b34d..3c0bce4d6 100644 --- a/app/widget/videoparamedit/CMakeLists.txt +++ b/app/node/group/CMakeLists.txt @@ -16,7 +16,7 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - widget/videoparamedit/videoparamedit.cpp - widget/videoparamedit/videoparamedit.h + 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..e64b551d8 --- /dev/null +++ b/app/node/group/group.cpp @@ -0,0 +1,215 @@ +/*** + + 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 { + +#define super Node + +NodeGroup::NodeGroup() : + output_passthrough_(nullptr) +{ +} + +QString NodeGroup::Name() const +{ + return tr("Group"); +} + +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() +{ + for (auto it=GetContextPositions().cbegin(); it!=GetContextPositions().cend(); it++) { + it.key()->Retranslate(); + } +} + +void NodeGroup::AddInputPassthrough(const NodeInput &input) +{ + Q_ASSERT(ContextContainsNode(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); + + emit InputPassthroughAdded(this, 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); + emit InputPassthroughRemoved(this, it.value()); + break; + } + } +} + +void NodeGroup::SetOutputPassthrough(Node *node) +{ + Q_ASSERT(!node || ContextContainsNode(node)); + + output_passthrough_ = node; + + emit OutputPassthroughChanged(this, output_passthrough_); +} + +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; +} + +QString NodeGroup::GetInputName(const QString &id) const +{ + return input_passthroughs_.value(id).name(); +} + +bool NodeGroup::LoadCustom(QXmlStreamReader *reader, XMLNodeData &xml_node_data, uint version, const QAtomicInt *cancelled) +{ + if (reader->name() == QStringLiteral("inputpassthroughs")) { + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("inputpassthrough")) { + XMLNodeData::GroupLink link; + + link.group = this; + + while (XMLReadNextStartElement(reader)) { + if (reader->name() == QStringLiteral("node")) { + link.input_node = reader->readElementText().toULongLong(); + } else if (reader->name() == QStringLiteral("input")) { + link.input_id = reader->readElementText(); + } else if (reader->name() == QStringLiteral("element")) { + link.input_element = reader->readElementText().toInt(); + } else { + reader->skipCurrentElement(); + } + } + + xml_node_data.group_input_links.append(link); + } else { + reader->skipCurrentElement(); + } + } + + return true; + } else if (reader->name() == QStringLiteral("outputpassthrough")) { + xml_node_data.group_output_links.insert(this, reader->readElementText().toULongLong()); + return true; + } else { + return super::LoadCustom(reader, xml_node_data, version, cancelled); + } +} + +void NodeGroup::SaveCustom(QXmlStreamWriter *writer) const +{ + super::SaveCustom(writer); + + writer->writeStartElement(QStringLiteral("inputpassthroughs")); + + foreach (const NodeInput &ip, input_passthroughs_) { + writer->writeStartElement(QStringLiteral("inputpassthrough")); + writer->writeTextElement(QStringLiteral("node"), QString::number(reinterpret_cast(ip.node()))); + writer->writeTextElement(QStringLiteral("input"), ip.input()); + writer->writeTextElement(QStringLiteral("element"), QString::number(ip.element())); + writer->writeEndElement(); // input + } + + writer->writeEndElement(); // inputpassthroughs + + writer->writeTextElement(QStringLiteral("outputpassthrough"), QString::number(reinterpret_cast(output_passthrough_))); +} + +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_); + } +} + +void NodeGroupSetOutputPassthrough::redo() +{ + old_output_ = group_->GetOutputPassthrough(); + group_->SetOutputPassthrough(new_output_); +} + +void NodeGroupSetOutputPassthrough::undo() +{ + group_->SetOutputPassthrough(old_output_); +} + +} diff --git a/app/node/group/group.h b/app/node/group/group.h new file mode 100644 index 000000000..979a52bc7 --- /dev/null +++ b/app/node/group/group.h @@ -0,0 +1,141 @@ +/*** + + 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 AddInputPassthrough(const NodeInput &input); + + void RemoveInputPassthrough(const NodeInput &input); + + Node *GetOutputPassthrough() const + { + return output_passthrough_; + } + + void SetOutputPassthrough(Node *node); + + static QString GetGroupInputIDFromInput(const NodeInput &input); + + const QHash &GetInputPassthroughs() const + { + return input_passthroughs_; + } + + bool ContainsInputPassthrough(const NodeInput &input) const; + + virtual QString GetInputName(const QString& id) const override; + +signals: + void InputPassthroughAdded(NodeGroup *group, const NodeInput &input); + + void InputPassthroughRemoved(NodeGroup *group, const NodeInput &input); + + void OutputPassthroughChanged(NodeGroup *group, Node *output); + +protected: + virtual bool LoadCustom(QXmlStreamReader* reader, XMLNodeData& xml_node_data, uint version, const QAtomicInt* cancelled) override; + + virtual void SaveCustom(QXmlStreamWriter* writer) const override; + +private: + QHash input_passthroughs_; + + Node *output_passthrough_; + +}; + +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_; + +}; + +class NodeGroupSetOutputPassthrough : public UndoCommand +{ +public: + NodeGroupSetOutputPassthrough(NodeGroup *group, Node *output) : + group_(group), + new_output_(output) + {} + + virtual Project * GetRelevantProject() const override + { + return group_->project(); + } + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + NodeGroup *group_; + + Node *new_output_; + Node *old_output_; + +}; + +} + +#endif // NODEGROUP_H diff --git a/app/node/keyframe.h b/app/node/keyframe.h index c9462cefe..a142c5015 100644 --- a/app/node/keyframe.h +++ b/app/node/keyframe.h @@ -43,6 +43,7 @@ public: * @brief Methods of interpolation to use with this keyframe */ enum Type { + kInvalid = -1, kLinear, kHold, kBezier diff --git a/app/node/math/merge/merge.cpp b/app/node/math/merge/merge.cpp index f5bd9ed0d..5b1582c84 100644 --- a/app/node/math/merge/merge.cpp +++ b/app/node/math/merge/merge.cpp @@ -32,6 +32,8 @@ MergeNode::MergeNode() AddInput(kBaseIn, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); AddInput(kBlendIn, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); + + SetFlags(kDontShowInParamView); } Node *MergeNode::copy() const diff --git a/app/node/node.cpp b/app/node/node.cpp index d79710962..90ee197f9 100644 --- a/app/node/node.cpp +++ b/app/node/node.cpp @@ -47,8 +47,10 @@ Node::Node() : override_color_(-1), folder_(nullptr), operation_stack_(0), - cache_result_(false) + cache_result_(false), + flags_(kNone) { + uuid_ = QUuid::createUuid(); } Node::~Node() @@ -88,6 +90,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 +173,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 +224,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 @@ -243,6 +257,40 @@ QIcon Node::icon() const return icon::New; } +bool Node::SetNodePositionInContext(Node *node, const QPointF &pos) +{ + Position p = context_positions_.value(node); + + p.position = pos; + + return SetNodePositionInContext(node, p); +} + +bool Node::SetNodePositionInContext(Node *node, const Position &pos) +{ + bool added = !ContextContainsNode(node); + context_positions_.insert(node, pos); + + if (added) { + emit NodeAddedToContext(node); + } + + emit NodePositionInContextChanged(node, pos.position); + + return added; +} + +bool Node::RemoveNodeFromContext(Node *node) +{ + if (ContextContainsNode(node)) { + context_positions_.remove(node); + emit NodeRemovedFromContext(node); + return true; + } else { + return false; + } +} + Color Node::color() const { int c; @@ -429,6 +477,11 @@ void Node::SaveInput(QXmlStreamWriter *writer, const QString &id) const writer->writeEndElement(); // subelements } +bool Node::IsInputHidden(const QString &input) const +{ + return (GetInputFlags(input) & kInputFlagHidden); +} + bool Node::IsInputConnectable(const QString &input) const { return !(GetInputFlags(input) & kInputFlagNotConnectable); @@ -1050,7 +1103,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); @@ -1196,13 +1249,10 @@ Node *Node::CopyNodeAndDependencyGraphMinusItemsInternal(QMap& cre command->add_child(new NodeSetValueHintCommand(copied_input, node->GetValueHintForInput(input.input(), input.element()))); } - if (node->parent()->GetPositionMap().contains(node)) { - // This node is a context, copy the context - const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - // Add either the copy (if it exists) or the original node to the context - command->add_child(new NodeSetPositionCommand(created.value(it.key(), it.key()), copy, it.value(), false)); - } + const PositionMap &map = node->GetContextPositions(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Add either the copy (if it exists) or the original node to the context + command->add_child(new NodeSetPositionCommand(created.value(it.key(), it.key()), copy, it.value())); } return copy; @@ -1229,13 +1279,10 @@ Node *Node::CopyNodeInGraph(Node *node, MultiUndoCommand *command) command->add_child(new NodeCopyInputsCommand(node, copy, true)); - if (node->parent()->GetPositionMap().contains(node)) { - // This node is a context, copy the context - const NodeGraph::PositionMap &map = node->parent()->GetPositionMap().value(node); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - // Add to the context - command->add_child(new NodeSetPositionCommand(it.key(), copy, it.value(), false)); - } + const PositionMap &map = node->GetContextPositions(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + // Add to the context + command->add_child(new NodeSetPositionCommand(it.key(), copy, it.value())); } } @@ -1310,7 +1357,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(); @@ -1457,7 +1504,7 @@ void Node::GizmoMove(const QPointF &, const rational&, const Qt::KeyboardModifie { } -void Node::GizmoRelease() +void Node::GizmoRelease(MultiUndoCommand *) { } @@ -1564,7 +1611,9 @@ void Node::CopyValuesOfElement(const Node *src, Node *dst, const QString &input, dst->SetSplitStandardValue(input, src->GetSplitStandardValue(input, src_element), dst_element); // Copy keyframes - dst->GetImmediate(input, dst_element)->delete_all_keyframes(); + if (NodeInputImmediate *immediate = dst->GetImmediate(input, dst_element)) { + immediate->delete_all_keyframes(); + } foreach (const NodeKeyframeTrack& track, src->GetImmediate(input, src_element)->keyframe_tracks()) { foreach (NodeKeyframe* key, track) { key->copy(dst_element, dst); @@ -2198,7 +2247,6 @@ void Node::childEvent(QChildEvent *event) GetImmediate(key->input(), key->element())->insert_keyframe(key); connect(key, &NodeKeyframe::TimeChanged, this, &Node::InvalidateFromKeyframeTimeChange); - connect(key, &NodeKeyframe::TimeChanged, this, &Node::KeyframeTimeChanged); connect(key, &NodeKeyframe::ValueChanged, this, &Node::InvalidateFromKeyframeValueChange); connect(key, &NodeKeyframe::TypeChanged, this, &Node::InvalidateFromKeyframeTypeChanged); connect(key, &NodeKeyframe::BezierControlInChanged, this, &Node::InvalidateFromKeyframeBezierInChange); @@ -2210,15 +2258,14 @@ void Node::childEvent(QChildEvent *event) TimeRange time_affected = GetRangeAffectedByKeyframe(key); disconnect(key, &NodeKeyframe::TimeChanged, this, &Node::InvalidateFromKeyframeTimeChange); - disconnect(key, &NodeKeyframe::TimeChanged, this, &Node::KeyframeTimeChanged); disconnect(key, &NodeKeyframe::ValueChanged, this, &Node::InvalidateFromKeyframeValueChange); disconnect(key, &NodeKeyframe::TypeChanged, this, &Node::InvalidateFromKeyframeTypeChanged); disconnect(key, &NodeKeyframe::BezierControlInChanged, this, &Node::InvalidateFromKeyframeBezierInChange); disconnect(key, &NodeKeyframe::BezierControlOutChanged, this, &Node::InvalidateFromKeyframeBezierOutChange); - GetImmediate(key->input(), key->element())->remove_keyframe(key); - emit KeyframeRemoved(key); + + GetImmediate(key->input(), key->element())->remove_keyframe(key); ParameterValueChanged(i, time_affected); } } @@ -2281,12 +2328,16 @@ void Node::InvalidateFromKeyframeTimeChange() foreach (const TimeRange& r, invalidate_range) { ParameterValueChanged(key->key_track_ref().input(), r); } + + emit KeyframeTimeChanged(key); } void Node::InvalidateFromKeyframeValueChange() { NodeKeyframe* key = static_cast(sender()); ParameterValueChanged(key->key_track_ref().input(), GetRangeAffectedByKeyframe(key)); + + emit KeyframeValueChanged(key); } void Node::InvalidateFromKeyframeTypeChanged() @@ -2301,6 +2352,8 @@ void Node::InvalidateFromKeyframeTypeChanged() // Invalidate entire range ParameterValueChanged(key->key_track_ref().input(), GetRangeAroundIndex(key->input(), track.indexOf(key), key->track(), key->element())); + + emit KeyframeTypeChanged(key); } Project *Node::ArrayInsertCommand::GetRelevantProject() const @@ -2318,119 +2371,40 @@ Project *Node::ArrayResizeCommand::GetRelevantProject() const return node_->project(); } -void NodeSetPositionAndShiftSurroundingsCommand::redo() -{ - if (commands_.isEmpty()) { - // Move first node - NodeSetPositionCommand* set_pos_command = new NodeSetPositionCommand(node_, relative_, position_, move_dependencies_); - set_pos_command->redo_now(); - commands_.append(set_pos_command); - - // Get bounding rect - qreal bounding_rect_sz = 1.0; - qreal bounding_rect_half_sz = bounding_rect_sz * 0.5; - QRectF bounding_rect(position_.x() - bounding_rect_half_sz, position_.y() - bounding_rect_half_sz, bounding_rect_sz, bounding_rect_sz); - - // Start moving other nodes - foreach (Node* surrounding, node_->parent()->nodes()) { - if (surrounding != node_) { - QPointF surrounding_position = node_->parent()->GetNodePosition(surrounding, relative_); - if (bounding_rect.contains(surrounding_position)) { - QPointF new_pos = surrounding_position; - - qreal move_rate = 0.50; - - if (surrounding_position.y() < position_.y()) { - move_rate = -move_rate; - } - - new_pos.setY(new_pos.y() + move_rate); - - auto sur_command = new NodeSetPositionAndShiftSurroundingsCommand(surrounding, relative_, new_pos, true); - sur_command->redo(); - commands_.append(sur_command); - } - } - } - } else { - for (int i=0; iredo_now(); - } - } -} - void NodeSetPositionCommand::redo() { - graph_ = node_->parent(); - if (!(added_ = !graph_->NodeMapContainsNode(node_, relevant_))) { - old_pos_ = graph_->GetNodePosition(node_, relevant_); + added_ = !context_->ContextContainsNode(node_); + + if (!added_) { + old_pos_ = context_->GetNodePositionDataInContext(node_); } - graph_->SetNodePosition(node_, relevant_, pos_); + + context_->SetNodePositionInContext(node_, pos_); } void NodeSetPositionCommand::undo() { if (added_) { - graph_->RemoveNodePosition(node_, relevant_); + context_->RemoveNodeFromContext(node_); } else { - graph_->SetNodePosition(node_, relevant_, old_pos_); + context_->SetNodePositionInContext(node_, old_pos_); } } -void NodeSetPositionAsChildCommand::redo() -{ - if (!sub_command_) { - // Calculate position of node - NodeGraph *graph = parent_->parent(); - QPointF pos = graph->GetNodePosition(parent_, relative_); - - // This is a dependency, so we'll place it one X before - pos.setX(pos.x() - 1); - - // The Y will be calculated using the index and child count - pos.setY(pos.y() - (double(child_count_)*0.5) + this_index_ + 0.5); - - sub_command_ = new MultiUndoCommand(); - if (shift_surroundings_) { - sub_command_->add_child(new NodeSetPositionAndShiftSurroundingsCommand(node_, relative_, pos, true)); - } else { - sub_command_->add_child(new NodeSetPositionCommand(node_, relative_, pos, true)); - } - } - - sub_command_->redo_now(); -} - -void NodeSetPositionToOffsetOfAnotherNodeCommand::redo() -{ - NodeGraph *graph = node_->parent(); - old_pos_ = graph->GetNodePosition(node_, relative_); - graph->SetNodePosition(node_, relative_, graph->GetNodePosition(other_node_, relative_) + offset_); -} - -void NodeSetPositionToOffsetOfAnotherNodeCommand::undo() -{ - NodeGraph *graph = node_->parent(); - graph->SetNodePosition(node_, relative_, old_pos_); -} - void NodeRemovePositionFromContextCommand::redo() { - NodeGraph *graph = node_->parent(); - - contained_ = graph->ContextContainsNode(node_, context_); + contained_ = context_->ContextContainsNode(node_); if (contained_) { - old_pos_ = graph->GetNodePosition(node_, context_); - graph->RemoveNodePosition(node_, context_); + old_pos_ = context_->GetNodePositionDataInContext(node_); + context_->RemoveNodeFromContext(node_); } } void NodeRemovePositionFromContextCommand::undo() { if (contained_) { - NodeGraph *graph = node_->parent(); - graph->SetNodePosition(node_, context_, old_pos_); + context_->SetNodePositionInContext(node_, old_pos_); } } @@ -2438,28 +2412,21 @@ void NodeRemovePositionFromAllContextsCommand::redo() { NodeGraph *graph = node_->parent(); - if (points_.empty()) { - // No points yet, let's see what points we should remove - auto map = graph->GetPositionMap(); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - if (it.value().contains(node_)) { - points_.insert({it.key(), it.value().value(node_)}); - } + foreach (Node* context, graph->nodes()) { + if (context->ContextContainsNode(node_)) { + contexts_.insert({context, context->GetNodePositionInContext(node_)}); + context->RemoveNodeFromContext(node_); } } - - for (auto it=points_.cbegin(); it!=points_.cend(); it++) { - graph->RemoveNodePosition(node_, it->first); - } } void NodeRemovePositionFromAllContextsCommand::undo() { - NodeGraph *graph = node_->parent(); - - for (auto it=points_.crbegin(); it!=points_.crend(); it++) { - graph->SetNodePosition(node_, it->first, it->second); + for (auto it = contexts_.crbegin(); it != contexts_.crend(); it++) { + it->first->SetNodePositionInContext(node_, it->second); } + + contexts_.clear(); } void Node::ValueHint::Hash(QCryptographicHash &hash) const @@ -2508,4 +2475,37 @@ void Node::ValueHint::Save(QXmlStreamWriter *writer) const writer->writeTextElement(QStringLiteral("tag"), tag_); } +void NodeSetPositionAndDependenciesRecursivelyCommand::prepare() +{ + move_recursively(node_, pos_.position - context_->GetNodePositionDataInContext(node_).position); +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::redo() +{ + for (auto it=commands_.cbegin(); it!=commands_.cend(); it++) { + (*it)->redo_now(); + } +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::undo() +{ + for (auto it=commands_.crbegin(); it!=commands_.crend(); it++) { + (*it)->undo_now(); + } +} + +void NodeSetPositionAndDependenciesRecursivelyCommand::move_recursively(Node *node, const QPointF &diff) +{ + Node::Position pos = context_->GetNodePositionDataInContext(node); + pos += diff; + commands_.append(new NodeSetPositionCommand(node_, context_, pos)); + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + Node *output = it->second; + if (context_->ContextContainsNode(output)) { + move_recursively(output, diff); + } + } +} + } diff --git a/app/node/node.h b/app/node/node.h index b22af3fff..45516fbb8 100644 --- a/app/node/node.h +++ b/app/node/node.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "codec/frame.h" @@ -93,6 +94,11 @@ public: kCategoryCount }; + enum Flag { + kNone = 0, + kDontShowInParamView = 0x1 + }; + Node(); virtual ~Node() override; @@ -112,6 +118,14 @@ public: Project* project() const; + const QUuid &GetUUID() const {return uuid_;} + void SetUUID(const QUuid &uuid) {uuid_ = uuid;} + + const uint64_t &GetFlags() const + { + return flags_; + } + /** * @brief Clear current node variables and replace them with */ @@ -208,6 +222,79 @@ public: return HasInputWithID(id); } + struct Position + { + Position(const QPointF &p = QPointF(0, 0), bool e = false) + { + position = p; + expanded = e; + } + + QPointF position; + bool expanded; + + inline Position &operator+=(const Position &p) + { + position += p.position; + return *this; + } + + inline Position &operator-=(const Position &p) + { + position -= p.position; + return *this; + } + + friend inline const Position operator+(Position a, const Position &b) + { + a += b; + return a; + } + + friend inline const Position operator-(Position a, const Position &b) + { + a -= b; + return a; + } + }; + + using PositionMap = QHash; + const PositionMap &GetContextPositions() const + { + return context_positions_; + } + + bool IsNodeExpandedInContext(Node *node) const + { + return context_positions_.value(node).expanded; + } + + bool ContextContainsNode(Node *node) const + { + return context_positions_.contains(node); + } + + Position GetNodePositionDataInContext(Node *node) + { + return context_positions_.value(node); + } + + QPointF GetNodePositionInContext(Node *node) + { + return GetNodePositionDataInContext(node).position; + } + + bool SetNodePositionInContext(Node *node, const QPointF &pos); + + bool SetNodePositionInContext(Node *node, const Position &pos); + + void SetNodeExpandedInContext(Node *node, bool e) + { + context_positions_[node].expanded = e; + } + + bool RemoveNodeFromContext(Node *node); + /** * @brief Retrieve the color of this node */ @@ -243,11 +330,12 @@ public: static void DisconnectEdge(Node *output, const NodeInput& input); - QString GetInputName(const QString& id) const; + virtual QString GetInputName(const QString& id) const; void LoadInput(QXmlStreamReader* reader, XMLNodeData &xml_node_data, const QAtomicInt *cancelled); void SaveInput(QXmlStreamWriter* writer, const QString& id) const; + bool IsInputHidden(const QString& input) const; bool IsInputConnectable(const QString& input) const; bool IsInputKeyframable(const QString& input) const; @@ -767,7 +855,7 @@ public: virtual bool GizmoPress(const NodeValueRow& row, const NodeGlobals &globals, const QPointF& p); virtual void GizmoMove(const QPointF& p, const rational &time, const Qt::KeyboardModifiers &modifiers); - virtual void GizmoRelease(); + virtual void GizmoRelease(MultiUndoCommand *command); const QString& GetLabel() const; void SetLabel(const QString& s); @@ -861,37 +949,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 - }; - - 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; @@ -986,12 +1046,12 @@ protected: tooltip_ = s; } -signals: - /** - * @brief Signal emitted whenever the position is set through SetPosition() - */ - void PositionChanged(const QPointF& pos); + void SetFlags(const uint64_t &f) + { + flags_ = f; + } +signals: /** * @brief Signal emitted when SetLabel() is called */ @@ -1021,7 +1081,11 @@ signals: void KeyframeRemoved(NodeKeyframe* key); - void KeyframeTimeChanged(); + void KeyframeTimeChanged(NodeKeyframe* key); + + void KeyframeTypeChanged(NodeKeyframe* key); + + void KeyframeValueChanged(NodeKeyframe* key); void KeyframeEnableChanged(const NodeInput& input, bool enabled); @@ -1037,6 +1101,12 @@ signals: void RemovedFromGraph(NodeGraph* graph); + void NodeAddedToContext(Node *node); + + void NodePositionInContextChanged(Node *node, const QPointF &pos); + + void NodeRemovedFromContext(Node *node); + private: class ArrayInsertCommand : public UndoCommand { @@ -1140,8 +1210,6 @@ private: return input_ids_.indexOf(input); } - InputFlags GetInputFlags(const QString& input) const; - Input* GetInternalInputData(const QString& input) { int i = GetInternalInputIndex(input); @@ -1258,6 +1326,12 @@ private: QMap value_hints_; + PositionMap context_positions_; + + QUuid uuid_; + + uint64_t flags_; + private slots: /** * @brief Slot when a keyframe's time changes to keep the keyframes correctly sorted by time @@ -1367,12 +1441,11 @@ using NodePtr = std::shared_ptr; class NodeSetPositionCommand : public UndoCommand { public: - NodeSetPositionCommand(Node* node, Node* relevant, const QPointF& pos, bool move_dependencies_relatively) + NodeSetPositionCommand(Node* node, Node* context, const Node::Position& pos) { node_ = node; - relevant_ = relevant; + context_ = context; pos_ = pos; - move_deps_ = move_dependencies_relatively; } virtual Project* GetRelevantProject() const override @@ -1387,153 +1460,43 @@ protected: private: Node* node_; - Node* relevant_; - QPointF pos_; - QPointF old_pos_; + Node* context_; + Node::Position pos_; + Node::Position old_pos_; bool added_; - bool move_deps_; - NodeGraph *graph_; }; -class NodeSetPositionAndShiftSurroundingsCommand : public UndoCommand -{ +class NodeSetPositionAndDependenciesRecursivelyCommand : public UndoCommand{ public: - NodeSetPositionAndShiftSurroundingsCommand(Node* node, Node *relative, const QPointF& pos, bool move_dependencies_relatively) : + NodeSetPositionAndDependenciesRecursivelyCommand(Node* node, Node* context, const Node::Position& pos) : node_(node), - relative_(relative), - position_(pos), - move_dependencies_(move_dependencies_relatively) + context_(context), + pos_(pos) {} - virtual ~NodeSetPositionAndShiftSurroundingsCommand() override - { - qDeleteAll(commands_); - } - - virtual Project * GetRelevantProject() const override + virtual Project* GetRelevantProject() const override { return node_->project(); } protected: + virtual void prepare() override; + virtual void redo() override; - virtual void undo() override - { - for (int i=commands_.size()-1; i>=0; i--) { - commands_.at(i)->undo_now(); - } - } + virtual void undo() override; private: + void move_recursively(Node *node, const QPointF &diff); + Node* node_; - - Node *relative_; - - QPointF position_; - - bool move_dependencies_; - + Node* context_; + Node::Position pos_; QVector commands_; }; -class NodeSetPositionAsChildCommand : public UndoCommand -{ -public: - NodeSetPositionAsChildCommand(Node* node, Node* parent, Node *relative, double this_index, int child_count, bool shift_surroundings) : - node_(node), - parent_(parent), - relative_(relative), - this_index_(this_index), - child_count_(child_count), - shift_surroundings_(shift_surroundings), - sub_command_(nullptr) - { - } - - virtual ~NodeSetPositionAsChildCommand() override - { - delete sub_command_; - } - - virtual Project * GetRelevantProject() const override - { - return node_->project(); - } - -protected: - virtual void redo() override; - - virtual void undo() override - { - sub_command_->undo_now(); - } - -private: - Node* node_; - Node* parent_; - Node *relative_; - - double this_index_; - int child_count_; - - bool shift_surroundings_; - - MultiUndoCommand* sub_command_; - -}; - -class NodePositionCloseChildGapCommand : public UndoCommand -{ -public: - NodePositionCloseChildGapCommand(Node *parent, void *relative, int remove_index, int child_count, bool shift_surroundings); - - virtual Project * GetRelevantProject() const override - { - return parent_->project(); - } - -protected: - virtual void redo() override; - - virtual void undo() override; - -private: - Node *parent_; - -}; - -class NodeSetPositionToOffsetOfAnotherNodeCommand : public UndoCommand -{ -public: - NodeSetPositionToOffsetOfAnotherNodeCommand(Node* node, Node* other_node, Node *relative, const QPointF& offset) : - node_(node), - other_node_(other_node), - relative_(relative), - offset_(offset) - {} - - virtual Project * GetRelevantProject() const override - { - return node_->project(); - } - -protected: - virtual void redo() override; - - virtual void undo() override; - -private: - Node* node_; - Node* other_node_; - Node *relative_; - QPointF offset_; - QPointF old_pos_; - -}; - class NodeRemovePositionFromContextCommand : public UndoCommand { public: @@ -1558,7 +1521,7 @@ private: Node *context_; - QPointF old_pos_; + Node::Position old_pos_; bool contained_; @@ -1585,7 +1548,7 @@ protected: private: Node *node_; - std::map points_; + std::map contexts_; }; diff --git a/app/node/nodecopypaste.cpp b/app/node/nodecopypaste.cpp index 2d66d72ae..190146472 100644 --- a/app/node/nodecopypaste.cpp +++ b/app/node/nodecopypaste.cpp @@ -29,7 +29,7 @@ namespace olive { -void NodeCopyPasteService::CopyNodesToClipboard(const QVector &nodes, void *userdata) +void NodeCopyPasteService::CopyNodesToClipboard(QVector nodes, void *userdata) { QString copy_str; @@ -42,22 +42,33 @@ void NodeCopyPasteService::CopyNodesToClipboard(const QVector &nodes, vo writer.writeTextElement(QStringLiteral("version"), QString::number(Core::kProjectVersion)); writer.writeStartElement(QStringLiteral("nodes")); - foreach (Node* n, nodes) { + for (int i=0; iid()); n->Save(&writer); writer.writeEndElement(); // node + + // If this is a group, add the child nodes too + if (NodeGroup *g = dynamic_cast(n)) { + for (auto it=g->GetContextPositions().cbegin(); it!=g->GetContextPositions().cend(); it++) { + if (!nodes.contains(it.key())) { + nodes.append(it.key()); + } + } + } } writer.writeEndElement(); // nodes writer.writeStartElement(QStringLiteral("contexts")); foreach (Node* n, nodes) { // Determine if this node is a context - if (n->parent()->GetPositionMap().contains(n)) { + if (!n->GetContextPositions().isEmpty()) { writer.writeStartElement(QStringLiteral("context")); writer.writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(n))); - const NodeGraph::PositionMap &map = n->parent()->GetNodesForContext(n); + const Node::PositionMap &map = n->GetContextPositions(); for (auto it=map.cbegin(); it!=map.cend(); it++) { writer.writeStartElement(QStringLiteral("node")); Project::SavePosition(&writer, it.key(), it.value()); @@ -92,7 +103,7 @@ QVector NodeCopyPasteService::PasteNodesFromClipboard(NodeGraph *graph, QVector pasted_nodes; XMLNodeData xml_node_data; - QMap > pasted_contexts; + QMap > pasted_contexts; while (XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("olive")) { @@ -131,7 +142,7 @@ QVector NodeCopyPasteService::PasteNodesFromClipboard(NodeGraph *graph, while (XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("context")) { // Get context ptr - QMap map; + QMap map; quintptr context_ptr = 0; XMLAttributeLoop((&reader), attr) { if (attr.name() == QStringLiteral("ptr")) { @@ -144,7 +155,7 @@ QVector NodeCopyPasteService::PasteNodesFromClipboard(NodeGraph *graph, while (XMLReadNextStartElement(&reader)) { if (reader.name() == QStringLiteral("node")) { quintptr node_ptr; - QPointF node_pos; + Node::Position node_pos; if (Project::LoadPosition(&reader, &node_ptr, &node_pos)) { map.insert(node_ptr, node_pos); @@ -201,31 +212,34 @@ QVector NodeCopyPasteService::PasteNodesFromClipboard(NodeGraph *graph, // Add all nodes to graph foreach (Node* n, pasted_nodes) { - command->add_child(new NodeAddCommand(graph, n)); + if (command) { + command->add_child(new NodeAddCommand(graph, n)); + } else { + n->setParent(graph); + } } - // Make connections - if (!xml_node_data.desired_connections.isEmpty()) { - XMLConnectNodes(xml_node_data, data_version, command); - } - - // Link blocks - XMLLinkBlocks(xml_node_data); - // Process contexts for (auto it=pasted_contexts.cbegin(); it!=pasted_contexts.cend(); it++) { Node *context = xml_node_data.node_ptrs.value(it.key()); if (context) { - const QMap &map = it.value(); + auto map = it.value(); for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { Node *subnode = xml_node_data.node_ptrs.value(jt.key()); if (subnode) { - command->add_child(new NodeSetPositionCommand(subnode, context, jt.value(), false)); + if (command) { + command->add_child(new NodeSetPositionCommand(subnode, context, jt.value())); + } else { + context->SetNodePositionInContext(subnode, jt.value()); + } } } } } + // Make connections + xml_node_data.PostConnect(data_version, command); + return pasted_nodes; } diff --git a/app/node/nodecopypaste.h b/app/node/nodecopypaste.h index cc29d281a..41a25302e 100644 --- a/app/node/nodecopypaste.h +++ b/app/node/nodecopypaste.h @@ -35,7 +35,7 @@ public: NodeCopyPasteService() = default; protected: - void CopyNodesToClipboard(const QVector &nodes, void* userdata = nullptr); + void CopyNodesToClipboard(QVector nodes, void* userdata = nullptr); QVector PasteNodesFromClipboard(NodeGraph *graph, MultiUndoCommand *command, void* userdata = nullptr); diff --git a/app/node/output/track/track.cpp b/app/node/output/track/track.cpp index 41eda30f2..9506f4513 100644 --- a/app/node/output/track/track.cpp +++ b/app/node/output/track/track.cpp @@ -41,9 +41,10 @@ const QString Track::kMutedInput = QStringLiteral("muted_in"); Track::Track() : track_type_(Track::kNone), index_(-1), - locked_(false) + locked_(false), + sequence_(nullptr) { - AddInput(kBlockInput, NodeValue::kNone, InputFlags(kInputFlagArray | kInputFlagNotKeyframable)); + AddInput(kBlockInput, NodeValue::kNone, InputFlags(kInputFlagArray | kInputFlagNotKeyframable | kInputFlagHidden)); // Since blocks are time based, we can handle the invalidate timing a little more intelligently // on our end diff --git a/app/node/output/track/track.h b/app/node/output/track/track.h index 97bb1e7b6..a7953ff09 100644 --- a/app/node/output/track/track.h +++ b/app/node/output/track/track.h @@ -26,6 +26,8 @@ namespace olive { +class Sequence; + /** * @brief A time traversal Node for sorting through one channel/track of Blocks */ @@ -168,17 +170,48 @@ public: QString ToString() const { - QString type_string; - - if (type_ == Track::kVideo) { - type_string = QStringLiteral("v"); - } else if (type_ == Track::kAudio) { - type_string = QStringLiteral("a"); - } else { + QString type_string = TypeToString(type_); + if (type_string.isEmpty()) { return QString(); + } else { + return QStringLiteral("%1:%2").arg(type_string, QString::number(index_)); + } + } + + /// For IDs that shouldn't change between localizations + static QString TypeToString(Type type) + { + switch (type) { + case kVideo: + return QStringLiteral("v"); + case kAudio: + return QStringLiteral("a"); + case kSubtitle: + return QStringLiteral("s"); + case kCount: + case kNone: + break; } - return QStringLiteral("%1:%2").arg(type_string, QString::number(index_)); + return QString(); + } + + /// For human-facing strings + static QString TypeToTranslatedString(Type type) + { + switch (type) { + case kVideo: + return tr("V"); + case kAudio: + return tr("A"); + case kSubtitle: + return tr("S"); + case kCount: + case kNone: + break; + } + + return QString(); } static Type TypeFromString(const QString& s) @@ -359,9 +392,18 @@ public: bool IsLocked() const; - int GetArrayIndexFromBlock(Block* block) const; + Sequence *sequence() const + { + return sequence_; + } + + void set_sequence(Sequence *sequence) + { + sequence_ = sequence; + } + static const double kTrackHeightDefault; static const double kTrackHeightMinimum; static const double kTrackHeightInterval; @@ -443,6 +485,8 @@ private: bool locked_; + Sequence *sequence_; + private slots: void BlockLengthChanged(); diff --git a/app/node/output/track/tracklist.cpp b/app/node/output/track/tracklist.cpp index c9f35e26f..f5d233515 100644 --- a/app/node/output/track/tracklist.cpp +++ b/app/node/output/track/tracklist.cpp @@ -83,6 +83,7 @@ void TrackList::TrackConnected(Node *node, int element) connect(track, &Track::TrackLengthChanged, this, &TrackList::UpdateTotalLength); track->set_type(type_); + track->set_sequence(parent()); emit TrackListChanged(); @@ -121,6 +122,7 @@ void TrackList::TrackDisconnected(Node *node, int element) track->SetIndex(-1); track->set_type(Track::kNone); + track->set_sequence(nullptr); disconnect(track, &Track::TrackLengthChanged, this, &TrackList::UpdateTotalLength); diff --git a/app/node/output/viewer/viewer.cpp b/app/node/output/viewer/viewer.cpp index 751cdbb62..82e5acdc6 100644 --- a/app/node/output/viewer/viewer.cpp +++ b/app/node/output/viewer/viewer.cpp @@ -23,7 +23,6 @@ #include "config/config.h" #include "core.h" #include "node/traverser.h" -#include "widget/videoparamedit/videoparamedit.h" namespace olive { @@ -34,8 +33,6 @@ const QString ViewerOutput::kSamplesInput = QStringLiteral("samples_in"); const QString ViewerOutput::kVideoAutoCacheInput = QStringLiteral("video_autocache_in"); const QString ViewerOutput::kAudioAutoCacheInput = QStringLiteral("audio_autocache_in"); -const uint64_t ViewerOutput::kVideoParamEditMask = VideoParamEdit::kWidthHeight | VideoParamEdit::kInterlacing | VideoParamEdit::kFrameRate | VideoParamEdit::kPixelAspect; - #define super Node ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_streams) : @@ -47,10 +44,9 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream video_cache_enabled_(true), audio_cache_enabled_(true) { - AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray)); - SetInputProperty(kVideoParamsInput, QStringLiteral("mask"), QVariant::fromValue(kVideoParamEditMask)); + AddInput(kVideoParamsInput, NodeValue::kVideoParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden)); - AddInput(kAudioParamsInput, NodeValue::kAudioParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray)); + AddInput(kAudioParamsInput, NodeValue::kAudioParams, InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden)); if (create_buffer_inputs) { AddInput(kTextureInput, NodeValue::kTexture, InputFlags(kInputFlagNotKeyframable)); @@ -70,6 +66,8 @@ ViewerOutput::ViewerOutput(bool create_buffer_inputs, bool create_default_stream AddStream(Track::kAudio, QVariant()); set_default_parameters(); } + + SetFlags(kDontShowInParamView); } Node *ViewerOutput::copy() const diff --git a/app/node/output/viewer/viewer.h b/app/node/output/viewer/viewer.h index 56a89908c..2be79bf2f 100644 --- a/app/node/output/viewer/viewer.h +++ b/app/node/output/viewer/viewer.h @@ -200,8 +200,6 @@ public: static const QString kVideoAutoCacheInput; static const QString kAudioAutoCacheInput; - static const uint64_t kVideoParamEditMask; - signals: void FrameRateChanged(const rational&); @@ -254,8 +252,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 9f6bd8c4d..d7676c615 100644 --- a/app/node/param.cpp +++ b/app/node/param.cpp @@ -33,6 +33,15 @@ QString NodeInput::name() const } } +bool NodeInput::IsHidden() const +{ + if (IsValid()) { + return node_->IsInputHidden(input_); + } else { + return false; + } +} + bool NodeInput::IsConnected() const { if (IsValid()) { @@ -60,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()) { @@ -78,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()) { @@ -123,6 +150,15 @@ QVariant NodeInput::GetSplitDefaultValueForTrack(int track) const } } +int NodeInput::GetArraySize() const +{ + if (IsValid() && element_ == -1) { + return node_->InputArraySize(input_); + } else { + return 0; + } +} + uint qHash(const NodeInput &i) { return qHash(i.node()) ^ qHash(i.input()) ^ qHash(i.element()); diff --git a/app/node/param.h b/app/node/param.h index 63aa413f7..77f8e63f6 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,11 +129,21 @@ public: return input_; } - int element() const + const int &element() const { return element_; } + void set_node(Node *node) + { + node_ = node; + } + + void set_input(const QString &input) + { + input_ = input; + } + void set_element(int e) { element_ = e; @@ -115,16 +156,22 @@ public: return node_ && !input_.isEmpty() && element_ >= -1; } + bool IsHidden() const; + bool IsConnected() const; bool IsKeyframing() const; 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; @@ -135,6 +182,8 @@ public: QVariant GetSplitDefaultValueForTrack(int track) const; + int GetArraySize() const; + void Reset() { *this = NodeInput(); diff --git a/app/node/project/folder/folder.cpp b/app/node/project/folder/folder.cpp index feae6d612..7b017923f 100644 --- a/app/node/project/folder/folder.cpp +++ b/app/node/project/folder/folder.cpp @@ -113,19 +113,12 @@ void Folder::InputDisconnectedEvent(const QString &input, int element, Node *out } } -FolderAddChild::FolderAddChild(Folder *folder, Node *child, bool autoposition) : +FolderAddChild::FolderAddChild(Folder *folder, Node *child) : folder_(folder), - child_(child), - autoposition_(autoposition), - position_command_(nullptr) + child_(child) { } -FolderAddChild::~FolderAddChild() -{ - delete position_command_; -} - Project *FolderAddChild::GetRelevantProject() const { return folder_->project(); @@ -136,21 +129,10 @@ void FolderAddChild::redo() int array_index = folder_->InputArraySize(Folder::kChildInput); folder_->InputArrayAppend(Folder::kChildInput, false); Node::ConnectEdge(child_, NodeInput(folder_, Folder::kChildInput, array_index)); - - if (autoposition_) { - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(child_, folder_, folder_->project()->root(), array_index, array_index+1, true); - } - position_command_->redo_now(); - } } void FolderAddChild::undo() { - if (position_command_) { - position_command_->undo_now(); - } - Node::DisconnectEdge(child_, NodeInput(folder_, Folder::kChildInput, folder_->InputArraySize(Folder::kChildInput)-1)); folder_->InputArrayRemoveLast(Folder::kChildInput); } diff --git a/app/node/project/folder/folder.h b/app/node/project/folder/folder.h index 1303ce7d7..12677e57b 100644 --- a/app/node/project/folder/folder.h +++ b/app/node/project/folder/folder.h @@ -208,9 +208,7 @@ private: class FolderAddChild : public UndoCommand { public: - FolderAddChild(Folder* folder, Node* child, bool autoposition = true); - - virtual ~FolderAddChild() override; + FolderAddChild(Folder* folder, Node* child); virtual Project * GetRelevantProject() const override; @@ -224,10 +222,6 @@ private: Node* child_; - bool autoposition_; - - NodeSetPositionAsChildCommand* position_command_; - }; } diff --git a/app/node/project/footage/footage.cpp b/app/node/project/footage/footage.cpp index 4ab67ebb8..a9aa54705 100644 --- a/app/node/project/footage/footage.cpp +++ b/app/node/project/footage/footage.cpp @@ -33,7 +33,6 @@ #include "core.h" #include "render/job/footagejob.h" #include "ui/icons/icons.h" -#include "widget/videoparamedit/videoparamedit.h" namespace olive { @@ -144,39 +143,6 @@ void Footage::InputValueChangedEvent(const QString &input, int element) AddStream(Track::kVideo, QVariant::fromValue(vp)); } - if (!footage_info.GetVideoStreams().isEmpty()) { - // FIXME: This will break on multiple video streams. Currently we don't have - // infrastructure for different properties per element. We'll see if this becomes - // a problem. - VideoParams vp = footage_info.GetVideoStreams().first(); - - uint64_t video_param_mask = 0; - - video_param_mask |= VideoParamEdit::kEnabled; - video_param_mask |= VideoParamEdit::kColorspace; - video_param_mask |= VideoParamEdit::kPixelAspect; - video_param_mask |= VideoParamEdit::kInterlacing; - video_param_mask |= VideoParamEdit::kFrameRateIsArbitrary; - - if (vp.channel_count() == VideoParams::kRGBAChannelCount) { - // Add premultiplied setting if this footage has an alpha channel - video_param_mask |= VideoParamEdit::kPremultipliedAlpha; - } - - if (vp.video_type() == VideoParams::kVideoTypeVideo) { - // This is video, ensure that the frame rate does not overwrite the timebase - video_param_mask |= VideoParamEdit::kFrameRateIsNotTimebase; - } else { - // This is not a video, so it's either a still image or an image sequence - video_param_mask |= VideoParamEdit::kIsImageSequence; - video_param_mask |= VideoParamEdit::kStartTime; - video_param_mask |= VideoParamEdit::kEndTime; - video_param_mask |= VideoParamEdit::kFrameRate; - } - - SetInputProperty(kVideoParamsInput, QStringLiteral("mask"), QVariant::fromValue(video_param_mask)); - } - for (int i=0; isetParent(this); root_->SetLabel(tr("Root")); root_->SetCanBeDeleted(false); - SetNodePosition(root_, root_, QPointF(0, 0)); // Adds a color manager "node" to this project so that it synchronizes color_manager_ = new ColorManager(); color_manager_->setParent(this); - SetNodePosition(color_manager_, root_, QPointF(1, 0)); color_manager_->SetCanBeDeleted(false); AddDefaultNode(color_manager_); // Same with project settings settings_ = new ProjectSettingsNode(); settings_->setParent(this); - SetNodePosition(settings_, root_, QPointF(2, 0)); settings_->SetCanBeDeleted(false); AddDefaultNode(settings_); @@ -161,13 +158,13 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("node")) { quintptr node_ptr; - QPointF node_pos; + Node::Position node_pos; if (LoadPosition(reader, &node_ptr, &node_pos)) { Node *node = xml_node_data.node_ptrs.value(node_ptr); if (node) { - SetNodePosition(node, context, node_pos); + context->SetNodePositionInContext(node, node_pos); } else { qWarning() << "Failed to find pointer for node position"; reader->skipCurrentElement(); @@ -196,10 +193,7 @@ void Project::Load(QXmlStreamReader *reader, MainWindowLayoutInfo* layout, uint } // Make connections - XMLConnectNodes(xml_node_data, version); - - // Link blocks - XMLLinkBlocks(xml_node_data); + xml_node_data.PostConnect(version); } void Project::Save(QXmlStreamWriter *writer) const @@ -230,20 +224,22 @@ void Project::Save(QXmlStreamWriter *writer) const writer->writeStartElement(QStringLiteral("positions")); - for (auto it=GetPositionMap().cbegin(); it!=GetPositionMap().cend(); it++) { - writer->writeStartElement(QStringLiteral("context")); + foreach (Node* context, nodes()) { + const Node::PositionMap &map = context->GetContextPositions(); - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(it.key()))); + if (!map.isEmpty()) { + writer->writeStartElement(QStringLiteral("context")); - const PositionMap &map = it.value(); + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(context))); - for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { - writer->writeStartElement(QStringLiteral("node")); - SavePosition(writer, jt.key(), jt.value()); - writer->writeEndElement(); // node + for (auto jt=map.cbegin(); jt!=map.cend(); jt++) { + writer->writeStartElement(QStringLiteral("node")); + SavePosition(writer, jt.key(), jt.value()); + writer->writeEndElement(); // node + } + + writer->writeEndElement(); // context } - - writer->writeEndElement(); // context } writer->writeEndElement(); // positions @@ -351,7 +347,7 @@ void Project::RegenerateUuid() uuid_ = QUuid::createUuid(); } -bool Project::LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, QPointF *pos) +bool Project::LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, Node::Position *pos) { bool got_node_ptr = false; bool got_pos_x = false; @@ -367,11 +363,13 @@ bool Project::LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, QPointF while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("x")) { - pos->setX(reader->readElementText().toDouble()); + pos->position.setX(reader->readElementText().toDouble()); got_pos_x = true; } else if (reader->name() == QStringLiteral("y")) { - pos->setY(reader->readElementText().toDouble()); + pos->position.setY(reader->readElementText().toDouble()); got_pos_y = true; + } else if (reader->name() == QStringLiteral("expanded")) { + pos->expanded = reader->readElementText().toInt(); } else { reader->skipCurrentElement(); } @@ -380,12 +378,13 @@ bool Project::LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, QPointF return got_node_ptr && got_pos_x && got_pos_y; } -void Project::SavePosition(QXmlStreamWriter *writer, Node *node, const QPointF &pos) +void Project::SavePosition(QXmlStreamWriter *writer, Node *node, const Node::Position &pos) { writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(node))); - writer->writeTextElement(QStringLiteral("x"), QString::number(pos.x())); - writer->writeTextElement(QStringLiteral("y"), QString::number(pos.y())); + writer->writeTextElement(QStringLiteral("x"), QString::number(pos.position.x())); + writer->writeTextElement(QStringLiteral("y"), QString::number(pos.position.y())); + writer->writeTextElement(QStringLiteral("expanded"), QString::number(pos.expanded)); } void Project::ColorManagerValueChanged(const NodeInput &input, const TimeRange &range) diff --git a/app/node/project/project.h b/app/node/project/project.h index 3dbceb2bf..ef2e3e192 100644 --- a/app/node/project/project.h +++ b/app/node/project/project.h @@ -82,8 +82,8 @@ public: void RegenerateUuid(); - static bool LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, QPointF *pos); - static void SavePosition(QXmlStreamWriter *writer, Node *node, const QPointF &pos); + static bool LoadPosition(QXmlStreamReader *reader, quintptr *node_ptr, Node::Position *pos); + static void SavePosition(QXmlStreamWriter *writer, Node *node, const Node::Position &pos); signals: void NameChanged(); diff --git a/app/node/project/sequence/sequence.cpp b/app/node/project/sequence/sequence.cpp index 27b17708f..0cb443188 100644 --- a/app/node/project/sequence/sequence.cpp +++ b/app/node/project/sequence/sequence.cpp @@ -41,7 +41,7 @@ Sequence::Sequence() // Create track input QString track_input_id = kTrackInputFormat.arg(i); - AddInput(track_input_id, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray)); + AddInput(track_input_id, NodeValue::kNone, InputFlags(kInputFlagNotKeyframable | kInputFlagArray | kInputFlagHidden)); IgnoreInvalidationsFrom(track_input_id); diff --git a/app/panel/curve/curve.h b/app/panel/curve/curve.h index 6eeee3606..6ddc14401 100644 --- a/app/panel/curve/curve.h +++ b/app/panel/curve/curve.h @@ -39,6 +39,18 @@ public: virtual void DeselectAll() override; public slots: + void SetNode(Node *node) + { + // Convert single pointer to either an empty vector or a vector of one + QVector nodes; + + if (node) { + nodes.append(node); + } + + SetNodes(nodes); + } + void SetNodes(const QVector &nodes); virtual void IncreaseTrackHeight() override; diff --git a/app/panel/node/node.cpp b/app/panel/node/node.cpp index 591f7a867..d6abc3a1d 100644 --- a/app/panel/node/node.cpp +++ b/app/panel/node/node.cpp @@ -20,39 +20,21 @@ #include "node.h" -#include - namespace olive { NodePanel::NodePanel(QWidget *parent) : PanelWidget(QStringLiteral("NodePanel"), parent) { - QWidget *outer_widget = new QWidget(this); + node_widget_ = new NodeWidget(); + connect(this, &NodePanel::visibilityChanged, node_widget_->view(), &NodeView::CenterOnItemsBoundingRect); - QVBoxLayout *outer_layout = new QVBoxLayout(outer_widget); - outer_layout->setMargin(0); - - toolbar_ = new NodeViewToolBar(); - outer_layout->addWidget(toolbar_); - - // Create NodeView widget - node_view_ = new NodeView(this); - outer_layout->addWidget(node_view_); - - // Connect toolbar to NodeView - connect(toolbar_, &NodeViewToolBar::MiniMapEnabledToggled, node_view_, &NodeView::SetMiniMapEnabled); - connect(toolbar_, &NodeViewToolBar::AddNodeClicked, node_view_, &NodeView::ShowAddMenu); - - // Set defaults - toolbar_->SetMiniMapEnabled(true); - node_view_->SetMiniMapEnabled(true); - - // Connect node view signals to this panel - connect(node_view_, &NodeView::NodesSelected, this, &NodePanel::NodesSelected); - connect(node_view_, &NodeView::NodesDeselected, this, &NodePanel::NodesDeselected); + // Connect node view signals to this panel - MAY REMOVE + connect(node_widget_->view(), &NodeView::NodesSelected, this, &NodePanel::NodesSelected); + connect(node_widget_->view(), &NodeView::NodesDeselected, this, &NodePanel::NodesDeselected); + connect(node_widget_->view(), &NodeView::NodeGroupOpenRequested, this, &NodePanel::NodeGroupOpenRequested); // Set it as the main widget of this panel - SetWidgetWithPadding(outer_widget); + SetWidgetWithPadding(node_widget_); // Set strings Retranslate(); diff --git a/app/panel/node/node.h b/app/panel/node/node.h index d145bb91d..f1dae048f 100644 --- a/app/panel/node/node.h +++ b/app/panel/node/node.h @@ -21,8 +21,7 @@ #ifndef NODEPANEL_H #define NODEPANEL_H -#include "widget/nodeview/nodeview.h" -#include "widget/nodeview/nodeviewtoolbar.h" +#include "widget/nodeview/nodewidget.h" #include "widget/panel/panel.h" namespace olive { @@ -36,86 +35,86 @@ class NodePanel : public PanelWidget public: NodePanel(QWidget* parent); - NodeGraph* GetGraph() const + NodeWidget *GetNodeWidget() const { - return node_view_->GetGraph(); + return node_widget_; } - void SetGraph(NodeGraph *graph, const QVector &nodes) + const QVector &GetContexts() const { - node_view_->SetGraph(graph, nodes); - toolbar_->setEnabled(graph); + return node_widget_->view()->GetContexts(); } - void ClearGraph() + void SetContexts(const QVector &nodes) { - node_view_->ClearGraph(); + node_widget_->SetContexts(nodes); + } + + void CloseContextsBelongingToProject(Project *project) + { + node_widget_->view()->CloseContextsBelongingToProject(project); } const QVector &GetCurrentContexts() const { - return node_view_->GetCurrentContexts(); + return node_widget_->view()->GetCurrentContexts(); } virtual void SelectAll() override { - node_view_->SelectAll(); + node_widget_->view()->SelectAll(); } virtual void DeselectAll() override { - node_view_->DeselectAll(); + node_widget_->view()->DeselectAll(); } virtual void DeleteSelected() override { - node_view_->DeleteSelected(); + node_widget_->view()->DeleteSelected(); } virtual void CutSelected() override { - node_view_->CopySelected(true); + node_widget_->view()->CopySelected(true); } virtual void CopySelected() override { - node_view_->CopySelected(false); + node_widget_->view()->CopySelected(false); } virtual void Paste() override { - node_view_->Paste(); + node_widget_->view()->Paste(); } virtual void Duplicate() override { - node_view_->Duplicate(); + node_widget_->view()->Duplicate(); } virtual void SetColorLabel(int index) override { - node_view_->SetColorLabel(index); + node_widget_->view()->SetColorLabel(index); } virtual void ZoomIn() override { - node_view_->ZoomIn(); + node_widget_->view()->ZoomIn(); } virtual void ZoomOut() override { - node_view_->ZoomOut(); + node_widget_->view()->ZoomOut(); } public slots: void Select(const QVector& nodes, bool center_view_on_item) { - node_view_->Select(nodes, center_view_on_item); - } - - void SelectWithDependencies(const QVector& nodes, bool center_view_on_item) - { - node_view_->SelectWithDependencies(nodes, center_view_on_item); + node_widget_->view()->Select(nodes, center_view_on_item); + this->raise(); } signals: @@ -123,15 +122,15 @@ signals: void NodesDeselected(const QVector& nodes); + void NodeGroupOpenRequested(NodeGroup *group); + private: virtual void Retranslate() override { SetTitle(tr("Node Editor")); } - NodeView* node_view_; - - NodeViewToolBar *toolbar_; + NodeWidget *node_widget_; }; diff --git a/app/panel/panelmanager.cpp b/app/panel/panelmanager.cpp index 779c2be35..e3959bfbf 100644 --- a/app/panel/panelmanager.cpp +++ b/app/panel/panelmanager.cpp @@ -95,6 +95,40 @@ PanelManager *PanelManager::instance() return instance_; } +void PanelManager::RegisterPanel(PanelWidget *panel) +{ + // Add panel to the bottom of the focus history + focus_history_.append(panel); + + panel->SetMovementLocked(locked_); + + // Get panel parent (it's assumed it has one) + QWidget *parent = panel->parentWidget(); + + // Sane default for panel size + panel->resize(parent->size() / 3); + + // We're about to center the panel relative to the parent (usually the main window), but for some + // reason this requires the panel to be shown first. + panel->show(); + + // Center the panel relative to the parent + QPoint parent_center = panel->mapFromGlobal(parent->mapToGlobal(parent->rect().center())); + QPoint panel_center = panel->rect().center(); + panel->move(parent_center - panel_center); + + if (focus_history_.size() == 1) { + // This is the first panel, focus it + panel->SetBorderVisible(true); + emit FocusedPanelChanged(panel); + } +} + +void PanelManager::UnregisterPanel(PanelWidget *panel) +{ + focus_history_.removeOne(panel); +} + void PanelManager::FocusChanged(QWidget *old, QWidget *now) { Q_UNUSED(old) @@ -151,11 +185,4 @@ void PanelManager::SetPanelsLocked(bool locked) locked_ = locked; } -void PanelManager::PanelDestroyed() -{ - PanelWidget* panel = static_cast(sender()); - - focus_history_.removeOne(panel); -} - } diff --git a/app/panel/panelmanager.h b/app/panel/panelmanager.h index 50dfefce6..a71a312a3 100644 --- a/app/panel/panelmanager.h +++ b/app/panel/panelmanager.h @@ -84,12 +84,6 @@ public: */ T* MostRecentlyFocused(); - template - /** - * @brief Create a panel - */ - T* CreatePanel(QWidget* parent); - /** * @brief Get whether panels are currently prevented from moving */ @@ -118,6 +112,16 @@ public: */ QList GetPanelsOfType(); + /** + * @brief Panel should call this upon construction so it can be kept track of + */ + void RegisterPanel(PanelWidget *panel); + + /** + * @brief Panel should call this upon destruction so no invalid pointers will be kept for it + */ + void UnregisterPanel(PanelWidget *panel); + public slots: /** * @brief Connect this to a QApplication's SIGNAL(focusChanged()) @@ -153,48 +157,8 @@ private: */ static PanelManager* instance_; -private slots: - /** - * @brief Processing if a panel gets deleted - */ - void PanelDestroyed(); - }; -template -T *PanelManager::CreatePanel(QWidget *parent) -{ - T* panel = new T(parent); - - // Add panel to the bottom of the focus history - focus_history_.append(panel); - - panel->SetMovementLocked(locked_); - - // Sane default for panel size - panel->resize(parent->size() / 3); - - // We're about to center the panel relative to the parent (usually the main window), but for some - // reason this requires the panel to be shown first. - panel->show(); - - // Center the panel relative to the parent - QPoint parent_center = panel->mapFromGlobal(parent->mapToGlobal(parent->rect().center())); - QPoint panel_center = panel->rect().center(); - panel->move(parent_center - panel_center); - - // Connect destroy signal so we can remove it from focus history - connect(panel, &PanelWidget::destroyed, this, &PanelManager::PanelDestroyed, Qt::DirectConnection); - - if (focus_history_.size() == 1) { - // This is the first panel, focus it - panel->SetBorderVisible(true); - emit FocusedPanelChanged(panel); - } - - return panel; -} - template T* PanelManager::MostRecentlyFocused() { diff --git a/app/panel/param/param.cpp b/app/panel/param/param.cpp index c90875d00..90c2c1983 100644 --- a/app/panel/param/param.cpp +++ b/app/panel/param/param.cpp @@ -29,7 +29,6 @@ ParamPanel::ParamPanel(QWidget* parent) : { NodeParamView* view = new NodeParamView(); connect(view, &NodeParamView::RequestSelectNode, this, &ParamPanel::RequestSelectNode); - connect(view, &NodeParamView::NodeOrderChanged, this, &ParamPanel::NodeOrderChanged); connect(view, &NodeParamView::FocusedNodeChanged, this, &ParamPanel::FocusedNodeChanged); SetTimeBasedWidget(view); @@ -39,15 +38,11 @@ ParamPanel::ParamPanel(QWidget* parent) : void ParamPanel::SelectNodes(const QVector &nodes) { static_cast(GetTimeBasedWidget())->SelectNodes(nodes); - - Retranslate(); } void ParamPanel::DeselectNodes(const QVector &nodes) { static_cast(GetTimeBasedWidget())->DeselectNodes(nodes); - - Retranslate(); } void ParamPanel::DeleteSelected() @@ -65,19 +60,14 @@ void ParamPanel::DeselectAll() static_cast(GetTimeBasedWidget())->DeselectAll(); } +void ParamPanel::SetContexts(const QVector &contexts) +{ + static_cast(GetTimeBasedWidget())->SetContexts(contexts); +} + void ParamPanel::Retranslate() { SetTitle(tr("Parameter Editor")); - - NodeParamView* view = static_cast(GetTimeBasedWidget()); - - if (view->GetItemMap().isEmpty()) { - SetSubtitle(tr("(none)")); - } else if (view->GetItemMap().size() == 1) { - SetSubtitle(view->GetItemMap().firstKey()->Name()); - } else { - SetSubtitle(tr("(multiple)")); - } } } diff --git a/app/panel/param/param.h b/app/panel/param/param.h index a20c3310a..95ddc9b9a 100644 --- a/app/panel/param/param.h +++ b/app/panel/param/param.h @@ -33,6 +33,26 @@ class ParamPanel : public TimeBasedPanel public: ParamPanel(QWidget* parent); + NodeParamView *GetParamView() const + { + return static_cast(GetTimeBasedWidget()); + } + + const QVector &GetContexts() const + { + return GetParamView()->GetContexts(); + } + + void SetCreateCheckBoxes(NodeParamViewCheckBoxBehavior e) + { + GetParamView()->SetCreateCheckBoxes(e); + } + + void SetIgnoreNodeFlags(bool e) + { + GetParamView()->SetIgnoreNodeFlags(e); + } + public slots: void SelectNodes(const QVector& nodes); void DeselectNodes(const QVector& nodes); @@ -43,11 +63,11 @@ public slots: virtual void DeselectAll() override; + void SetContexts(const QVector &contexts); + signals: void RequestSelectNode(const QVector& target); - void NodeOrderChanged(const QVector& nodes); - void FocusedNodeChanged(Node* n); protected: diff --git a/app/panel/pixelsampler/pixelsamplerpanel.cpp b/app/panel/pixelsampler/pixelsamplerpanel.cpp index 2f17abb9e..dce9fd698 100644 --- a/app/panel/pixelsampler/pixelsamplerpanel.cpp +++ b/app/panel/pixelsampler/pixelsamplerpanel.cpp @@ -25,7 +25,7 @@ namespace olive { PixelSamplerPanel::PixelSamplerPanel(QWidget *parent) : - PanelWidget(QStringLiteral("ProjectPanel"), parent) + PanelWidget(QStringLiteral("PixelSamplerPanel"), parent) { sampler_widget_ = new ManagedPixelSamplerWidget(); SetWidgetWithPadding(sampler_widget_); diff --git a/app/panel/viewer/viewer.cpp b/app/panel/viewer/viewer.cpp index d23981aab..3d2bf8561 100644 --- a/app/panel/viewer/viewer.cpp +++ b/app/panel/viewer/viewer.cpp @@ -24,24 +24,6 @@ namespace olive { ViewerPanel::ViewerPanel(const QString &object_name, QWidget *parent) : ViewerPanelBase(object_name, parent) -{ - Init(); -} - -ViewerPanel::ViewerPanel(QWidget *parent) : - ViewerPanelBase(QStringLiteral("ViewerPanel"), parent) -{ - Init(); -} - -void ViewerPanel::Retranslate() -{ - ViewerPanelBase::Retranslate(); - - SetTitle(tr("Viewer")); -} - -void ViewerPanel::Init() { // Set ViewerWidget as the central widget ViewerWidget* vw = new ViewerWidget(); @@ -52,4 +34,11 @@ void ViewerPanel::Init() Retranslate(); } +void ViewerPanel::Retranslate() +{ + ViewerPanelBase::Retranslate(); + + SetTitle(tr("Viewer")); +} + } diff --git a/app/panel/viewer/viewer.h b/app/panel/viewer/viewer.h index a0f1d99bd..3b1cc4eb5 100644 --- a/app/panel/viewer/viewer.h +++ b/app/panel/viewer/viewer.h @@ -34,14 +34,14 @@ class ViewerPanel : public ViewerPanelBase { Q_OBJECT public: ViewerPanel(const QString& object_name, QWidget* parent); - ViewerPanel(QWidget* parent); + ViewerPanel(QWidget *parent) : + ViewerPanel(QStringLiteral("ViewerPanel"), parent) + { + } protected: virtual void Retranslate() override; -private: - void Init(); - }; } diff --git a/app/render/previewautocacher.cpp b/app/render/previewautocacher.cpp index 2c121745d..887c8b578 100644 --- a/app/render/previewautocacher.cpp +++ b/app/render/previewautocacher.cpp @@ -367,12 +367,20 @@ void PreviewAutoCacher::ProcessUpdateQueue() void PreviewAutoCacher::AddNode(Node *node) { + if (dynamic_cast(node)) { + // Group nodes are just dummy nodes, no need to copy them + return; + } + // Copy node Node* copy = node->copy(); // 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/task/project/loadotio/loadotio.cpp b/app/task/project/loadotio/loadotio.cpp index 96302d0ce..2e88390e2 100644 --- a/app/task/project/loadotio/loadotio.cpp +++ b/app/task/project/loadotio/loadotio.cpp @@ -210,7 +210,7 @@ bool LoadOTIOTask::Run() block->setParent(sequence->parent()); // Position transition in its own context - sequence->parent()->SetNodePosition(block, block, QPointF(0, 0)); + block->SetNodePositionInContext(block, QPointF(0, 0)); } if (otio_block->schema_name() == "Gap") { @@ -218,7 +218,7 @@ bool LoadOTIOTask::Run() block->setParent(sequence->parent()); // Position transition in its own context - sequence->parent()->SetNodePosition(block, block, QPointF(0, 0)); + block->SetNodePositionInContext(block, QPointF(0, 0)); } // Update this after it's used but before any continue statements @@ -246,7 +246,7 @@ bool LoadOTIOTask::Run() QFileInfo info(probed_item->filename()); probed_item->SetLabel(info.fileName()); - FolderAddChild add(sequence_footage, probed_item, true); + FolderAddChild add(sequence_footage, probed_item); add.redo_now(); } @@ -254,10 +254,10 @@ bool LoadOTIOTask::Run() block->setParent(sequence->parent()); // Position clip in its own context - sequence->parent()->SetNodePosition(block, block, QPointF(0, 0)); + block->SetNodePositionInContext(block, QPointF(0, 0)); // Position footage in its context - sequence->parent()->SetNodePosition(probed_item, block, QPointF(-2, 0)); + block->SetNodePositionInContext(probed_item, QPointF(-2, 0)); if (track->type() == Track::kVideo) { @@ -266,14 +266,14 @@ bool LoadOTIOTask::Run() Node::ConnectEdge(probed_item, NodeInput(transform, TransformDistortNode::kTextureInput)); Node::ConnectEdge(transform, NodeInput(block, ClipBlock::kBufferIn)); - sequence->parent()->SetNodePosition(transform, block, QPointF(-1, 0)); + block->SetNodePositionInContext(transform, QPointF(-1, 0)); } else { VolumeNode* volume_node = new VolumeNode(); volume_node->setParent(sequence->parent()); Node::ConnectEdge(probed_item, NodeInput(volume_node, VolumeNode::kSamplesInput)); Node::ConnectEdge(volume_node, NodeInput(block, ClipBlock::kBufferIn)); - sequence->parent()->SetNodePosition(volume_node, block, QPointF(-1, 0)); + block->SetNodePositionInContext(volume_node, QPointF(-1, 0)); } } } diff --git a/app/ui/icons/icons.cpp b/app/ui/icons/icons.cpp index 322237b65..20ecdaeb2 100644 --- a/app/ui/icons/icons.cpp +++ b/app/ui/icons/icons.cpp @@ -76,6 +76,7 @@ QIcon icon::Clock; QIcon icon::Diamond; QIcon icon::Plus; QIcon icon::Minus; +QIcon icon::AddEffect; void icon::LoadAll(const QString& theme) { @@ -129,6 +130,7 @@ void icon::LoadAll(const QString& theme) Diamond = Create(theme, "diamond"); Plus = Create(theme, "plus"); Minus = Create(theme, "minus"); + AddEffect = Create(theme, "add-effect"); } QIcon icon::Create(const QString& theme, const QString &name) diff --git a/app/ui/icons/icons.h b/app/ui/icons/icons.h index c8c1788a0..2bca32bb5 100644 --- a/app/ui/icons/icons.h +++ b/app/ui/icons/icons.h @@ -86,6 +86,7 @@ extern QIcon Clock; extern QIcon Diamond; extern QIcon Plus; extern QIcon Minus; +extern QIcon AddEffect; /** * @brief Create an icon object loaded from file diff --git a/app/undo/undocommand.cpp b/app/undo/undocommand.cpp index 075348c15..f182f86f4 100644 --- a/app/undo/undocommand.cpp +++ b/app/undo/undocommand.cpp @@ -24,34 +24,24 @@ namespace olive { -MultiUndoCommand::MultiUndoCommand() : - done_(false) -{ -} - void MultiUndoCommand::redo() { - if (!done_) { - for (auto it=children_.cbegin(); it!=children_.cend(); it++) { - (*it)->redo_and_set_modified(); - } - done_ = true; + for (auto it=children_.cbegin(); it!=children_.cend(); it++) { + (*it)->redo_and_set_modified(); } } void MultiUndoCommand::undo() { - if (done_) { - for (auto it=children_.crbegin(); it!=children_.crend(); it++) { - (*it)->undo_and_set_modified(); - } - done_ = false; + for (auto it=children_.crbegin(); it!=children_.crend(); it++) { + (*it)->undo_and_set_modified(); } } UndoCommand::UndoCommand() { prepared_ = false; + done_ = false; } void UndoCommand::redo_and_set_modified() @@ -76,17 +66,23 @@ void UndoCommand::undo_and_set_modified() void UndoCommand::redo_now() { - if (!prepared_) { - prepare(); - prepared_ = true; - } + if (!done_) { + if (!prepared_) { + prepare(); + prepared_ = true; + } - redo(); + redo(); + done_ = true; + } } void UndoCommand::undo_now() { - undo(); + if (done_) { + undo(); + done_ = false; + } } } diff --git a/app/undo/undocommand.h b/app/undo/undocommand.h index 94d4d3e8f..bd6032da2 100644 --- a/app/undo/undocommand.h +++ b/app/undo/undocommand.h @@ -75,12 +75,14 @@ private: bool prepared_; + bool done_; + }; class MultiUndoCommand : public UndoCommand { public: - MultiUndoCommand(); + MultiUndoCommand() = default; virtual Project* GetRelevantProject() const override { @@ -109,8 +111,6 @@ protected: private: std::vector children_; - bool done_; - }; } diff --git a/app/widget/CMakeLists.txt b/app/widget/CMakeLists.txt index 38fefccbd..915214d9e 100644 --- a/app/widget/CMakeLists.txt +++ b/app/widget/CMakeLists.txt @@ -51,7 +51,6 @@ add_subdirectory(timelinewidget) add_subdirectory(timeruler) add_subdirectory(timetarget) add_subdirectory(toolbar) -add_subdirectory(videoparamedit) add_subdirectory(viewer) set(OLIVE_SOURCES diff --git a/app/widget/curvewidget/CMakeLists.txt b/app/widget/curvewidget/CMakeLists.txt index 1ceeadf85..baafedb04 100644 --- a/app/widget/curvewidget/CMakeLists.txt +++ b/app/widget/curvewidget/CMakeLists.txt @@ -16,11 +16,9 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - widget/curvewidget/beziercontrolpointitem.h - widget/curvewidget/beziercontrolpointitem.cpp - widget/curvewidget/curveview.h widget/curvewidget/curveview.cpp - widget/curvewidget/curvewidget.h + widget/curvewidget/curveview.h widget/curvewidget/curvewidget.cpp + widget/curvewidget/curvewidget.h PARENT_SCOPE ) diff --git a/app/widget/curvewidget/beziercontrolpointitem.cpp b/app/widget/curvewidget/beziercontrolpointitem.cpp deleted file mode 100644 index 26f062803..000000000 --- a/app/widget/curvewidget/beziercontrolpointitem.cpp +++ /dev/null @@ -1,106 +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 "beziercontrolpointitem.h" - -#include -#include -#include -#include - -#include "common/qtutils.h" - -namespace olive { - -BezierControlPointItem::BezierControlPointItem(NodeKeyframe* key, NodeKeyframe::BezierType mode, QGraphicsItem *parent) : - QGraphicsRectItem(parent), - key_(key), - mode_(mode), - x_scale_(1.0), - y_scale_(1.0) -{ - setFlag(QGraphicsItem::ItemIsMovable); - - connect(key, &NodeKeyframe::TimeChanged, this, &BezierControlPointItem::UpdatePos); - - if (mode_ == NodeKeyframe::kInHandle) { - connect(key, &NodeKeyframe::BezierControlInChanged, this, &BezierControlPointItem::UpdatePos); - } else { - connect(key, &NodeKeyframe::BezierControlOutChanged, this, &BezierControlPointItem::UpdatePos); - } - - - int control_point_size = QtUtils::QFontMetricsWidth(qApp->fontMetrics(), "o"); - int half_sz = control_point_size / 2; - setRect(-half_sz, -half_sz, control_point_size, control_point_size); -} - -void BezierControlPointItem::SetXScale(double scale) -{ - x_scale_ = scale; - UpdatePos(); -} - -void BezierControlPointItem::SetYScale(double scale) -{ - y_scale_ = scale; - UpdatePos(); -} - -NodeKeyframe* BezierControlPointItem::key() const -{ - return key_; -} - -const NodeKeyframe::BezierType &BezierControlPointItem::mode() const -{ - return mode_; -} - -QPointF BezierControlPointItem::GetCorrespondingKeyframeHandle() const -{ - return key_->bezier_control(mode_); -} - -void BezierControlPointItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - if (option->state & QStyle::State_Selected) { - painter->setPen(widget->palette().highlight().color()); - } else { - painter->setPen(widget->palette().text().color()); - } - - painter->drawEllipse(rect()); -} - -void BezierControlPointItem::UpdatePos() -{ - QPointF handle_offset = GetCorrespondingKeyframeHandle(); - - // Scale handle offset - handle_offset.setX(handle_offset.x() * x_scale_); - - // Flip the Y coordinate because bezier curves are drawn bottom to top - handle_offset.setY(-handle_offset.y() * y_scale_); - - setPos(handle_offset - rect().center()); -} - -} diff --git a/app/widget/curvewidget/beziercontrolpointitem.h b/app/widget/curvewidget/beziercontrolpointitem.h deleted file mode 100644 index 26d87f8dc..000000000 --- a/app/widget/curvewidget/beziercontrolpointitem.h +++ /dev/null @@ -1,68 +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 . - -***/ - -#ifndef BEZIERCONTROLPOINTITEM_H -#define BEZIERCONTROLPOINTITEM_H - -#include - -#include "node/keyframe.h" - -namespace olive { - -class BezierControlPointItem : public QObject, public QGraphicsRectItem -{ -public: - BezierControlPointItem(NodeKeyframe* key, NodeKeyframe::BezierType mode, QGraphicsItem* parent = nullptr); - - void SetXScale(double scale); - - void SetYScale(double scale); - - NodeKeyframe* key() const; - - const NodeKeyframe::BezierType& mode() const; - - QPointF GetCorrespondingKeyframeHandle() const; - - void SetCorrespondingKeyframeHandle(const QPointF& handle); - - void SetOpposingKeyframeHandle(const QPointF& handle); - -protected: - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; - -private: - NodeKeyframe* key_; - - NodeKeyframe::BezierType mode_; - - double x_scale_; - - double y_scale_; - -private slots: - void UpdatePos(); - -}; - -} - -#endif // BEZIERCONTROLPOINTITEM_H diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 80108a0d5..e756d7376 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -20,47 +20,32 @@ #include "curveview.h" +#include #include #include +#include #include #include -#include #include "common/qtutils.h" +#include "widget/keyframeview/keyframeviewundo.h" +#include "widget/nodeparamview/nodeparamviewundo.h" namespace olive { -#define super KeyframeViewBase +#define super KeyframeView CurveView::CurveView(QWidget *parent) : - KeyframeViewBase(parent) + KeyframeView(parent), + dragging_bezier_pt_(nullptr) { setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - setDragMode(RubberBandDrag); - setViewportUpdateMode(FullViewportUpdate); SetYAxisEnabled(true); + SetAutoSelectSiblings(false); text_padding_ = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("i")); minimum_grid_space_ = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("00000")); - - connect(scene(), &QGraphicsScene::selectionChanged, this, &CurveView::SelectionChanged); -} - -CurveView::~CurveView() -{ - // Quick way to avoid segfault when QGraphicsScene::selectionChanged is emitted after other members have been destroyed - Clear(); -} - -void CurveView::Clear() -{ - KeyframeViewBase::Clear(); - - foreach (QGraphicsLineItem* line, lines_) { - delete line; - } - lines_.clear(); } void CurveView::ConnectInput(const NodeKeyframeTrackReference& ref) @@ -71,7 +56,12 @@ void CurveView::ConnectInput(const NodeKeyframeTrackReference& ref) } // Add keyframes from track - AddKeyframesOfTrack(ref); + KeyframeViewInputConnection *track_con = AddKeyframesOfTrack(ref); + track_con->SetBrush(keyframe_colors_.value(ref)); + track_connections_.insert(ref, track_con); + + // Signal to CurveWidget to update its bezier/linear/hold buttons if a key type changes + connect(track_con, &KeyframeViewInputConnection::TypeChanged, this, &CurveView::SelectionChanged); // Append to the list connected_inputs_.append(ref); @@ -85,7 +75,7 @@ void CurveView::DisconnectInput(const NodeKeyframeTrackReference& ref) } // Remove keyframes belonging to this element and track - RemoveKeyframesOfTrack(ref); + RemoveKeyframesOfTrack(track_connections_.take(ref)); // Remove from the list connected_inputs_.removeOne(ref); @@ -95,36 +85,21 @@ void CurveView::SelectKeyframesOfInput(const NodeKeyframeTrackReference& ref) { DeselectAll(); - for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { - if (it.key()->key_track_ref() == ref) { - it.value()->setSelected(true); + foreach (KeyframeViewInputConnection *con, track_connections_) { + foreach (NodeKeyframe *key, con->GetKeyframes()) { + SelectKeyframe(key); } } } -void CurveView::ZoomToFitInput(const NodeKeyframeTrackReference& ref) -{ - QList keys; - - for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { - if (it.key()->key_track_ref() == ref) { - keys.append(it.key()); - } - } - - ZoomToFitInternal(keys); -} - void CurveView::SetKeyframeTrackColor(const NodeKeyframeTrackReference &ref, const QColor &color) { // Insert color into hashmap keyframe_colors_.insert(ref, color); - // Update all keyframes - for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { - if (it.key()->key_track_ref() == ref) { - it.value()->SetOverrideBrush(color); - } + if (KeyframeViewInputConnection *con = track_connections_.value(ref)) { + // Update all keyframes + con->SetBrush(color); } } @@ -196,7 +171,7 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) QPainterPath path; // Draw straight line leading to first keyframe - QPointF first_key_pos = item_map().value(track.first())->pos(); + QPointF first_key_pos = GetKeyframePosition(track.first()); path.moveTo(QPointF(scene_bottom_left.x(), first_key_pos.y())); path.lineTo(first_key_pos); @@ -205,24 +180,24 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) NodeKeyframe* before = track.at(i-1); NodeKeyframe* after = track.at(i); - KeyframeViewItem* before_item = item_map().value(before); - KeyframeViewItem* after_item = item_map().value(after); + QPointF before_pos = GetKeyframePosition(before); + QPointF after_pos = GetKeyframePosition(after); if (before->type() == NodeKeyframe::kHold) { // Draw a hold keyframe (basically a right angle) - path.lineTo(after_item->pos().x(), before_item->pos().y()); - path.lineTo(after_item->pos().x(), after_item->pos().y()); + path.lineTo(after_pos.x(), before_pos.y()); + path.lineTo(after_pos.x(), after_pos.y()); } else if (before->type() == NodeKeyframe::kBezier && after->type() == NodeKeyframe::kBezier) { // Draw a cubic bezier // Cubic beziers have two control points, so we can just use both - QPointF before_control_point = before_item->pos() + ScalePoint(before->valid_bezier_control_out()); - QPointF after_control_point = after_item->pos() + ScalePoint(after->valid_bezier_control_in()); + QPointF before_control_point = before_pos + ScalePoint(before->valid_bezier_control_out()); + QPointF after_control_point = after_pos + ScalePoint(after->valid_bezier_control_in()); - path.cubicTo(before_control_point, after_control_point, after_item->pos()); + path.cubicTo(before_control_point, after_control_point, after_pos); } else if (before->type() == NodeKeyframe::kBezier || after->type() == NodeKeyframe::kBezier) { // Draw a quadratic bezier @@ -232,10 +207,10 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) QPointF control_point; if (before->type() == NodeKeyframe::kBezier) { - key_anchor = before_item->pos(); + key_anchor = before_pos; control_point = before->valid_bezier_control_out(); } else { - key_anchor = after_item->pos(); + key_anchor = after_pos; control_point = after->valid_bezier_control_in(); } @@ -243,66 +218,31 @@ void CurveView::drawBackground(QPainter *painter, const QRectF &rect) control_point = key_anchor + ScalePoint(control_point); // Create the path from both keyframes - path.quadTo(control_point, after_item->pos()); + path.quadTo(control_point, after_pos); } else { // Linear to linear - path.lineTo(after_item->pos()); + path.lineTo(after_pos); } } // Draw straight line leading from end keyframe - QPointF last_key_pos = item_map().value(track.last())->pos(); + QPointF last_key_pos = GetKeyframePosition(track.last()); path.lineTo(QPointF(scene_top_right.x(), last_key_pos.y())); painter->drawPath(path); } } } - - // Draw bezier control point lines - if (!bezier_control_points_.isEmpty()) { - painter->setPen(QPen(palette().text().color(), 1)); - - QVector bezier_lines; - foreach (BezierControlPointItem* item, bezier_control_points_) { - // All BezierControlPointItems should be children of a KeyframeViewItem - KeyframeViewItem* par = static_cast(item->parentItem()); - - bezier_lines.append(QLineF(par->pos(), par->pos() + item->pos())); - } - painter->drawLines(bezier_lines); - } } -void CurveView::KeyframeAboutToBeRemoved(NodeKeyframe *key) +void CurveView::drawForeground(QPainter *painter, const QRectF &rect) { - disconnect(key, &NodeKeyframe::ValueChanged, this, &CurveView::KeyframeValueChanged); - disconnect(key, &NodeKeyframe::TypeChanged, this, &CurveView::KeyframeTypeChanged); -} + bezier_pts_.clear(); -void CurveView::ScaleChangedEvent(const double& scale) -{ - KeyframeViewBase::ScaleChangedEvent(scale); - - foreach (BezierControlPointItem* item, bezier_control_points_) { - item->SetXScale(scale); - } -} - -void CurveView::VerticalScaleChangedEvent(double scale) -{ - Q_UNUSED(scale) - - for (auto iterator=item_map().begin();iterator!=item_map().end();iterator++) { - SetItemYFromKeyframeValue(iterator.value()->key(), iterator.value()); - } - - foreach (BezierControlPointItem* item, bezier_control_points_) { - item->SetYScale(scale); - } + super::drawForeground(painter, rect); } void CurveView::ContextMenuEvent(Menu &m) @@ -320,62 +260,334 @@ void CurveView::ContextMenuEvent(Menu &m) void CurveView::SceneRectUpdateEvent(QRectF &r) { - r.setTop(r.top() - this->height()); - r.setBottom(r.bottom() + this->height()); + double min_val, max_val; + bool got_val = false; + + foreach (KeyframeViewInputConnection *con, track_connections_) { + foreach (NodeKeyframe *key, con->GetKeyframes()) { + qreal key_y = GetItemYFromKeyframeValue(key); + + if (got_val) { + min_val = qMin(key_y, min_val); + max_val = qMax(key_y, max_val); + } else { + min_val = key_y; + max_val = key_y; + got_val = true; + } + } + } + + if (got_val) { + r.setTop(min_val - this->height()); + r.setBottom(max_val + this->height()); + } } -void CurveView::ZoomToFitInternal(const QList &keys) +qreal CurveView::GetKeyframeSceneY(KeyframeViewInputConnection *track, NodeKeyframe *key) { - if (keys.isEmpty()) { - // Prevent scaling to DBL_MIN/DBL_MAX + return GetItemYFromKeyframeValue(key); +} + +void CurveView::DrawKeyframe(QPainter *painter, NodeKeyframe *key, KeyframeViewInputConnection *track, const QRectF &key_rect) +{ + if (IsKeyframeSelected(key) && key->type() == NodeKeyframe::kBezier) { + // Draw bezier control points if keyframe is selected + int control_point_size = QtUtils::QFontMetricsWidth(fontMetrics(), "o"); + int half_sz = control_point_size / 2; + QRectF control_point_rect(-half_sz, -half_sz, control_point_size, control_point_size); + + painter->setPen(palette().text().color()); + painter->setBrush(Qt::NoBrush); + + QRectF cp_in = control_point_rect.translated(key_rect.center() + ScalePoint(key->bezier_control_in())); + QRectF cp_out = control_point_rect.translated(key_rect.center() + ScalePoint(key->bezier_control_out())); + + painter->drawLine(key_rect.center(), cp_in.center()); + painter->drawLine(key_rect.center(), cp_out.center()); + + painter->drawEllipse(cp_in); + painter->drawEllipse(cp_out); + + bezier_pts_.append({cp_in, key, NodeKeyframe::kInHandle}); + bezier_pts_.append({cp_out, key, NodeKeyframe::kOutHandle}); + } + + super::DrawKeyframe(painter, key, track, key_rect); +} + +bool CurveView::FirstChanceMousePress(QMouseEvent *event) +{ + dragging_bezier_pt_ = nullptr; + QPointF scene_pt = mapToScene(event->pos()); + foreach (const BezierPoint &b, bezier_pts_) { + if (b.rect.contains(scene_pt)) { + dragging_bezier_pt_ = &b; + break; + } + } + + if (dragging_bezier_pt_) { + NodeKeyframe *key = dragging_bezier_pt_->keyframe; + dragging_bezier_point_start_ = (dragging_bezier_pt_->type == NodeKeyframe::kInHandle) ? key->bezier_control_in() : key->bezier_control_out(); + dragging_bezier_point_opposing_start_ = (dragging_bezier_pt_->type == NodeKeyframe::kInHandle) ? key->bezier_control_out() : key->bezier_control_in(); + + drag_start_ = mapToScene(event->pos()); + return true; + } else { + return false; + } +} + +void CurveView::FirstChanceMouseMove(QMouseEvent *event) +{ + // Calculate cursor difference and scale it + QPointF scene_pos = mapToScene(event->pos()); + QPointF mouse_diff_scaled = GetScaledCursorPos(scene_pos - drag_start_); + + if (event->modifiers() & Qt::ShiftModifier) { + // If holding shift, only move one axis + mouse_diff_scaled.setY(0); + } + + // Flip the mouse Y because bezier control points are drawn bottom to top, not top to bottom + mouse_diff_scaled.setY(-mouse_diff_scaled.y()); + + QPointF new_bezier_pos = GenerateBezierControlPosition(dragging_bezier_pt_->type, + dragging_bezier_point_start_, + mouse_diff_scaled); + + // If the user is NOT holding control, we set the other handle to the exact negative of this handle + QPointF new_opposing_pos; + NodeKeyframe::BezierType opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_pt_->type); + + + if (!(event->modifiers() & Qt::ControlModifier)) { + new_opposing_pos = GenerateBezierControlPosition(opposing_type, + dragging_bezier_point_opposing_start_, + -mouse_diff_scaled); + } else { + new_opposing_pos = dragging_bezier_point_opposing_start_; + } + + dragging_bezier_pt_->keyframe->set_bezier_control(dragging_bezier_pt_->type, + new_bezier_pos); + + dragging_bezier_pt_->keyframe->set_bezier_control(opposing_type, + new_opposing_pos); + + Redraw(); +} + +void CurveView::FirstChanceMouseRelease(QMouseEvent *event) +{ + MultiUndoCommand* command = new MultiUndoCommand(); + + // Create undo command with the current bezier point and the old one + command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_pt_->keyframe, + dragging_bezier_pt_->type, + dragging_bezier_pt_->keyframe->bezier_control(dragging_bezier_pt_->type), + dragging_bezier_point_start_)); + + if (!(event->modifiers() & Qt::ControlModifier)) { + auto opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_pt_->type); + + command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_pt_->keyframe, + opposing_type, + dragging_bezier_pt_->keyframe->bezier_control(opposing_type), + dragging_bezier_point_opposing_start_)); + } + + dragging_bezier_pt_ = nullptr; + + Core::instance()->undo_stack()->push(command); +} + +void CurveView::KeyframeDragStart(QMouseEvent *event) +{ + drag_keyframe_values_.resize(GetSelectedKeyframes().size()); + for (int i=0; ivalue(); + } + + drag_start_ = mapToScene(event->pos()); +} + +void CurveView::KeyframeDragMove(QMouseEvent *event, QString &tip) +{ + if (event->modifiers() & Qt::ShiftModifier) { + // Lock to X axis only and set original values on all keys + for (int i=0; iset_value(drag_keyframe_values_.at(i)); + } return; } - rational min_time = RATIONAL_MAX; - rational max_time = RATIONAL_MIN; + // Calculate cursor difference + double scaled_diff = (mapToScene(event->pos()).y() - drag_start_.y()) / GetYScale(); - double min_val = DBL_MAX; - double max_val = DBL_MIN; + // Validate movement - ensure no keyframe goes above its max point or below its min point + for (int i=0; iparent(), - GetTimeTarget(), - key->time(), - false); + FloatSlider::DisplayType display = GetFloatDisplayTypeFromKeyframe(key); + Node* node = key->parent(); + double original_val = FloatSlider::TransformValueToDisplay(drag_keyframe_values_.at(i).toDouble(), display); + const QString& input = key->input(); + double new_val = FloatSlider::TransformDisplayToValue(original_val - scaled_diff, display); + double limited = new_val; - min_time = qMin(transformed_time, min_time); - max_time = qMax(transformed_time, max_time); + if (node->HasInputProperty(input, QStringLiteral("min"))) { + limited = qMax(limited, node->GetInputProperty(input, QStringLiteral("min")).toDouble()); + } - min_val = qMin(key->value().toDouble(), min_val); - max_val = qMax(key->value().toDouble(), max_val); + if (node->HasInputProperty(input, QStringLiteral("max"))) { + limited = qMin(limited, node->GetInputProperty(input, QStringLiteral("max")).toDouble()); + } + + if (limited != new_val) { + scaled_diff = original_val - limited; + } } - double time_range = max_time.toDouble() - min_time.toDouble(); - double new_x_scale = CalculateScaleFromDimensions(this->width(), time_range); - double new_y_scale = CalculateScaleFromDimensions(this->height(), max_val - min_val); + // Set values + for (int i=0; iset_value(FloatSlider::TransformDisplayToValue(FloatSlider::TransformValueToDisplay(drag_keyframe_values_.at(i).toDouble(), display) - scaled_diff, display)); + } - emit ScaleChanged(new_x_scale); - SetYScale(new_y_scale); + NodeKeyframe *tip_item = GetSelectedKeyframes().first(); - QMetaObject::invokeMethod(horizontalScrollBar(), "setValue", Qt::QueuedConnection, - Q_ARG(int, TimeToScene(min_time) - CalculatePaddingFromDimensionScale(this->width()))); - QMetaObject::invokeMethod(verticalScrollBar(), "setValue", Qt::QueuedConnection, - Q_ARG(int, GetItemYFromKeyframeValue(max_val) - CalculatePaddingFromDimensionScale(this->height()))); + bool ok; + double num_value = tip_item->value().toDouble(&ok); + + if (ok) { + tip = QStringLiteral("%1\n"); + tip.append(FloatSlider::ValueToString(num_value + GetOffsetFromKeyframe(tip_item), GetFloatDisplayTypeFromKeyframe(tip_item), 2, true)); + } +} + +void CurveView::KeyframeDragRelease(QMouseEvent *event, MultiUndoCommand *command) +{ + for (int i=0; ivalue().toDouble(), drag_keyframe_values_.at(i).toDouble())) { + command->add_child(new NodeParamSetKeyframeValueCommand(k, k->value(), drag_keyframe_values_.at(i))); + } + } +} + +QPointF CurveView::GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, const QPointF &start_point, const QPointF &scaled_cursor_diff) +{ + QPointF new_bezier_pos = start_point; + + new_bezier_pos += scaled_cursor_diff; + + // LIMIT bezier handles from overlapping each other + if (mode == NodeKeyframe::kInHandle) { + if (new_bezier_pos.x() > 0) { + new_bezier_pos.setX(0); + } + } else { + if (new_bezier_pos.x() < 0) { + new_bezier_pos.setX(0); + } + } + + return new_bezier_pos; +} + +QPointF CurveView::GetScaledCursorPos(const QPointF &cursor_pos) +{ + return QPointF(cursor_pos.x() / GetScale(), + cursor_pos.y() / GetYScale()); +} + +void CurveView::ZoomToFitInternal(bool selected_only) +{ + bool got_val = false; + + rational min_time, max_time; + double min_val, max_val; + + foreach (KeyframeViewInputConnection *con, track_connections_) { + foreach (NodeKeyframe *key, con->GetKeyframes()) { + if (!selected_only || IsKeyframeSelected(key)) { + rational transformed_time = GetAdjustedTime(key->parent(), + GetTimeTarget(), + key->time(), + false); + + qreal key_y = GetUnscaledItemYFromKeyframeValue(key); + + if (got_val) { + min_time = qMin(transformed_time, min_time); + max_time = qMax(transformed_time, max_time); + + min_val = qMin(key_y, min_val); + max_val = qMax(key_y, max_val); + } else { + min_time = transformed_time; + max_time = transformed_time; + + min_val = key_y; + max_val = key_y; + + got_val = true; + } + } + } + } + + // Prevent scaling if no keyframes were found + if (got_val) { + QRectF desired(QPointF(min_time.toDouble(), min_val), QPointF(max_time.toDouble(), max_val)); + + const double scale_divider = 0.5; + double scale_half_divider = scale_divider*0.5; + + double new_x_scale = viewport()->width() / desired.width() * scale_divider; + double new_y_scale; + + if (qFuzzyIsNull(desired.height())) { + // Catch divide by zero + new_y_scale = 1.0; + scale_half_divider = 0.5; + } else { + // Use height as normal + new_y_scale = viewport()->height() / desired.height() * scale_divider; + } + + emit ScaleChanged(new_x_scale); + SetYScale(new_y_scale); + + UpdateSceneRect(); + + int sb_x = desired.left() * new_x_scale - viewport()->width() * scale_half_divider; + QMetaObject::invokeMethod(horizontalScrollBar(), "setValue", Qt::QueuedConnection, Q_ARG(int, sb_x)); + + int sb_y = desired.top() * new_y_scale - viewport()->height() * scale_half_divider; + QMetaObject::invokeMethod(verticalScrollBar(), "setValue", Qt::QueuedConnection, Q_ARG(int, sb_y)); + } } qreal CurveView::GetItemYFromKeyframeValue(NodeKeyframe *key) { - return GetItemYFromKeyframeValue(key->value().toDouble()); + return GetUnscaledItemYFromKeyframeValue(key) * GetYScale(); } -qreal CurveView::GetItemYFromKeyframeValue(double value) +qreal CurveView::GetUnscaledItemYFromKeyframeValue(NodeKeyframe *key) { - return -value * GetYScale(); -} + double val = key->value().toDouble(); -void CurveView::SetItemYFromKeyframeValue(NodeKeyframe *key, KeyframeViewItem *item) -{ - item->SetOverrideY(GetItemYFromKeyframeValue(key)); + val = FloatSlider::TransformValueToDisplay(val, GetFloatDisplayTypeFromKeyframe(key)); + + val += GetOffsetFromKeyframe(key); + + return -val; } QPointF CurveView::ScalePoint(const QPointF &point) @@ -384,80 +596,48 @@ QPointF CurveView::ScalePoint(const QPointF &point) return QPointF(point.x() * GetScale(), - point.y() * GetYScale()); } -void CurveView::CreateBezierControlPoints(KeyframeViewItem* item) +FloatSlider::DisplayType CurveView::GetFloatDisplayTypeFromKeyframe(NodeKeyframe *key) { - BezierControlPointItem* bezier_in_pt = new BezierControlPointItem(item->key(), NodeKeyframe::kInHandle, item); - bezier_in_pt->SetXScale(GetScale()); - bezier_in_pt->SetYScale(GetYScale()); - bezier_control_points_.append(bezier_in_pt); - connect(bezier_in_pt, &QObject::destroyed, this, &CurveView::BezierControlPointDestroyed, Qt::DirectConnection); - - BezierControlPointItem* bezier_out_pt = new BezierControlPointItem(item->key(), NodeKeyframe::kOutHandle, item); - bezier_out_pt->SetXScale(GetScale()); - bezier_out_pt->SetYScale(GetYScale()); - bezier_control_points_.append(bezier_out_pt); - connect(bezier_out_pt, &QObject::destroyed, this, &CurveView::BezierControlPointDestroyed, Qt::DirectConnection); -} - -void CurveView::KeyframeValueChanged() -{ - NodeKeyframe* key = static_cast(sender()); - KeyframeViewItem* item = item_map().value(key); - - SetItemYFromKeyframeValue(key, item); -} - -void CurveView::KeyframeTypeChanged() -{ - NodeKeyframe* key = static_cast(sender()); - KeyframeViewItem* item = item_map().value(key); - - if (item->isSelected()) { - item->setSelected(false); - item->setSelected(true); - } -} - -void CurveView::SelectionChanged() -{ - // Clear current bezier handles - while (!bezier_control_points_.isEmpty()) { - delete bezier_control_points_.first(); + Node* node = key->parent(); + const QString& input = key->input(); + if (node->HasInputProperty(input, QStringLiteral("view"))) { + // Try to get view from input (which will be normal if unset) + return static_cast(node->GetInputProperty(input, QStringLiteral("view")).toInt()); } - QList selected = scene()->selectedItems(); - - foreach (QGraphicsItem* item, selected) { - KeyframeViewItem* this_item = static_cast(item); - - if (this_item->key()->type() == NodeKeyframe::kBezier) { - CreateBezierControlPoints(this_item); - } - } + // Fallback to normal + return FloatSlider::kNormal; } -void CurveView::BezierControlPointDestroyed() +double CurveView::GetOffsetFromKeyframe(NodeKeyframe *key) { - BezierControlPointItem* item = static_cast(sender()); - bezier_control_points_.removeOne(item); + Node *node = key->parent(); + const QString &input = key->input(); + if (node->HasInputProperty(input, QStringLiteral("offset"))) { + QVariant v = node->GetInputProperty(input, QStringLiteral("offset")); + + // NOTE: Implement getting correct offset for the track based on the data type + QVector track_vals = NodeValue::split_normal_value_into_track_values(node->GetInputDataType(input), v); + + return track_vals.at(key->track()).toDouble(); + } + + return 0; +} + +QPointF CurveView::GetKeyframePosition(NodeKeyframe *key) +{ + return QPointF(GetKeyframeSceneX(key), GetItemYFromKeyframeValue(key)); } void CurveView::ZoomToFit() { - ZoomToFitInternal(item_map().keys()); + ZoomToFitInternal(false); } void CurveView::ZoomToFitSelected() { - QList selected_keys; - - for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { - if (it.value()->isSelected()) { - selected_keys.append(it.key()); - } - } - - ZoomToFitInternal(selected_keys); + ZoomToFitInternal(true); } void CurveView::ResetZoom() @@ -466,16 +646,4 @@ void CurveView::ResetZoom() SetYScale(1.0); } -KeyframeViewItem* CurveView::AddKeyframe(NodeKeyframe* key) -{ - KeyframeViewItem* item = super::AddKeyframe(key); - SetItemYFromKeyframeValue(key, item); - item->SetOverrideBrush(keyframe_colors_.value(key->key_track_ref())); - - connect(key, &NodeKeyframe::ValueChanged, this, &CurveView::KeyframeValueChanged); - connect(key, &NodeKeyframe::TypeChanged, this, &CurveView::KeyframeTypeChanged); - - return item; -} - } diff --git a/app/widget/curvewidget/curveview.h b/app/widget/curvewidget/curveview.h index 6a4e155c7..1eb8355f1 100644 --- a/app/widget/curvewidget/curveview.h +++ b/app/widget/curvewidget/curveview.h @@ -21,36 +21,27 @@ #ifndef CURVEVIEW_H #define CURVEVIEW_H -#include "beziercontrolpointitem.h" #include "node/keyframe.h" #include "widget/keyframeview/keyframeview.h" -#include "widget/keyframeview/keyframeviewitem.h" +#include "widget/slider/floatslider.h" namespace olive { -class CurveView : public KeyframeViewBase +class CurveView : public KeyframeView { Q_OBJECT public: CurveView(QWidget* parent = nullptr); - virtual ~CurveView() override; - - virtual void Clear() override; - void ConnectInput(const NodeKeyframeTrackReference &ref); void DisconnectInput(const NodeKeyframeTrackReference &ref); void SelectKeyframesOfInput(const NodeKeyframeTrackReference &ref); - void ZoomToFitInput(const NodeKeyframeTrackReference &ref); - void SetKeyframeTrackColor(const NodeKeyframeTrackReference& ref, const QColor& color); public slots: - virtual KeyframeViewItem* AddKeyframe(NodeKeyframe* key) override; - void ZoomToFit(); void ZoomToFitSelected(); @@ -59,51 +50,70 @@ public slots: protected: virtual void drawBackground(QPainter* painter, const QRectF& rect) override; - - virtual void KeyframeAboutToBeRemoved(NodeKeyframe *key) override; - - virtual void ScaleChangedEvent(const double &scale) override; - - virtual void VerticalScaleChangedEvent(double scale) override; + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; virtual void ContextMenuEvent(Menu &m) override; virtual void SceneRectUpdateEvent(QRectF &r) override; + virtual qreal GetKeyframeSceneY(KeyframeViewInputConnection *track, NodeKeyframe *key) override; + + virtual void DrawKeyframe(QPainter *painter, NodeKeyframe *key, KeyframeViewInputConnection *track, const QRectF &key_rect) override; + + virtual bool FirstChanceMousePress(QMouseEvent *event) override; + virtual void FirstChanceMouseMove(QMouseEvent *event) override; + virtual void FirstChanceMouseRelease(QMouseEvent *event) override; + + virtual void KeyframeDragStart(QMouseEvent *event) override; + virtual void KeyframeDragMove(QMouseEvent *event, QString &tip) override; + virtual void KeyframeDragRelease(QMouseEvent *event, MultiUndoCommand *command) override; + private: - void ZoomToFitInternal(const QList &keys); + void ZoomToFitInternal(bool selected_only); qreal GetItemYFromKeyframeValue(NodeKeyframe* key); - qreal GetItemYFromKeyframeValue(double value); - - void SetItemYFromKeyframeValue(NodeKeyframe* key, KeyframeViewItem* item); + qreal GetUnscaledItemYFromKeyframeValue(NodeKeyframe* key); QPointF ScalePoint(const QPointF& point); + static FloatSlider::DisplayType GetFloatDisplayTypeFromKeyframe(NodeKeyframe *key); + + static double GetOffsetFromKeyframe(NodeKeyframe *key); + void AdjustLines(); - void CreateBezierControlPoints(KeyframeViewItem *item); + QPointF GetKeyframePosition(NodeKeyframe *key); + + static QPointF GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, + const QPointF& start_point, + const QPointF& scaled_cursor_diff); + + QPointF GetScaledCursorPos(const QPointF &cursor_pos); QHash keyframe_colors_; + QHash track_connections_; int text_padding_; int minimum_grid_space_; - QVector lines_; - - QVector bezier_control_points_; - QVector connected_inputs_; -private slots: - void KeyframeValueChanged(); + struct BezierPoint + { + QRectF rect; + NodeKeyframe *keyframe; + NodeKeyframe::BezierType type; + }; - void KeyframeTypeChanged(); + QVector bezier_pts_; + const BezierPoint *dragging_bezier_pt_; - void SelectionChanged(); + QPointF dragging_bezier_point_start_; + QPointF dragging_bezier_point_opposing_start_; + QPointF drag_start_; - void BezierControlPointDestroyed(); + QVector drag_keyframe_values_; }; diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index 4de5787ef..4d56896ae 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -34,8 +34,10 @@ namespace olive { +#define super TimeBasedWidget + CurveWidget::CurveWidget(QWidget *parent) : - TimeBasedWidget(parent) + super(parent) { QHBoxLayout* outer_layout = new QHBoxLayout(this); @@ -45,10 +47,7 @@ CurveWidget::CurveWidget(QWidget *parent) : tree_view_ = new NodeTreeView(); tree_view_->SetOnlyShowKeyframable(true); tree_view_->SetShowKeyframeTracksAsRows(true); - connect(tree_view_, &NodeTreeView::NodeEnableChanged, this, &CurveWidget::NodeEnabledChanged); - connect(tree_view_, &NodeTreeView::InputEnableChanged, this, &CurveWidget::InputEnabledChanged); connect(tree_view_, &NodeTreeView::InputSelectionChanged, this, &CurveWidget::InputSelectionChanged); - connect(tree_view_, &NodeTreeView::InputDoubleClicked, this, &CurveWidget::InputDoubleClicked); splitter->addWidget(tree_view_); QWidget* workarea = new QWidget(); @@ -99,7 +98,7 @@ CurveWidget::CurveWidget(QWidget *parent) : // Connect ruler and view together connect(view_, &CurveView::TimeChanged, this, &CurveWidget::SetTimeAndSignal); - connect(view_->scene(), &QGraphicsScene::selectionChanged, this, &CurveWidget::SelectionChanged); + connect(view_, &CurveView::SelectionChanged, this, &CurveWidget::SelectionChanged); connect(view_, &CurveView::ScaleChanged, this, &CurveWidget::SetScale); connect(view_, &CurveView::Dragged, this, &CurveWidget::KeyframeViewDragged); @@ -113,12 +112,6 @@ CurveWidget::CurveWidget(QWidget *parent) : SetScale(120.0); } -CurveWidget::~CurveWidget() -{ - // Quick way to avoid segfault when QGraphicsScene::selectionChanged is emitted after other members have been destroyed - view_->Clear(); -} - const double &CurveWidget::GetVerticalScale() { return view_->GetYScale(); @@ -138,27 +131,38 @@ void CurveWidget::SetNodes(const QVector &nodes) { tree_view_->SetNodes(nodes); - // Detect removed nodes - foreach (Node* n, nodes_) { - if (!nodes.contains(n)) { - ConnectNode(n, false); - } - } - - // Detect added nodes - foreach (Node* n, nodes) { - if (tree_view_->IsNodeEnabled(n) && !nodes_.contains(n)) { - ConnectNode(n, true); - } - } - // Save new node list nodes_ = nodes; + + // Generate colors + foreach (Node *node, nodes_) { + foreach (const QString& input, node->inputs()) { + if (node->IsInputKeyframable(input) && !node->IsInputHidden(input)) { + int arr_sz = node->InputArraySize(input); + for (int i=-1; i& tracks = node->GetKeyframeTracks(input, i); + + for (int j=0; jSetKeyframeTrackColor(ref, c); + view_->SetKeyframeTrackColor(ref, c); + } + } + } + } + } + } } void CurveWidget::TimeChangedEvent(const rational &time) { - TimeBasedWidget::TimeChangedEvent(time); + super::TimeChangedEvent(time); view_->SetTime(time); UpdateBridgeTime(time); @@ -166,20 +170,22 @@ void CurveWidget::TimeChangedEvent(const rational &time) void CurveWidget::TimebaseChangedEvent(const rational &timebase) { - TimeBasedWidget::TimebaseChangedEvent(timebase); + super::TimebaseChangedEvent(timebase); view_->SetTimebase(timebase); } void CurveWidget::ScaleChangedEvent(const double &scale) { - TimeBasedWidget::ScaleChangedEvent(scale); + super::ScaleChangedEvent(scale); view_->SetScale(scale); } void CurveWidget::TimeTargetChangedEvent(Node *target) { + TimeTargetObject::TimeTargetChangedEvent(target); + key_control_->SetTimeTarget(target); view_->SetTimeTarget(target); @@ -187,6 +193,8 @@ void CurveWidget::TimeTargetChangedEvent(Node *target) void CurveWidget::ConnectedNodeChangeEvent(ViewerOutput *n) { + super::ConnectedNodeChangeEvent(n); + SetTimeTarget(n); } @@ -216,91 +224,47 @@ void CurveWidget::UpdateBridgeTime(const rational &time) key_control_->SetTime(time); } -void CurveWidget::ConnectNode(Node *node, bool connect) +void CurveWidget::ConnectInput(Node *node, const QString &input, int element) { - foreach (const QString& input, node->inputs()) { - if (node->IsInputKeyframable(input)) { - ConnectInput(node, input, connect); + if (element == -1 && node->InputIsArray(input)) { + // This is the root element, connect all elements (if applicable) + int arr_sz = node->InputArraySize(input); + for (int i=-1; iIsInputKeyframable(input)) { - qWarning() << "Tried to connect input that isn't keyframable"; - return; - } - - int track_count = NodeValue::get_number_of_keyframe_tracks(node->GetInputDataType(input)); - bool multiple_tracks = track_count > 1; - - int arr_sz = node->InputArraySize(input); - for (int i=-1; i& tracks = node->GetKeyframeTracks(input, i); - - for (int j=0; jSetKeyframeTrackColor(ref, c); - view_->SetKeyframeTrackColor(ref, c); - } - } - - if (tree_view_->IsInputEnabled(NodeKeyframeTrackReference(NodeInput(node, input, i), multiple_tracks ? -1 : 0))) { - if (multiple_tracks) { - for (int j=0; jIsInputEnabled(ref)) { - if (connect) { - view_->ConnectInput(ref); - } else { - view_->DisconnectInput(ref); - } - } - } - } else { - NodeKeyframeTrackReference ref(NodeInput(node, input, i), 0); - if (connect) { - view_->ConnectInput(ref); - } else { - view_->DisconnectInput(ref); - } - } - } + NodeInput input_ref(node, input, element); + int track_count = NodeValue::get_number_of_keyframe_tracks(input_ref.GetDataType()); + for (int i=0; iConnectInput(track_ref); + selected_tracks_.append(track_ref); } } void CurveWidget::SelectionChanged() { - QList selected = view_->scene()->selectedItems(); + const QVector &selected = view_->GetSelectedKeyframes(); SetKeyframeButtonChecked(false); SetKeyframeButtonEnabled(!selected.isEmpty()); if (!selected.isEmpty()) { bool all_same_type = true; - NodeKeyframe::Type type = static_cast(selected.first())->key()->type(); + NodeKeyframe::Type type = selected.first()->type(); for (int i=1;i(selected.at(i-1)); - KeyframeViewItem* this_item = static_cast(selected.at(i)); + NodeKeyframe* prev_item = selected.at(i-1); + NodeKeyframe* this_item = selected.at(i); - if (prev_item->key()->type() != this_item->key()->type()) { + if (prev_item->type() != this_item->type()) { all_same_type = false; break; } @@ -323,7 +287,7 @@ void CurveWidget::KeyframeTypeButtonTriggered(bool checked) } // Get selected items and do nothing if there are none - QList selected = view_->scene()->selectedItems(); + const QVector &selected = view_->GetSelectedKeyframes(); if (selected.isEmpty()) { return; } @@ -345,51 +309,40 @@ void CurveWidget::KeyframeTypeButtonTriggered(bool checked) MultiUndoCommand* command = new MultiUndoCommand(); - foreach (QGraphicsItem* item, selected) { - KeyframeViewItem* key_item = static_cast(item); - - command->add_child(new KeyframeSetTypeCommand(key_item->key(), new_type)); + foreach (NodeKeyframe* item, selected) { + command->add_child(new KeyframeSetTypeCommand(item, new_type)); } Core::instance()->undo_stack()->push(command); } -void CurveWidget::NodeEnabledChanged(Node* n, bool e) -{ - ConnectNode(n, e); -} - -void CurveWidget::InputEnabledChanged(const NodeKeyframeTrackReference& ref, bool e) -{ - if (e) { - view_->ConnectInput(ref); - } else { - view_->DisconnectInput(ref); - } -} - -void CurveWidget::AddKeyframe(NodeKeyframe *key) -{ - view_->AddKeyframe(key); -} - -void CurveWidget::RemoveKeyframe(NodeKeyframe *key) -{ - view_->RemoveKeyframe(key); -} - void CurveWidget::InputSelectionChanged(const NodeKeyframeTrackReference& ref) { key_control_->SetInput(ref.input()); - if (ref.IsValid()) { - view_->SelectKeyframesOfInput(ref); + foreach (const NodeKeyframeTrackReference &c, selected_tracks_) { + view_->DisconnectInput(c); } -} -void CurveWidget::InputDoubleClicked(const NodeKeyframeTrackReference& ref) -{ - view_->ZoomToFitInput(ref); + selected_tracks_.clear(); + + if (ref.IsValid() && !ref.input().IsArray()) { + // This reference is a track, connect it only + view_->ConnectInput(ref); + selected_tracks_.append(ref); + } else if (ref.input().IsValid()) { + // This reference is a input, connect all tracks + ConnectInput(ref.input().node(), ref.input().input(), ref.input().element()); + } else if (Node *node = ref.input().node()) { + // This is a node, add all inputs + foreach (const QString &input, node->inputs()) { + if (node->IsInputKeyframable(input) && !node->IsInputHidden(input)) { + ConnectInput(node, input, -1); + } + } + } + + view_->ZoomToFit(); } void CurveWidget::KeyframeViewDragged(int x, int y) diff --git a/app/widget/curvewidget/curvewidget.h b/app/widget/curvewidget/curvewidget.h index 7b9e01021..ba6d1304b 100644 --- a/app/widget/curvewidget/curvewidget.h +++ b/app/widget/curvewidget/curvewidget.h @@ -40,8 +40,6 @@ class CurveWidget : public TimeBasedWidget, public TimeTargetObject public: CurveWidget(QWidget* parent = nullptr); - virtual ~CurveWidget() override; - const double& GetVerticalScale(); void SetVerticalScale(const double& vscale); @@ -78,9 +76,9 @@ private: void UpdateBridgeTime(const rational &time); - void ConnectNode(Node* node, bool connect); + void ConnectInput(Node *node, const QString &input, int element); - void ConnectInput(Node* node, const QString& input, bool connect); + void ConnectInputInternal(Node *node, const QString &input, int element); QHash keyframe_colors_; @@ -98,23 +96,15 @@ private: QVector nodes_; + QVector selected_tracks_; + private slots: void SelectionChanged(); void KeyframeTypeButtonTriggered(bool checked); - void NodeEnabledChanged(Node* n, bool e); - - void InputEnabledChanged(const NodeKeyframeTrackReference &ref, bool e); - - void AddKeyframe(NodeKeyframe* key); - - void RemoveKeyframe(NodeKeyframe* key); - void InputSelectionChanged(const NodeKeyframeTrackReference& ref); - void InputDoubleClicked(const NodeKeyframeTrackReference& ref); - void KeyframeViewDragged(int x, int y); void CatchUpYScrollToPoint(int point); diff --git a/app/widget/keyframeview/CMakeLists.txt b/app/widget/keyframeview/CMakeLists.txt index c4d5ef43e..d61022fbe 100644 --- a/app/widget/keyframeview/CMakeLists.txt +++ b/app/widget/keyframeview/CMakeLists.txt @@ -16,13 +16,11 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - widget/keyframeview/keyframeview.h widget/keyframeview/keyframeview.cpp - widget/keyframeview/keyframeviewbase.h - widget/keyframeview/keyframeviewbase.cpp - widget/keyframeview/keyframeviewitem.h - widget/keyframeview/keyframeviewitem.cpp - widget/keyframeview/keyframeviewundo.h + widget/keyframeview/keyframeview.h + widget/keyframeview/keyframeviewinputconnection.cpp + widget/keyframeview/keyframeviewinputconnection.h widget/keyframeview/keyframeviewundo.cpp + widget/keyframeview/keyframeviewundo.h PARENT_SCOPE ) diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index bba5f29f9..57d8f8c5d 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -20,40 +20,528 @@ #include "keyframeview.h" +#include +#include +#include + +#include "common/qtutils.h" +#include "dialog/keyframeproperties/keyframeproperties.h" +#include "keyframeviewundo.h" +#include "node/node.h" +#include "widget/menu/menu.h" +#include "widget/menu/menushared.h" +#include "widget/nodeparamview/nodeparamviewundo.h" + namespace olive { -#define super KeyframeViewBase +#define super TimeBasedView KeyframeView::KeyframeView(QWidget *parent) : - KeyframeViewBase(parent), - max_scroll_(0) + super(parent), + selection_manager_(this), + autoselect_siblings_(true), + max_scroll_(0), + first_chance_mouse_event_(false) { setAlignment(Qt::AlignLeft | Qt::AlignTop); + SetDefaultDragMode(RubberBandDrag); + setContextMenuPolicy(Qt::CustomContextMenu); + + connect(this, &KeyframeView::customContextMenuRequested, this, &KeyframeView::ShowContextMenu); } -void KeyframeView::SetElementY(const NodeInput &c, int y) +void KeyframeView::DeleteSelected() { - qreal scene_y = mapToScene(mapFromGlobal(QPoint(0, y))).y(); + MultiUndoCommand* command = new MultiUndoCommand(); - element_y_.insert(c, scene_y); + foreach (NodeKeyframe *key, GetSelectedKeyframes()) { + command->add_child(new NodeParamRemoveKeyframeCommand(key)); + } - for (auto it=item_map().cbegin(); it!=item_map().cend(); it++) { - if (it.key()->key_track_ref().input() == c) { - it.value()->SetOverrideY(scene_y); + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + +KeyframeView::NodeConnections KeyframeView::AddKeyframesOfNode(Node *n) +{ + NodeConnections map; + + foreach (const QString& i, n->inputs()) { + map.insert(i, AddKeyframesOfInput(n, i)); + } + + return map; +} + +KeyframeView::InputConnections KeyframeView::AddKeyframesOfInput(Node* n, const QString& input) +{ + InputConnections vec; + + if (n->IsInputKeyframable(input)) { + int arr_sz = n->InputArraySize(input); + vec.resize(arr_sz + 1); + for (int i=-1; i& tracks = input.node()->GetKeyframeTracks(input); + ElementConnections vec(tracks.size()); + + for (int i=0; iGetKeyframes()) { + selection_manager_.Deselect(key); + } + delete connection; + Redraw(); + emit SelectionChanged(); + } +} + +void KeyframeView::SelectAll() +{ + foreach (KeyframeViewInputConnection *track, tracks_) { + foreach (NodeKeyframe *key, track->GetKeyframes()) { + SelectKeyframe(key); } } } + +void KeyframeView::DeselectAll() +{ + selection_manager_.ClearSelection(); + + Redraw(); +} + +void KeyframeView::Clear() +{ + if (!tracks_.isEmpty()) { + qDeleteAll(tracks_); + tracks_.clear(); + Redraw(); + } + + selection_manager_.ClearSelection(); +} + +void KeyframeView::SelectionManagerSelectEvent(void *obj) +{ + if (autoselect_siblings_) { + NodeKeyframe *key = static_cast(obj); + QVector keys = key->parent()->GetKeyframesAtTime(key->input(), key->time(), key->element()); + foreach (NodeKeyframe* k, keys) { + if (k != key) { + SelectKeyframe(k); + } + } + } + + emit SelectionChanged(); +} + +void KeyframeView::SelectionManagerDeselectEvent(void *obj) +{ + if (autoselect_siblings_) { + NodeKeyframe *key = static_cast(obj); + QVector keys = key->parent()->GetKeyframesAtTime(key->input(), key->time(), key->element()); + foreach (NodeKeyframe* k, keys) { + if (k != key) { + DeselectKeyframe(k); + } + } + } + + emit SelectionChanged(); +} + +void KeyframeView::mousePressEvent(QMouseEvent *event) +{ + NodeKeyframe *key_under_cursor = selection_manager_.GetObjectAtPoint(event->pos()); + + if (HandPress(event) || (!key_under_cursor && PlayheadPress(event))) { + return; + } + + // Do mouse press things + if (FirstChanceMousePress(event)) { + first_chance_mouse_event_ = true; + } else if (NodeKeyframe *initial_key = selection_manager_.MousePress(event)) { + selection_manager_.DragStart(initial_key, event); + KeyframeDragStart(event); + } else { + selection_manager_.RubberBandStart(event); + } + + // Update view + Redraw(); +} + +void KeyframeView::mouseMoveEvent(QMouseEvent *event) +{ + if (HandMove(event) || PlayheadMove(event)) { + return; + } + + if (first_chance_mouse_event_) { + FirstChanceMouseMove(event); + } else if (selection_manager_.IsDragging()) { + QString tip; + KeyframeDragMove(event, tip); + selection_manager_.DragMove(event, tip); + } else if (selection_manager_.IsRubberBanding()) { + selection_manager_.RubberBandMove(event); + Redraw(); + } + + if (event->buttons()) { + // Signal cursor pos in case we should scroll to catch up to it + QPointF scene_pos = mapToScene(event->pos()); + emit Dragged(scene_pos.x(), scene_pos.y()); + } +} + +void KeyframeView::mouseReleaseEvent(QMouseEvent *event) +{ + if (HandRelease(event) || PlayheadRelease(event)) { + return; + } + + if (first_chance_mouse_event_) { + FirstChanceMouseRelease(event); + first_chance_mouse_event_ = false; + } else if (selection_manager_.IsDragging()) { + MultiUndoCommand* command = new MultiUndoCommand(); + selection_manager_.DragStop(command); + KeyframeDragRelease(event, command); + Core::instance()->undo_stack()->push(command); + } else if (selection_manager_.IsRubberBanding()) { + selection_manager_.RubberBandStop(); + Redraw(); + emit SelectionChanged(); + } +} + +int BinarySearchFirstKeyframeAfterOrAt(const QVector &keys, const rational &time) +{ + int low = 0; + int high = keys.size()-1; + + while (low <= high) { + int mid = low + (high-low)/2; + NodeKeyframe *test_key = keys.at(mid); + + if (test_key->time() == time || (test_key->time() > time && (mid == 0 || keys.at(mid-1)->time() < time))) { + return mid; + } else if (test_key->time() < time) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + return -1; +} + +void KeyframeView::drawForeground(QPainter *painter, const QRectF &rect) +{ + int key_sz = QtUtils::QFontMetricsWidth(fontMetrics(), "Oi"); + int key_rad = key_sz/2; + + selection_manager_.ClearDrawnObjects(); + + painter->setRenderHint(QPainter::Antialiasing); + + foreach (KeyframeViewInputConnection *track, tracks_) { + const QVector &keys = track->GetKeyframes(); + + if (keys.isEmpty()) { + continue; + } + + if (!IsYAxisEnabled()) { + // Filter out if the keyframes are offscreen Y + qreal y = GetKeyframeSceneY(track, keys.first()); + if (y + key_rad < rect.top() || y - key_rad >= rect.bottom()) { + continue; + } + } + + // Find first keyframe to show with binary search + rational left_time = SceneToTime(rect.left() - key_sz); + int using_index = BinarySearchFirstKeyframeAfterOrAt(keys, left_time); + + rational next_key = RATIONAL_MIN; + NodeKeyframe::Type last_type = NodeKeyframe::kInvalid; + for (int i=using_index; itime() < next_key && key->type() == last_type) { + // This key will be drawn at exactly the same location as the last one and therefore + // doesn't need to be drawn. See if the next one will be drawn. + i++; + if (i == keys.size()) { + break; + } + + key = keys.at(i); + + if (key->time() < next_key) { + // Next key still won't be drawn, so we'll switch to a binary search + i = BinarySearchFirstKeyframeAfterOrAt(keys, next_key); + + if (i == -1) { + break; + } + + key = keys.at(i); + } + } + + QRectF key_rect(-key_rad, -key_rad, key_sz, key_sz); + qreal key_x = GetKeyframeSceneX(key); + key_rect.translate(key_x, GetKeyframeSceneY(track, key)); + + if (key_rect.left() >= rect.right()) { + // Break after last keyframe + break; + } + + DrawKeyframe(painter, key, track, key_rect); + + next_key = SceneToTime(key_x + 1); + last_type = key->type(); + } + } + + super::drawForeground(painter, rect); +} + +void KeyframeView::DrawKeyframe(QPainter *painter, NodeKeyframe *key, KeyframeViewInputConnection *track, const QRectF &key_rect) +{ + painter->setPen(Qt::black); + + if (IsKeyframeSelected(key)) { + painter->setBrush(palette().highlight()); + } else { + painter->setBrush(track->GetBrush()); + } + + selection_manager_.DeclareDrawnObject(key, key_rect); + + switch (key->type()) { + case NodeKeyframe::kInvalid: + break; + case NodeKeyframe::kLinear: + { + QPointF points[] = { + QPointF(key_rect.center().x(), key_rect.top()), + QPointF(key_rect.right(), key_rect.center().y()), + QPointF(key_rect.center().x(), key_rect.bottom()), + QPointF(key_rect.left(), key_rect.center().y()) + }; + + painter->drawPolygon(points, 4); + break; + } + case NodeKeyframe::kBezier: + painter->drawEllipse(key_rect); + break; + case NodeKeyframe::kHold: + painter->drawRect(key_rect); + break; + } +} + +void KeyframeView::ScaleChangedEvent(const double &scale) +{ + super::ScaleChangedEvent(scale); + + Redraw(); +} + +void KeyframeView::TimeTargetChangedEvent(Node *target) +{ + Redraw(); +} + +void KeyframeView::TimebaseChangedEvent(const rational &timebase) +{ + super::TimebaseChangedEvent(timebase); + + selection_manager_.SetTimebase(timebase); +} + +void KeyframeView::ContextMenuEvent(Menu& m) +{ + Q_UNUSED(m) +} + +void KeyframeView::SelectKeyframe(NodeKeyframe *key) +{ + if (selection_manager_.Select(key)) { + Redraw(); + + emit SelectionChanged(); + } +} + +void KeyframeView::DeselectKeyframe(NodeKeyframe *key) +{ + if (selection_manager_.Deselect(key)) { + Redraw(); + + emit SelectionChanged(); + } +} + +rational KeyframeView::GetAdjustedKeyframeTime(NodeKeyframe *key) +{ + return GetAdjustedTime(key->parent(), GetTimeTarget(), key->time(), false); +} + +double KeyframeView::GetKeyframeSceneX(NodeKeyframe *key) +{ + return TimeToScene(GetAdjustedKeyframeTime(key)); +} + +qreal KeyframeView::GetKeyframeSceneY(KeyframeViewInputConnection *track, NodeKeyframe *key) +{ + return mapFromGlobal(QPoint(0, track->GetKeyframeY())).y(); +} + void KeyframeView::SceneRectUpdateEvent(QRectF &rect) { rect.setY(0); rect.setHeight(max_scroll_); } -KeyframeViewItem* KeyframeView::AddKeyframe(NodeKeyframe* key) +rational KeyframeView::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff) { - KeyframeViewItem* item = super::AddKeyframe(key); - item->SetOverrideY(element_y_.value(key->key_track_ref().input())); - return item; + return rational::fromDouble(old_time.toDouble() + cursor_diff); +} + +void KeyframeView::ShowContextMenu() +{ + Menu m; + + MenuShared::instance()->AddItemsForEditMenu(&m, false); + + QAction* linear_key_action = nullptr; + QAction* bezier_key_action = nullptr; + QAction* hold_key_action = nullptr; + + if (!GetSelectedKeyframes().isEmpty()) { + bool all_keys_are_same_type = true; + NodeKeyframe::Type type = GetSelectedKeyframes().first()->type(); + + for (int i=1;itype() != prev_item->type()) { + all_keys_are_same_type = false; + break; + } + } + + m.addSeparator(); + + linear_key_action = m.addAction(tr("Linear")); + bezier_key_action = m.addAction(tr("Bezier")); + hold_key_action = m.addAction(tr("Hold")); + + if (all_keys_are_same_type) { + switch (type) { + case NodeKeyframe::kInvalid: + break; + case NodeKeyframe::kLinear: + linear_key_action->setChecked(true); + break; + case NodeKeyframe::kBezier: + bezier_key_action->setChecked(true); + break; + case NodeKeyframe::kHold: + hold_key_action->setChecked(true); + break; + } + } + } + + m.addSeparator(); + + AddSetScrollZoomsByDefaultActionToMenu(&m); + + m.addSeparator(); + + ContextMenuEvent(m); + + if (!GetSelectedKeyframes().isEmpty()) { + m.addSeparator(); + + QAction* properties_action = m.addAction(tr("P&roperties")); + connect(properties_action, &QAction::triggered, this, &KeyframeView::ShowKeyframePropertiesDialog); + } + + QAction* selected = m.exec(QCursor::pos()); + + // Process keyframe type changes + if (selected) { + if (selected == linear_key_action + || selected == bezier_key_action + || selected == hold_key_action) { + NodeKeyframe::Type new_type; + + if (selected == hold_key_action) { + new_type = NodeKeyframe::kHold; + } else if (selected == bezier_key_action) { + new_type = NodeKeyframe::kBezier; + } else { + new_type = NodeKeyframe::kLinear; + } + + MultiUndoCommand* command = new MultiUndoCommand(); + foreach (NodeKeyframe* item, GetSelectedKeyframes()) { + command->add_child(new KeyframeSetTypeCommand(item, new_type)); + } + Core::instance()->undo_stack()->push(command); + } + } +} + +void KeyframeView::ShowKeyframePropertiesDialog() +{ + if (!GetSelectedKeyframes().isEmpty()) { + KeyframePropertiesDialog kd(GetSelectedKeyframes(), timebase(), this); + kd.exec(); + } +} + +void KeyframeView::Redraw() +{ + viewport()->update(); } } diff --git a/app/widget/keyframeview/keyframeview.h b/app/widget/keyframeview/keyframeview.h index 9b7f76d89..09565f1b2 100644 --- a/app/widget/keyframeview/keyframeview.h +++ b/app/widget/keyframeview/keyframeview.h @@ -18,39 +18,135 @@ ***/ -#ifndef KEYFRAMEVIEW_H -#define KEYFRAMEVIEW_H +#ifndef KEYFRAMEVIEWBASE_H +#define KEYFRAMEVIEWBASE_H -#include "keyframeviewbase.h" +#include "keyframeviewinputconnection.h" +#include "node/keyframe.h" +#include "widget/menu/menu.h" +#include "widget/timebased/timebasedview.h" +#include "widget/timebased/timebasedviewselectionmanager.h" +#include "widget/timetarget/timetarget.h" namespace olive { -class KeyframeView : public KeyframeViewBase +class KeyframeView : public TimeBasedView, public TimeTargetObject { Q_OBJECT public: KeyframeView(QWidget* parent = nullptr); + void DeleteSelected(); + + using ElementConnections = QVector; + using InputConnections = QVector; + using NodeConnections = QMap; + + NodeConnections AddKeyframesOfNode(Node* n); + + InputConnections AddKeyframesOfInput(Node *n, const QString &input); + + ElementConnections AddKeyframesOfElement(const NodeInput &input); + + KeyframeViewInputConnection *AddKeyframesOfTrack(const NodeKeyframeTrackReference &ref); + + void RemoveKeyframesOfTrack(KeyframeViewInputConnection *connection); + + void SelectAll(); + + void DeselectAll(); + + void Clear(); + + const QVector &GetSelectedKeyframes() const + { + return selection_manager_.GetSelectedObjects(); + } + + virtual void SelectionManagerSelectEvent(void *obj) override; + virtual void SelectionManagerDeselectEvent(void *obj) override; + void SetMaxScroll(int i) { max_scroll_ = i; + UpdateSceneRect(); } - void SetElementY(const NodeInput& c, int y); +signals: + void Dragged(int current_x, int current_y); + + void SelectionChanged(); protected: + virtual void mousePressEvent(QMouseEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + + virtual void drawForeground(QPainter *painter, const QRectF &rect) override; + + virtual void DrawKeyframe(QPainter *painter, NodeKeyframe *key, KeyframeViewInputConnection *track, const QRectF &key_rect); + + virtual void ScaleChangedEvent(const double& scale) override; + + virtual void TimeTargetChangedEvent(Node*) override; + + virtual void TimebaseChangedEvent(const rational &timebase) override; + + virtual void ContextMenuEvent(Menu &m); + + virtual bool FirstChanceMousePress(QMouseEvent *event){return false;} + virtual void FirstChanceMouseMove(QMouseEvent *event){} + virtual void FirstChanceMouseRelease(QMouseEvent *event){} + + virtual void KeyframeDragStart(QMouseEvent *event){} + virtual void KeyframeDragMove(QMouseEvent *event, QString &tip){} + virtual void KeyframeDragRelease(QMouseEvent *event, MultiUndoCommand *command){} + + void SelectKeyframe(NodeKeyframe *key); + + void DeselectKeyframe(NodeKeyframe *key); + + bool IsKeyframeSelected(NodeKeyframe *key) const + { + return selection_manager_.IsSelected(key); + } + + rational GetAdjustedKeyframeTime(NodeKeyframe *key); + + double GetKeyframeSceneX(NodeKeyframe *key); + + virtual qreal GetKeyframeSceneY(KeyframeViewInputConnection *track, NodeKeyframe *key); + + void SetAutoSelectSiblings(bool e) + { + autoselect_siblings_ = e; + } + virtual void SceneRectUpdateEvent(QRectF& rect) override; -public slots: - virtual KeyframeViewItem* AddKeyframe(NodeKeyframe* key) override; +protected slots: + void Redraw(); private: - QHash element_y_; + rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff); + + QVector tracks_; + + TimeBasedViewSelectionManager selection_manager_; + + bool autoselect_siblings_; int max_scroll_; + bool first_chance_mouse_event_; + +private slots: + void ShowContextMenu(); + + void ShowKeyframePropertiesDialog(); + }; } -#endif // KEYFRAMEVIEW_H +#endif // KEYFRAMEVIEWBASE_H diff --git a/app/widget/keyframeview/keyframeviewbase.cpp b/app/widget/keyframeview/keyframeviewbase.cpp deleted file mode 100644 index 5d0384708..000000000 --- a/app/widget/keyframeview/keyframeviewbase.cpp +++ /dev/null @@ -1,623 +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 "keyframeviewbase.h" - -#include -#include -#include - -#include "dialog/keyframeproperties/keyframeproperties.h" -#include "keyframeviewundo.h" -#include "node/node.h" -#include "widget/menu/menu.h" -#include "widget/menu/menushared.h" -#include "widget/nodeparamview/nodeparamviewundo.h" - -namespace olive { - -KeyframeViewBase::KeyframeViewBase(QWidget *parent) : - TimeBasedView(parent), - dragging_bezier_point_(nullptr), - currently_autoselecting_(false), - dragging_(false) -{ - SetDefaultDragMode(RubberBandDrag); - setContextMenuPolicy(Qt::CustomContextMenu); - - connect(this, &KeyframeViewBase::customContextMenuRequested, this, &KeyframeViewBase::ShowContextMenu); - connect(scene(), &QGraphicsScene::selectionChanged, this, &KeyframeViewBase::AutoSelectKeyTimeNeighbors); -} - -void KeyframeViewBase::Clear() -{ - QMap::iterator iterator; - - for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { - delete iterator.value(); - } - - item_map_.clear(); -} - -void KeyframeViewBase::DeleteSelected() -{ - MultiUndoCommand* command = new MultiUndoCommand(); - - QMap::const_iterator i; - - for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { - if (i.value()->isSelected()) { - command->add_child(new NodeParamRemoveKeyframeCommand(i.key())); - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); -} - -void KeyframeViewBase::AddKeyframesOfNode(Node *n) -{ - foreach (const QString& i, n->inputs()) { - AddKeyframesOfInput(n, i); - } -} - -void KeyframeViewBase::AddKeyframesOfInput(Node* n, const QString& input) -{ - if (!n->IsInputKeyframable(input)) { - return; - } - - int arr_sz = n->InputArraySize(input); - for (int i=-1; i& tracks = input.node()->GetKeyframeTracks(input); - - for (int i=0; i& tracks = ref.input().node()->GetKeyframeTracks(ref.input()); - const NodeKeyframeTrack& t = tracks.at(ref.track()); - - foreach (NodeKeyframe* key, t) { - AddKeyframe(key); - } -} - -void KeyframeViewBase::RemoveKeyframesOfNode(Node *n) -{ - foreach (const QString& i, n->inputs()) { - RemoveKeyframesOfInput(n, i); - } -} - -void KeyframeViewBase::RemoveKeyframesOfInput(Node* n, const QString& input) -{ - if (!n->IsInputKeyframable(input)) { - return; - } - - int arr_sz = n->InputArraySize(input); - for (int i=-1; i& tracks = input.node()->GetKeyframeTracks(input); - - for (int i=0; i& tracks = ref.input().node()->GetKeyframeTracks(ref.input()); - const NodeKeyframeTrack& t = tracks.at(ref.track()); - - foreach (NodeKeyframe* key, t) { - RemoveKeyframe(key); - } -} - -void KeyframeViewBase::SelectAll() -{ - for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { - it.value()->setSelected(true); - } -} - -void KeyframeViewBase::DeselectAll() -{ - for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { - it.value()->setSelected(false); - } -} - -void KeyframeViewBase::RemoveKeyframe(NodeKeyframe* key) -{ - KeyframeAboutToBeRemoved(key); - - delete item_map_.take(key); -} - -KeyframeViewItem *KeyframeViewBase::AddKeyframe(NodeKeyframe* key) -{ - KeyframeViewItem* item = item_map_.value(key); - - if (!item) { - item = new KeyframeViewItem(key); - item->SetTimeTarget(GetTimeTarget()); - item->SetScale(GetScale()); - item_map_.insert(key, item); - scene()->addItem(item); - } - - return item; -} - -void KeyframeViewBase::mousePressEvent(QMouseEvent *event) -{ - QGraphicsItem* item_under_cursor = itemAt(event->pos()); - - if (HandPress(event) || (!item_under_cursor && PlayheadPress(event))) { - return; - } - - active_tool_ = Core::instance()->tool(); - - if (event->button() == Qt::LeftButton) { - QGraphicsView::mousePressEvent(event); - - if (active_tool_ == Tool::kPointer) { - if (item_under_cursor) { - - dragging_ = true; - drag_start_ = mapToScene(event->pos()); - - // Determine what type of item is under the cursor - dragging_bezier_point_ = dynamic_cast(item_under_cursor); - - if (dragging_bezier_point_) { - - dragging_bezier_point_start_ = dragging_bezier_point_->GetCorrespondingKeyframeHandle(); - dragging_bezier_point_opposing_start_ = dragging_bezier_point_->key()->bezier_control(NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode())); - - } else { - - QList selected_items = scene()->selectedItems(); - - selected_keys_.resize(selected_items.size()); - - initial_drag_item_ = static_cast(item_under_cursor); - - for (int i=0;i(selected_items.at(i)); - - selected_keys_.replace(i, {key, - key->x(), - GetAdjustedTime(key->key()->parent(), GetTimeTarget(), key->key()->time(), false), - key->key()->value().toDouble()}); - } - } - } - } - } -} - -void KeyframeViewBase::mouseMoveEvent(QMouseEvent *event) -{ - if (HandMove(event) || PlayheadMove(event)) { - return; - } - - if (event->buttons() & Qt::LeftButton) { - QGraphicsView::mouseMoveEvent(event); - - if (dragging_) { - // Calculate cursor difference and scale it - QPointF mouse_diff_scaled = GetScaledCursorPos(mapToScene(event->pos()) - drag_start_); - - if (event->modifiers() & Qt::ShiftModifier) { - // If holding shift, only move one axis - mouse_diff_scaled.setY(0); - } - - if (dragging_bezier_point_) { - - // Flip the mouse Y because bezier control points are drawn bottom to top, not top to bottom - mouse_diff_scaled.setY(-mouse_diff_scaled.y()); - - QPointF new_bezier_pos = GenerateBezierControlPosition(dragging_bezier_point_->mode(), - dragging_bezier_point_start_, - mouse_diff_scaled); - - // If the user is NOT holding control, we set the other handle to the exact negative of this handle - QPointF new_opposing_pos; - NodeKeyframe::BezierType opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode()); - - - if (!(event->modifiers() & Qt::ControlModifier)) { - new_opposing_pos = GenerateBezierControlPosition(opposing_type, - dragging_bezier_point_opposing_start_, - -mouse_diff_scaled); - } else { - new_opposing_pos = dragging_bezier_point_opposing_start_; - } - - dragging_bezier_point_->key()->set_bezier_control(dragging_bezier_point_->mode(), - new_bezier_pos); - - dragging_bezier_point_->key()->set_bezier_control(opposing_type, - new_opposing_pos); - - // Bezier control points are parented to keyframe items making their positions relative - // to those items. We need to map them to the scene coordinates for this to work properly. - QPointF bezier_pos = dragging_bezier_point_->pos() + dragging_bezier_point_->parentItem()->pos(); - emit Dragged(qRound(bezier_pos.x()), qRound(bezier_pos.y())); - - } else if (!selected_keys_.isEmpty()) { - - // Validate movement - ensure no keyframe goes above its max point or below its min point - FloatSlider::DisplayType display_type = FloatSlider::kNormal; - - if (IsYAxisEnabled()) { - foreach (const KeyframeItemAndTime& keypair, selected_keys_) { - Node* node = keypair.key->key()->parent(); - const QString& input = keypair.key->key()->input(); - double new_val = keypair.value - mouse_diff_scaled.y(); - double limited = new_val; - - if (node->HasInputProperty(input, QStringLiteral("min"))) { - limited = qMax(limited, node->GetInputProperty(input, QStringLiteral("min")).toDouble()); - } - - if (node->HasInputProperty(input, QStringLiteral("max"))) { - limited = qMin(limited, node->GetInputProperty(input, QStringLiteral("max")).toDouble()); - } - - if (limited != new_val) { - mouse_diff_scaled.setY(keypair.value - limited); - } - } - - Node* initial_drag_input = initial_drag_item_->key()->parent(); - const QString& initial_drag_input_id = initial_drag_item_->key()->input(); - if (initial_drag_input->HasInputProperty(initial_drag_input_id, QStringLiteral("view"))) { - display_type = static_cast(initial_drag_input->GetInputProperty(initial_drag_input_id, QStringLiteral("view")).toInt()); - } - } - - foreach (const KeyframeItemAndTime& keypair, selected_keys_) { - rational node_time = GetAdjustedTime(GetTimeTarget(), - keypair.key->key()->parent(), - CalculateNewTimeFromScreen(keypair.time, mouse_diff_scaled.x()), - true); - - keypair.key->key()->set_time(node_time); - - if (IsYAxisEnabled()) { - keypair.key->key()->set_value(keypair.value - mouse_diff_scaled.y()); - } - } - - // Show information about this keyframe - QString tip = Timecode::time_to_timecode(initial_drag_item_->key()->time(), timebase(), - Core::instance()->GetTimecodeDisplay(), false); - - if (IsYAxisEnabled()) { - bool ok; - double num_value = initial_drag_item_->key()->value().toDouble(&ok); - - if (ok) { - tip.append('\n'); - tip.append(FloatSlider::ValueToString(num_value, display_type, 2, true)); - } - - // Force viewport to update since Qt might try to optimize it out if the keyframe is - // offscreen - viewport()->update(); - } - - QToolTip::hideText(); - QToolTip::showText(QCursor::pos(), tip); - - emit Dragged(qRound(initial_drag_item_->x()), qRound(initial_drag_item_->y())); - - } - } - } -} - -void KeyframeViewBase::mouseReleaseEvent(QMouseEvent *event) -{ - if (HandRelease(event) || PlayheadRelease(event)) { - return; - } - - if (event->button() == Qt::LeftButton) { - QGraphicsView::mouseReleaseEvent(event); - - if (dragging_) { - if (dragging_bezier_point_) { - MultiUndoCommand* command = new MultiUndoCommand(); - - // Create undo command with the current bezier point and the old one - command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(), - dragging_bezier_point_->mode(), - dragging_bezier_point_->key()->bezier_control(dragging_bezier_point_->mode()), - dragging_bezier_point_start_)); - - if (!(event->modifiers() & Qt::ControlModifier)) { - auto opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_point_->mode()); - - command->add_child(new KeyframeSetBezierControlPoint(dragging_bezier_point_->key(), - opposing_type, - dragging_bezier_point_->key()->bezier_control(opposing_type), - dragging_bezier_point_opposing_start_)); - } - - dragging_bezier_point_ = nullptr; - - Core::instance()->undo_stack()->push(command); - } else if (!selected_keys_.isEmpty()) { - MultiUndoCommand* command = new MultiUndoCommand(); - - foreach (const KeyframeItemAndTime& keypair, selected_keys_) { - NodeKeyframe* item = keypair.key->key(); - - // Commit movement - command->add_child(new NodeParamSetKeyframeTimeCommand(item, - item->time(), - keypair.time)); - - // Commit value if we're setting a value - if (IsYAxisEnabled()) { - command->add_child(new NodeParamSetKeyframeValueCommand(item, - item->value(), - keypair.value)); - } - } - - Core::instance()->undo_stack()->push(command); - } - - selected_keys_.clear(); - - dragging_ = false; - - QToolTip::hideText(); - } - } -} - -void KeyframeViewBase::ScaleChangedEvent(const double &scale) -{ - TimeBasedView::ScaleChangedEvent(scale); - - for (auto iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { - iterator.value()->SetScale(scale); - } -} - -const QMap &KeyframeViewBase::item_map() const -{ - return item_map_; -} - -void KeyframeViewBase::KeyframeAboutToBeRemoved(NodeKeyframe *) -{ -} - -void KeyframeViewBase::TimeTargetChangedEvent(Node *target) -{ - QMap::const_iterator i; - - for (i=item_map_.begin();i!=item_map_.end();i++) { - i.value()->SetTimeTarget(target); - } -} - -void KeyframeViewBase::ContextMenuEvent(Menu& m) -{ - Q_UNUSED(m) -} - -rational KeyframeViewBase::CalculateNewTimeFromScreen(const rational &old_time, double cursor_diff) -{ - return rational::fromDouble(old_time.toDouble() + cursor_diff); -} - -QPointF KeyframeViewBase::GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, const QPointF &start_point, const QPointF &scaled_cursor_diff) -{ - QPointF new_bezier_pos = start_point; - - new_bezier_pos += scaled_cursor_diff; - - // LIMIT bezier handles from overlapping each other - if (mode == NodeKeyframe::kInHandle) { - if (new_bezier_pos.x() > 0) { - new_bezier_pos.setX(0); - } - } else { - if (new_bezier_pos.x() < 0) { - new_bezier_pos.setX(0); - } - } - - return new_bezier_pos; -} - -QPointF KeyframeViewBase::GetScaledCursorPos(const QPointF &cursor_pos) -{ - return QPointF(cursor_pos.x() / GetScale(), - cursor_pos.y() / GetYScale()); -} - -void KeyframeViewBase::ShowContextMenu() -{ - Menu m; - - MenuShared::instance()->AddItemsForEditMenu(&m, false); - - QAction* linear_key_action = nullptr; - QAction* bezier_key_action = nullptr; - QAction* hold_key_action = nullptr; - - QList items = scene()->selectedItems(); - if (!items.isEmpty()) { - bool all_keys_are_same_type = true; - NodeKeyframe::Type type = static_cast(items.first())->key()->type(); - - for (int i=1;i(items.at(i)); - KeyframeViewItem* prev_item = static_cast(items.at(i-1)); - - if (key_item->key()->type() != prev_item->key()->type()) { - all_keys_are_same_type = false; - break; - } - } - - m.addSeparator(); - - linear_key_action = m.addAction(tr("Linear")); - bezier_key_action = m.addAction(tr("Bezier")); - hold_key_action = m.addAction(tr("Hold")); - - if (all_keys_are_same_type) { - switch (type) { - case NodeKeyframe::kLinear: - linear_key_action->setChecked(true); - break; - case NodeKeyframe::kBezier: - bezier_key_action->setChecked(true); - break; - case NodeKeyframe::kHold: - hold_key_action->setChecked(true); - break; - } - } - } - - m.addSeparator(); - - AddSetScrollZoomsByDefaultActionToMenu(&m); - - m.addSeparator(); - - ContextMenuEvent(m); - - if (!items.isEmpty()) { - m.addSeparator(); - - QAction* properties_action = m.addAction(tr("P&roperties")); - connect(properties_action, &QAction::triggered, this, &KeyframeViewBase::ShowKeyframePropertiesDialog); - } - - QAction* selected = m.exec(QCursor::pos()); - - // Process keyframe type changes - if (!items.isEmpty()) { - if (selected == linear_key_action - || selected == bezier_key_action - || selected == hold_key_action) { - NodeKeyframe::Type new_type; - - if (selected == hold_key_action) { - new_type = NodeKeyframe::kHold; - } else if (selected == bezier_key_action) { - new_type = NodeKeyframe::kBezier; - } else { - new_type = NodeKeyframe::kLinear; - } - - MultiUndoCommand* command = new MultiUndoCommand(); - foreach (QGraphicsItem* item, items) { - command->add_child(new KeyframeSetTypeCommand(static_cast(item)->key(), - new_type)); - } - Core::instance()->undo_stack()->pushIfHasChildren(command); - } - } -} - -void KeyframeViewBase::ShowKeyframePropertiesDialog() -{ - QList items = scene()->selectedItems(); - QVector keys; - - foreach (QGraphicsItem* item, items) { - keys.append(static_cast(item)->key()); - } - - if (!keys.isEmpty()) { - KeyframePropertiesDialog kd(keys, timebase(), this); - kd.exec(); - } -} - -void KeyframeViewBase::AutoSelectKeyTimeNeighbors() -{ - if (currently_autoselecting_ || IsYAxisEnabled()) { - return; - } - - // Prevents infinite loop - currently_autoselecting_ = true; - - QList selected_items = scene()->selectedItems(); - - foreach (QGraphicsItem* g, selected_items) { - KeyframeViewItem* key_item = static_cast(g); - - rational key_time = key_item->key()->time(); - - QVector keys = key_item->key()->parent()->GetKeyframesAtTime(key_item->key()->input(), key_time, key_item->key()->element()); - - foreach (NodeKeyframe* k, keys) { - if (k == key_item->key()) { - continue; - } - - // Ensure this key is not already selected - KeyframeViewItem* item = item_map_.value(k); - - if (item) { - item->setSelected(true); - } - } - } - - currently_autoselecting_ = false; -} - -} diff --git a/app/widget/keyframeview/keyframeviewbase.h b/app/widget/keyframeview/keyframeviewbase.h deleted file mode 100644 index 55b6ecf17..000000000 --- a/app/widget/keyframeview/keyframeviewbase.h +++ /dev/null @@ -1,136 +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 . - -***/ - -#ifndef KEYFRAMEVIEWBASE_H -#define KEYFRAMEVIEWBASE_H - -#include "keyframeviewitem.h" -#include "node/keyframe.h" -#include "widget/curvewidget/beziercontrolpointitem.h" -#include "widget/menu/menu.h" -#include "widget/timebased/timebasedview.h" -#include "widget/timetarget/timetarget.h" - -namespace olive { - -class KeyframeViewBase : public TimeBasedView, public TimeTargetObject -{ - Q_OBJECT -public: - KeyframeViewBase(QWidget* parent = nullptr); - - virtual void Clear(); - - void DeleteSelected(); - - void AddKeyframesOfNode(Node* n); - - void AddKeyframesOfInput(Node *n, const QString &input); - - void AddKeyframesOfElement(const NodeInput &input); - - void AddKeyframesOfTrack(const NodeKeyframeTrackReference &ref); - - void RemoveKeyframesOfNode(Node* n); - - void RemoveKeyframesOfInput(Node *n, const QString &input); - - void RemoveKeyframesOfElement(const NodeInput &input); - - void RemoveKeyframesOfTrack(const NodeKeyframeTrackReference &ref); - - void SelectAll(); - - void DeselectAll(); - -signals: - void Dragged(int current_x, int current_y); - -public slots: - virtual KeyframeViewItem* AddKeyframe(NodeKeyframe* key); - - void RemoveKeyframe(NodeKeyframe* key); - -protected: - virtual void mousePressEvent(QMouseEvent *event) override; - virtual void mouseMoveEvent(QMouseEvent *event) override; - virtual void mouseReleaseEvent(QMouseEvent *event) override; - - virtual void ScaleChangedEvent(const double& scale) override; - - const QMap& item_map() const; - - virtual void KeyframeAboutToBeRemoved(NodeKeyframe* key); - - virtual void TimeTargetChangedEvent(Node*) override; - - virtual void ContextMenuEvent(Menu &m); - - bool IsDragging() const - { - return dragging_; - } - -private: - rational CalculateNewTimeFromScreen(const rational& old_time, double cursor_diff); - - static QPointF GenerateBezierControlPosition(const NodeKeyframe::BezierType mode, - const QPointF& start_point, - const QPointF& scaled_cursor_diff); - - QPointF GetScaledCursorPos(const QPointF &cursor_pos); - - struct KeyframeItemAndTime { - KeyframeViewItem* key; - qreal item_x; - rational time; - double value; - }; - - QMap item_map_; - - Tool::Item active_tool_; - - QPointF drag_start_; - - BezierControlPointItem* dragging_bezier_point_; - QPointF dragging_bezier_point_start_; - QPointF dragging_bezier_point_opposing_start_; - - KeyframeViewItem* initial_drag_item_; - - QVector selected_keys_; - - bool currently_autoselecting_; - - bool dragging_; - -private slots: - void ShowContextMenu(); - - void ShowKeyframePropertiesDialog(); - - void AutoSelectKeyTimeNeighbors(); - -}; - -} - -#endif // KEYFRAMEVIEWBASE_H diff --git a/app/widget/keyframeview/keyframeviewinputconnection.cpp b/app/widget/keyframeview/keyframeviewinputconnection.cpp new file mode 100644 index 000000000..c0d2c3f31 --- /dev/null +++ b/app/widget/keyframeview/keyframeviewinputconnection.cpp @@ -0,0 +1,100 @@ +/*** + + 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 "keyframeviewinputconnection.h" + +#include "keyframeview.h" + +namespace olive { + +KeyframeViewInputConnection::KeyframeViewInputConnection(const NodeKeyframeTrackReference &input, KeyframeView *parent) : + QObject(parent), + keyframe_view_(parent), + input_(input), + y_(0), + y_behavior_(kSingleRow), + brush_(Qt::white) +{ + Node *n = input.input().node(); + + connect(n, &Node::KeyframeAdded, this, &KeyframeViewInputConnection::AddKeyframe); + connect(n, &Node::KeyframeRemoved, this, &KeyframeViewInputConnection::RemoveKeyframe); + connect(n, &Node::KeyframeTimeChanged, this, &KeyframeViewInputConnection::KeyframeChanged); + connect(n, &Node::KeyframeTypeChanged, this, &KeyframeViewInputConnection::KeyframeChanged); + connect(n, &Node::KeyframeTypeChanged, this, &KeyframeViewInputConnection::KeyframeTypeChanged); + connect(n, &Node::KeyframeValueChanged, this, &KeyframeViewInputConnection::KeyframeChanged); +} + +void KeyframeViewInputConnection::SetKeyframeY(int y) +{ + if (y_ != y) { + y_ = y; + + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::SetYBehavior(YBehavior e) +{ + if (y_behavior_ != e) { + y_behavior_ = e; + + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::SetBrush(const QBrush &brush) +{ + if (brush_ != brush) { + brush_ = brush; + + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::AddKeyframe(NodeKeyframe *key) +{ + if (key->key_track_ref() == input_) { + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::RemoveKeyframe(NodeKeyframe *key) +{ + if (key->key_track_ref() == input_) { + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::KeyframeChanged(NodeKeyframe *key) +{ + if (key->key_track_ref() == input_) { + emit RequireUpdate(); + } +} + +void KeyframeViewInputConnection::KeyframeTypeChanged(NodeKeyframe *key) +{ + if (key->key_track_ref() == input_) { + emit TypeChanged(); + } +} + +} diff --git a/app/widget/keyframeview/keyframeviewinputconnection.h b/app/widget/keyframeview/keyframeviewinputconnection.h new file mode 100644 index 000000000..b6b0c1da5 --- /dev/null +++ b/app/widget/keyframeview/keyframeviewinputconnection.h @@ -0,0 +1,94 @@ +/*** + + 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 KEYFRAMEVIEWINPUTCONNECTION_H +#define KEYFRAMEVIEWINPUTCONNECTION_H + +#include + +#include "node/node.h" +#include "node/param.h" + +namespace olive { + +class KeyframeView; + +class KeyframeViewInputConnection : public QObject +{ + Q_OBJECT +public: + KeyframeViewInputConnection(const NodeKeyframeTrackReference &input, KeyframeView *parent); + + const int &GetKeyframeY() const + { + return y_; + } + + void SetKeyframeY(int y); + + enum YBehavior { + kSingleRow, + kValueIsHeight + }; + + void SetYBehavior(YBehavior e); + + const QVector GetKeyframes() const + { + return input_.input().node()->GetKeyframeTracks(input_.input()).at(input_.track()); + } + + const QBrush &GetBrush() const + { + return brush_; + } + + void SetBrush(const QBrush &brush); + +signals: + void RequireUpdate(); + + void TypeChanged(); + +private: + KeyframeView *keyframe_view_; + + NodeKeyframeTrackReference input_; + + int y_; + + YBehavior y_behavior_; + + QBrush brush_; + +private slots: + void AddKeyframe(NodeKeyframe *key); + + void RemoveKeyframe(NodeKeyframe *key); + + void KeyframeChanged(NodeKeyframe *key); + + void KeyframeTypeChanged(NodeKeyframe *key); + +}; + +} + +#endif // KEYFRAMEVIEWINPUTCONNECTION_H diff --git a/app/widget/keyframeview/keyframeviewitem.cpp b/app/widget/keyframeview/keyframeviewitem.cpp deleted file mode 100644 index 45becc01b..000000000 --- a/app/widget/keyframeview/keyframeviewitem.cpp +++ /dev/null @@ -1,129 +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 "keyframeviewitem.h" - -#include -#include -#include -#include - -#include "common/qtutils.h" - -namespace olive { - -KeyframeViewItem::KeyframeViewItem(NodeKeyframe* key, QGraphicsItem *parent) : - QGraphicsRectItem(parent), - key_(key), - scale_(1.0), - vert_center_(0), - use_custom_brush_(false) -{ - setFlag(QGraphicsItem::ItemIsSelectable); - - connect(key, &NodeKeyframe::TimeChanged, this, &KeyframeViewItem::UpdatePos); - connect(key, &NodeKeyframe::TypeChanged, this, &KeyframeViewItem::Redraw); - - int keyframe_size = QtUtils::QFontMetricsWidth(qApp->fontMetrics(), "Oi"); - int half_sz = keyframe_size/2; - setRect(-half_sz, -half_sz, keyframe_size, keyframe_size); - - UpdatePos(); - - // Set default brush -} - -void KeyframeViewItem::SetOverrideY(qreal vertical_center) -{ - vert_center_ = vertical_center; - UpdatePos(); -} - -void KeyframeViewItem::SetScale(double scale) -{ - scale_ = scale; - UpdatePos(); -} - -void KeyframeViewItem::SetOverrideBrush(const QBrush &b) -{ - use_custom_brush_ = true; - setBrush(b); -} - -NodeKeyframe* KeyframeViewItem::key() const -{ - return key_; -} - -void KeyframeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) -{ - painter->setRenderHint(QPainter::Antialiasing); - - painter->setPen(Qt::black); - - if (option->state & QStyle::State_Selected) { - painter->setBrush(widget->palette().highlight()); - } else if (use_custom_brush_) { - painter->setBrush(brush()); - } else { - painter->setBrush(widget->palette().text()); - } - - switch (key_->type()) { - case NodeKeyframe::kLinear: - { - QPointF points[] = { - QPointF(rect().center().x(), rect().top()), - QPointF(rect().right(), rect().center().y()), - QPointF(rect().center().x(), rect().bottom()), - QPointF(rect().left(), rect().center().y()) - }; - - painter->drawPolygon(points, 4); - break; - } - case NodeKeyframe::kBezier: - painter->drawEllipse(rect()); - break; - case NodeKeyframe::kHold: - painter->drawRect(rect()); - break; - } -} - -void KeyframeViewItem::TimeTargetChangedEvent(Node *) -{ - UpdatePos(); -} - -void KeyframeViewItem::UpdatePos() -{ - rational adjusted = GetAdjustedTime(key_->parent(), GetTimeTarget(), key_->time(), false); - - setPos(adjusted.toDouble() * scale_, vert_center_); -} - -void KeyframeViewItem::Redraw() -{ - QGraphicsItem::update(); -} - -} diff --git a/app/widget/keyframeview/keyframeviewitem.h b/app/widget/keyframeview/keyframeviewitem.h deleted file mode 100644 index 3f3725dc6..000000000 --- a/app/widget/keyframeview/keyframeviewitem.h +++ /dev/null @@ -1,68 +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 . - -***/ - -#ifndef KEYFRAMEVIEWITEM_H -#define KEYFRAMEVIEWITEM_H - -#include - -#include "node/keyframe.h" -#include "widget/timetarget/timetarget.h" - -namespace olive { - -class KeyframeViewItem : public QObject, public QGraphicsRectItem, public TimeTargetObject -{ - Q_OBJECT -public: - KeyframeViewItem(NodeKeyframe* key, QGraphicsItem *parent = nullptr); - - void SetOverrideY(qreal vertical_center); - - void SetScale(double scale); - - void SetOverrideBrush(const QBrush& b); - - NodeKeyframe* key() const; - -protected: - virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; - - virtual void TimeTargetChangedEvent(Node* ) override; - -private: - NodeKeyframe* key_; - - double scale_; - - qreal vert_center_; - - bool use_custom_brush_; - -private slots: - void UpdatePos(); - - void Redraw(); - -}; - -} - -#endif // KEYFRAMEVIEWITEM_H diff --git a/app/widget/nodeparamview/CMakeLists.txt b/app/widget/nodeparamview/CMakeLists.txt index 5d0f37f40..067ffad1f 100644 --- a/app/widget/nodeparamview/CMakeLists.txt +++ b/app/widget/nodeparamview/CMakeLists.txt @@ -16,23 +16,29 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} - widget/nodeparamview/nodeparamview.h widget/nodeparamview/nodeparamview.cpp - widget/nodeparamview/nodeparamviewarraywidget.h + widget/nodeparamview/nodeparamview.h widget/nodeparamview/nodeparamviewarraywidget.cpp - widget/nodeparamview/nodeparamviewconnectedlabel.h + widget/nodeparamview/nodeparamviewarraywidget.h widget/nodeparamview/nodeparamviewconnectedlabel.cpp - widget/nodeparamview/nodeparamviewdockarea.h + widget/nodeparamview/nodeparamviewconnectedlabel.h + widget/nodeparamview/nodeparamviewcontext.cpp + widget/nodeparamview/nodeparamviewcontext.h widget/nodeparamview/nodeparamviewdockarea.cpp - widget/nodeparamview/nodeparamviewitem.h + widget/nodeparamview/nodeparamviewdockarea.h widget/nodeparamview/nodeparamviewitem.cpp - widget/nodeparamview/nodeparamviewkeyframecontrol.h + widget/nodeparamview/nodeparamviewitem.h + widget/nodeparamview/nodeparamviewitembase.cpp + widget/nodeparamview/nodeparamviewitembase.h + widget/nodeparamview/nodeparamviewitemtitlebar.cpp + widget/nodeparamview/nodeparamviewitemtitlebar.h widget/nodeparamview/nodeparamviewkeyframecontrol.cpp - widget/nodeparamview/nodeparamviewtextedit.h + widget/nodeparamview/nodeparamviewkeyframecontrol.h widget/nodeparamview/nodeparamviewtextedit.cpp - widget/nodeparamview/nodeparamviewundo.h + widget/nodeparamview/nodeparamviewtextedit.h widget/nodeparamview/nodeparamviewundo.cpp - widget/nodeparamview/nodeparamviewwidgetbridge.h + widget/nodeparamview/nodeparamviewundo.h widget/nodeparamview/nodeparamviewwidgetbridge.cpp + widget/nodeparamview/nodeparamviewwidgetbridge.h PARENT_SCOPE ) diff --git a/app/widget/nodeparamview/nodeparamview.cpp b/app/widget/nodeparamview/nodeparamview.cpp index 9d3e77000..68f04e7fd 100644 --- a/app/widget/nodeparamview/nodeparamview.cpp +++ b/app/widget/nodeparamview/nodeparamview.cpp @@ -25,6 +25,7 @@ #include #include +#include "common/functiontimer.h" #include "common/timecodefunctions.h" #include "node/output/viewer/viewer.h" @@ -32,10 +33,13 @@ 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), + ignore_flags_(false) { // Create horizontal layout to place scroll area in (and keyframe editing eventually) QHBoxLayout* layout = new QHBoxLayout(this); @@ -46,25 +50,18 @@ 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_widget_container_ = new QWidget(); + param_scroll_area_->setWidget(param_widget_container_); param_widget_area_ = new NodeParamViewDockArea(); - // Disable dock widgets from tabbing and disable glitchy animations - param_widget_area_->setDockOptions(static_cast(0)); - - // HACK: Hide the main window separators (unfortunately the cursors still appear) - param_widget_area_->setStyleSheet(QStringLiteral("QMainWindow::separator {background: rgba(0, 0, 0, 0)}")); - QVBoxLayout* param_widget_container_layout = new QVBoxLayout(param_widget_container_); QMargins param_widget_margin = param_widget_container_layout->contentsMargins(); param_widget_margin.setTop(ruler()->height()); @@ -74,34 +71,24 @@ 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 contexts for three different types + context_items_.resize(Track::kCount + 1); + for (int i=0; isetVisible(false); - // Create ruler object - keyframe_area_layout->addWidget(ruler()); + NodeParamViewItemTitleBar *title_bar = static_cast(c->titleBarWidget()); - // Create keyframe view - keyframe_view_ = new KeyframeView(); - keyframe_view_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - ConnectTimelineView(keyframe_view_); - keyframe_area_layout->addWidget(keyframe_view_); + if (i == Track::kVideo || i == Track::kAudio) { + title_bar->SetAddEffectButtonVisible(true); + title_bar->SetText(tr("%1 Nodes").arg(Footage::GetStreamTypeName(static_cast(i)))); + } else { + title_bar->SetText(tr("Other")); + } - // 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}); + context_items_[i] = c; + param_widget_area_->AddItem(c); + } // Disable collapsing param view (but collapsing keyframe view is permitted) splitter->setCollapsible(0, false); @@ -112,18 +99,54 @@ NodeParamView::NodeParamView(QWidget *parent) : 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(param_scroll_area_->verticalScrollBar(), &QScrollBar::rangeChanged, vertical_scrollbar_, &QScrollBar::setRange); + connect(param_scroll_area_->verticalScrollBar(), &QScrollBar::rangeChanged, this, &NodeParamView::UpdateGlobalScrollBar); + 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 (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); - connect(keyframe_view_->horizontalScrollBar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll); + // 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}); + + 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); + + // 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); + } else { + keyframe_view_ = nullptr; + } // Set a default scale - FIXME: Hardcoded SetScale(120); @@ -135,8 +158,14 @@ NodeParamView::NodeParamView(QWidget *parent) : &NodeParamView::FocusChanged); } -void NodeParamView::SelectNodes(const QVector &nodes) +NodeParamView::~NodeParamView() { + qDeleteAll(context_items_); +} + +/*void NodeParamView::SelectNodes(const QVector &nodes) +{ + return; int original_node_count = items_.size(); foreach (Node* n, nodes) { @@ -149,7 +178,7 @@ void NodeParamView::SelectNodes(const QVector &nodes) active_nodes_.append(n); // Create node UI - AddNode(n); + AddNode(n, param_widget_area_); } if (items_.size() > original_node_count ) { @@ -164,6 +193,7 @@ void NodeParamView::SelectNodes(const QVector &nodes) void NodeParamView::DeselectNodes(const QVector &nodes) { + return; // Remove item from map and delete the widget int original_node_count = items_.size(); @@ -190,6 +220,69 @@ void NodeParamView::DeselectNodes(const QVector &nodes) SignalNodeOrder(); } +}*/ + +void NodeParamView::SetInputChecked(const NodeInput &input, bool e) +{ + input_checked_.insert(input, e); + foreach (NodeParamViewContext *ctx, context_items_) { + ctx->SetInputChecked(input, e); + } +} + +void NodeParamView::SetContexts(const QVector &contexts) +{ + TIME_THIS_FUNCTION; + + foreach (NodeParamViewContext *ctx, context_items_) { + ctx->Clear(); + ctx->setVisible(false); + } + contexts_ = contexts; + + if (keyframe_view_) { + keyframe_view_->Clear(); + } + + if (focused_node_) { + focused_node_ = nullptr; + emit FocusedNodeChanged(nullptr); + } + + foreach (Node *ctx, contexts) { + Track::Type ctx_type = Track::kCount; + + if (ClipBlock *clip = dynamic_cast(ctx)) { + if (clip->track()) { + if (clip->track()->type() != Track::kNone) { + ctx_type = clip->track()->type(); + } + } + } else if (Track *track = dynamic_cast(ctx)) { + if (track->type() != Track::kNone) { + ctx_type = track->type(); + } + } + + NodeParamViewContext *item = context_items_.at(ctx_type); + + item->AddContext(ctx); + item->setVisible(true); + + for (auto it=ctx->GetContextPositions().cbegin(); it!=ctx->GetContextPositions().cend(); it++) { + if (!(it.key()->GetFlags() & Node::kDontShowInParamView) || ignore_flags_) { + AddNode(it.key(), item); + } + } + } + + foreach (NodeParamViewContext *ctx, context_items_) { + SortItemsInContext(ctx); + } + + if (keyframe_view_) { + QueueKeyframePositionUpdate(); + } } void NodeParamView::resizeEvent(QResizeEvent *event) @@ -197,25 +290,27 @@ void NodeParamView::resizeEvent(QResizeEvent *event) super::resizeEvent(event); vertical_scrollbar_->setPageStep(vertical_scrollbar_->height()); - - UpdateGlobalScrollBar(); } 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); + foreach (NodeParamViewContext* ctx, context_items_) { + ctx->SetTimebase(timebase); } UpdateItemTime(GetTime()); @@ -225,34 +320,52 @@ 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_) { + foreach (NodeParamViewContext* item, context_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::SelectNodes(const QVector &nodes) +{ + // Do nothing, this is a placeholder if we ever need this to do anything in the future +} + +void NodeParamView::DeselectNodes(const QVector &nodes) +{ + // Do nothing, this is a placeholder if we ever need this to do anything in the future } void NodeParamView::UpdateItemTime(const rational &time) { - foreach (NodeParamViewItem* item, items_) { + foreach (NodeParamViewContext* item, context_items_) { item->SetTime(time); } } @@ -262,104 +375,102 @@ void NodeParamView::QueueKeyframePositionUpdate() QMetaObject::invokeMethod(this, &NodeParamView::UpdateElementY, Qt::QueuedConnection); } -void NodeParamView::SignalNodeOrder() +void NodeParamView::AddNode(Node *n, NodeParamViewContext *context) { - // Sort by item Y (apparently there's no way in Qt to get the order of dock widgets) - QVector nodes; - QVector item_ys; + NodeParamViewItem* item = new NodeParamViewItem(n, create_checkboxes_, context); - for (auto it=items_.cbegin(); it!=items_.cend(); it++) { - int item_y = it.value()->pos().y(); + connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); + connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); + connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); + 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()); + } + } + } + + item->SetTimeTarget(GetTimeTarget()); + item->SetTimebase(timebase()); + item->SetTime(GetTime()); + + context->AddNode(item); + + if (!focused_node_ && n->HasGizmos()) { + // We'll focus this node now + item->SetHighlighted(true); + focused_node_ = item; + emit FocusedNodeChanged(n); + } + + if (keyframe_view_) { + connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); + connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate); + + item->SetKeyframeConnections(keyframe_view_->AddKeyframesOfNode(n)); + } +} + +int GetDistanceBetweenNodes(Node *start, Node *end) +{ + if (start == end) { + return 0; + } + + for (auto it=start->input_connections().cbegin(); it!=start->input_connections().cend(); it++) { + int this_node_dist = GetDistanceBetweenNodes(it->second, end); + if (this_node_dist != -1) { + return 1 + this_node_dist; + } + } + + return -1; +} + +void NodeParamView::SortItemsInContext(NodeParamViewContext *context_item) +{ + QVector > distances; + + for (auto it=context_item->GetItems().cbegin(); it!=context_item->GetItems().cend(); it++) { + int distance = -1; + foreach (Node *ctx, context_item->GetContexts()) { + distance = qMax(distance, GetDistanceBetweenNodes(ctx, it.key())); + } + + if (distance == -1) { + distance = INT_MAX; + } bool inserted = false; + QPair dist(it.value(), distance); - for (int i=0; i item_y) { - item_ys.insert(i, item_y); - nodes.insert(i, it.key()); + for (int i=0; isetAllowedAreas(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); - - connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal); - connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode); - connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::QueueKeyframePositionUpdate); - connect(item, &NodeParamViewItem::dockLocationChanged, this, &NodeParamView::SignalNodeOrder); - connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode); - connect(item, &NodeParamViewItem::ArrayExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); - connect(item, &NodeParamViewItem::ExpandedChanged, this, &NodeParamView::QueueKeyframePositionUpdate); - connect(item, &NodeParamViewItem::Moved, this, &NodeParamView::QueueKeyframePositionUpdate); - - // Set time target - item->SetTimeTarget(GetTimeTarget()); - - // Set the timebase - item->SetTimebase(timebase()); - - items_.insert(n, item); - param_widget_area_->addDockWidget(Qt::LeftDockWidgetArea, item); - - if (!focused_node_ && n->HasGizmos()) { - // We'll focus this node now - item->SetHighlighted(true); - focused_node_ = n; - emit FocusedNodeChanged(focused_node_); - } - - keyframe_view_->AddKeyframesOfNode(n); -} - -void NodeParamView::RemoveNode(Node *n) -{ - keyframe_view_->RemoveKeyframesOfNode(n); - - disconnect(n, &Node::KeyframeAdded, keyframe_view_, &KeyframeView::AddKeyframe); - disconnect(n, &Node::KeyframeRemoved, keyframe_view_, &KeyframeView::RemoveKeyframe); - - delete items_.take(n); - - if (focused_node_ == n) { - // Try to find new node with gizmos to focus - focused_node_ = nullptr; - for (auto it=items_.cbegin(); it!=items_.cend(); it++) { - if (it.key()->HasGizmos()) { - focused_node_ = it.key(); - it.value()->SetHighlighted(true); - break; - } - } - - emit FocusedNodeChanged(focused_node_); + foreach (auto info, distances) { + context_item->GetDockArea()->AddItem(info.first); } } 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(param_widget_container_->height() - ruler()->height()); + } } void NodeParamView::PinNode(bool pin) @@ -373,8 +484,7 @@ void NodeParamView::PinNode(bool pin) pinned_nodes_.removeOne(node); if (!active_nodes_.contains(node)) { - RemoveNode(node); - SignalNodeOrder(); + //RemoveNode(node); } } } @@ -384,26 +494,36 @@ void NodeParamView::FocusChanged(QWidget* old, QWidget* now) Q_UNUSED(old) QObject* parent = now; - NodeParamViewItem* item; while (parent) { - item = dynamic_cast(parent); + if (NodeParamViewItem* item = dynamic_cast(parent)) { + if (item != focused_node_) { + // Found a NodeParamViewItem that isn't already focused, see if it belongs to us + bool ours = false; - if (item) { - // Found it! - if (item->GetNode() != focused_node_) { - if (focused_node_) { - // De-focus current node - items_.value(focused_node_)->SetHighlighted(false); + do { + parent = parent->parent(); + + if (parent == this) { + ours = true; + break; + } + } while (parent); + + if (ours) { + // This item is ours, + if (focused_node_) { + // De-focus current node + focused_node_->SetHighlighted(false); + } + + focused_node_ = item; + + item->SetHighlighted(true); + + emit FocusedNodeChanged(item->GetNode()); } - - focused_node_ = item->GetNode(); - - item->SetHighlighted(true); - - emit FocusedNodeChanged(focused_node_); } - break; } @@ -421,15 +541,34 @@ 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); + foreach (NodeParamViewContext *ctx, context_items_) { + for (auto it=ctx->GetItems().cbegin(); it!=ctx->GetItems().cend(); it++) { + const KeyframeView::NodeConnections &connections = it.value()->GetKeyframeConnections(); - for (int i=-1; iinputs()) { + if (!(it.key()->GetInputFlags(input) & kInputFlagHidden)) { + int arr_sz = it.key()->InputArraySize(input); - int y = it.value()->GetElementY(ic); - keyframe_view_->SetElementY(ic, y); + for (int i=-1; iGetElementY(ic); + + // For some reason Qt's mapToGlobal doesn't seem to handle this, so we offset here + y += vertical_scrollbar_->value(); + + const KeyframeView::InputConnections &input_con = connections.value(input); + int use_index = i + 1; + if (use_index < input_con.size()) { + const KeyframeView::ElementConnections &ele_con = input_con.at(ic.element()+1); + foreach (KeyframeViewInputConnection *track, ele_con) { + track->SetKeyframeY(y); + } + } + } + } + } } } } diff --git a/app/widget/nodeparamview/nodeparamview.h b/app/widget/nodeparamview/nodeparamview.h index 1718f445e..b36e31cc0 100644 --- a/app/widget/nodeparamview/nodeparamview.h +++ b/app/widget/nodeparamview/nodeparamview.h @@ -25,6 +25,7 @@ #include #include "node/node.h" +#include "nodeparamviewcontext.h" #include "nodeparamviewdockarea.h" #include "nodeparamviewitem.h" #include "widget/keyframeview/keyframeview.h" @@ -32,40 +33,26 @@ namespace olive { -class NodeParamViewParamContainer : public QWidget -{ - Q_OBJECT -public: - NodeParamViewParamContainer(QWidget* parent = nullptr) : - QWidget(parent) - { - } - -protected: - virtual void resizeEvent(QResizeEvent *event) override - { - QWidget::resizeEvent(event); - - emit Resized(event->size().height()); - } - -signals: - void Resized(int new_height); - -}; - class NodeParamView : public TimeBasedWidget { Q_OBJECT public: - NodeParamView(QWidget* parent = nullptr); - - void SelectNodes(const QVector &nodes); - void DeselectNodes(const QVector& nodes); - - const QMap& GetItemMap() const + NodeParamView(bool create_keyframe_view, QWidget* parent = nullptr); + NodeParamView(QWidget* parent = nullptr) : + NodeParamView(true, parent) { - return items_; + } + + virtual ~NodeParamView() override; + + void SetCreateCheckBoxes(NodeParamViewCheckBoxBehavior e) + { + create_checkboxes_ = e; + } + + bool IsInputChecked(const NodeInput &input) const + { + return input_checked_.value(input); } Node* GetTimeTarget() const; @@ -82,11 +69,27 @@ public: keyframe_view_->DeselectAll(); } + void SetIgnoreNodeFlags(bool e) + { + ignore_flags_ = e; + } + + void SelectNodes(const QVector &nodes); + void DeselectNodes(const QVector &nodes); + + const QVector &GetContexts() const + { + return contexts_; + } + +public slots: + void SetInputChecked(const NodeInput &input, bool e); + + void SetContexts(const QVector &contexts); + signals: void RequestSelectNode(const QVector& target); - void NodeOrderChanged(const QVector& nodes); - void FocusedNodeChanged(Node* n); protected: @@ -103,33 +106,39 @@ private: void QueueKeyframePositionUpdate(); - void SignalNodeOrder(); + void AddNode(Node* n, NodeParamViewContext *context); - void AddNode(Node* n); - - void RemoveNode(Node* n); + void SortItemsInContext(NodeParamViewContext *context); KeyframeView* keyframe_view_; - QMap items_; + QVector context_items_; QScrollBar* vertical_scrollbar_; int last_scroll_val_; - NodeParamViewParamContainer* param_widget_container_; + QScrollArea* param_scroll_area_; + + QWidget* 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_; QVector active_nodes_; - QMap node_expanded_state_; + NodeParamViewItem* focused_node_; - Node* focused_node_; + NodeParamViewCheckBoxBehavior create_checkboxes_; + + Node *time_target_; + + QHash input_checked_; + + bool ignore_flags_; + + QVector contexts_; private slots: void UpdateGlobalScrollBar(); diff --git a/app/widget/nodeparamview/nodeparamviewcontext.cpp b/app/widget/nodeparamview/nodeparamviewcontext.cpp new file mode 100644 index 000000000..b55081014 --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewcontext.cpp @@ -0,0 +1,101 @@ +/*** + + 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 "nodeparamviewcontext.h" + +#include + +#include "node/block/clip/clip.h" + +namespace olive { + +#define super NodeParamViewItemBase + +NodeParamViewContext::NodeParamViewContext(QWidget *parent) : + super(parent) +{ + QWidget *body = new QWidget(); + QHBoxLayout *body_layout = new QHBoxLayout(body); + SetBody(body); + + dock_area_ = new NodeParamViewDockArea(); + body_layout->addWidget(dock_area_); + + setBackgroundRole(QPalette::Base); + + Retranslate(); + + connect(title_bar(), &NodeParamViewItemTitleBar::AddEffectButtonClicked, this, &NodeParamViewContext::AddEffectButtonClicked); +} + +void NodeParamViewContext::AddNode(NodeParamViewItem *item) +{ + items_.insert(item->GetNode(), item); + dock_area_->AddItem(item); +} + +void NodeParamViewContext::RemoveNode(Node *node) +{ +} + +void NodeParamViewContext::Clear() +{ + qDeleteAll(items_); + items_.clear(); +} + +void NodeParamViewContext::SetInputChecked(const NodeInput &input, bool e) +{ + if (NodeParamViewItem *item = items_.value(input.node())) { + item->SetInputChecked(input, e); + } +} + +void NodeParamViewContext::SetTimebase(const rational &timebase) +{ + foreach (NodeParamViewItem* item, items_) { + item->SetTimebase(timebase); + } +} + +void NodeParamViewContext::SetTimeTarget(Node *n) +{ + foreach (NodeParamViewItem* item, items_) { + item->SetTimeTarget(n); + } +} + +void NodeParamViewContext::SetTime(const rational &time) +{ + foreach (NodeParamViewItem* item, items_) { + item->SetTime(time); + } +} + +void NodeParamViewContext::Retranslate() +{ +} + +void NodeParamViewContext::AddEffectButtonClicked() +{ + QMessageBox::information(this, tr("STUB"), tr("This feature is coming soon. Thanks for testing development builds of Olive :)")); +} + +} diff --git a/app/widget/nodeparamview/nodeparamviewcontext.h b/app/widget/nodeparamview/nodeparamviewcontext.h new file mode 100644 index 000000000..a8590271e --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewcontext.h @@ -0,0 +1,93 @@ +/*** + + 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 NODEPARAMVIEWCONTEXT_H +#define NODEPARAMVIEWCONTEXT_H + +#include "nodeparamviewdockarea.h" +#include "nodeparamviewitembase.h" +#include "nodeparamviewitem.h" + +namespace olive { + +class NodeParamViewContext : public NodeParamViewItemBase +{ + Q_OBJECT +public: + NodeParamViewContext(QWidget *parent = nullptr); + + NodeParamViewDockArea *GetDockArea() const + { + return dock_area_; + } + + const QVector &GetContexts() const + { + return contexts_; + } + + const QMap &GetItems() const + { + return items_; + } + + void AddNode(NodeParamViewItem *item); + + void RemoveNode(Node *node); + + void Clear(); + + void SetInputChecked(const NodeInput &input, bool e); + + void SetTimebase(const rational &timebase); + + void SetTimeTarget(Node *n); + + void SetTime(const rational &time); + +public slots: + void AddContext(Node *node) + { + contexts_.append(node); + } + + void RemoveContext(Node *node) + { + contexts_.removeOne(node); + } + +protected slots: + virtual void Retranslate() override; + +private: + NodeParamViewDockArea *dock_area_; + + QVector contexts_; + + QMap items_; + +private slots: + void AddEffectButtonClicked(); + +}; + +} + +#endif // NODEPARAMVIEWCONTEXT_H diff --git a/app/widget/nodeparamview/nodeparamviewdockarea.cpp b/app/widget/nodeparamview/nodeparamviewdockarea.cpp index a875bbf24..7f6c32e0d 100644 --- a/app/widget/nodeparamview/nodeparamviewdockarea.cpp +++ b/app/widget/nodeparamview/nodeparamviewdockarea.cpp @@ -20,11 +20,18 @@ #include "nodeparamviewdockarea.h" +#include + namespace olive { NodeParamViewDockArea::NodeParamViewDockArea(QWidget *parent) : QMainWindow(parent) { + // Disable dock widgets from tabbing and disable glitchy animations + setDockOptions(static_cast(0)); + + // HACK: Hide the main window separators (unfortunately the cursors still appear) + setStyleSheet(QStringLiteral("QMainWindow::separator {background: rgba(0, 0, 0, 0)}")); } QMenu *NodeParamViewDockArea::createPopupMenu() @@ -32,4 +39,11 @@ QMenu *NodeParamViewDockArea::createPopupMenu() return nullptr; } +void NodeParamViewDockArea::AddItem(QDockWidget *item) +{ + item->setAllowedAreas(Qt::LeftDockWidgetArea); + item->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable); + addDockWidget(Qt::LeftDockWidgetArea, item); +} + } diff --git a/app/widget/nodeparamview/nodeparamviewdockarea.h b/app/widget/nodeparamview/nodeparamviewdockarea.h index 90a00c1a0..099eb087c 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 @@ -33,6 +35,8 @@ public: virtual QMenu *createPopupMenu() override; + void AddItem(QDockWidget *item); + }; } diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index 3c2d5bbb0..50e275a0c 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -22,8 +22,6 @@ #include #include -#include -#include #include "common/qtutils.h" #include "core.h" @@ -38,227 +36,105 @@ 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 +#define super NodeParamViewItemBase -NodeParamViewItem::NodeParamViewItem(Node *node, QWidget *parent) : +NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget *parent) : super(parent), - node_(node), - highlighted_(false) + node_(node) { - // Create title bar widget - title_bar_ = new NodeParamViewItemTitleBar(this); - - // Add title bar to widget - this->setTitleBarWidget(title_bar_); + node_->Retranslate(); // 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(title_bar_, &NodeParamViewItemTitleBar::ExpandedStateChanged, this, &NodeParamViewItem::SetExpanded); - connect(title_bar_, &NodeParamViewItemTitleBar::PinToggled, this, &NodeParamViewItem::PinToggled); - - this->setWidget(body_); - - // Use dummy QWidget to retain width when not expanded (QDockWidget seems to ignore the titlebar - // size hints and will shrink as small as possible if the body is hidden) - hidden_body_ = new QWidget(this); + connect(body_, &NodeParamViewItemBody::InputCheckedChanged, this, &NodeParamViewItem::InputCheckedChanged); + SetBody(body_); connect(node_, &Node::LabelChanged, this, &NodeParamViewItem::Retranslate); - setBackgroundRole(QPalette::Base); - setAutoFillBackground(true); - - setFocusPolicy(Qt::ClickFocus); + setBackgroundRole(QPalette::Window); Retranslate(); } -void NodeParamViewItem::SetTimeTarget(Node *target) -{ - body_->SetTimeTarget(target); -} - -void NodeParamViewItem::SetTime(const rational &time) -{ - time_ = time; - - body_->SetTime(time_); -} - -void NodeParamViewItem::SetTimebase(const rational& timebase) -{ - body_->SetTimebase(timebase); -} - -Node *NodeParamViewItem::GetNode() const -{ - return node_; -} - -void NodeParamViewItem::changeEvent(QEvent *e) -{ - if (e->type() == QEvent::LanguageChange) { - Retranslate(); - } - - super::changeEvent(e); -} - -void NodeParamViewItem::paintEvent(QPaintEvent *event) -{ - super::paintEvent(event); - - // Draw border if focused - if (highlighted_) { - QPainter p(this); - p.setBrush(Qt::NoBrush); - p.setPen(palette().highlight().color()); - p.drawRect(rect().adjusted(0, 0, -1, -1)); - } -} - -void NodeParamViewItem::moveEvent(QMoveEvent *event) -{ - super::moveEvent(event); - - emit Moved(); -} - void NodeParamViewItem::Retranslate() { node_->Retranslate(); - if (node_->GetLabel().isEmpty()) { - title_bar_->SetText(node_->Name()); - } else { - title_bar_->SetText(tr("%1 (%2)").arg(node_->GetLabel(), node_->Name())); - } + title_bar()->SetText(GetTitleBarTextFromNode(node_)); body_->Retranslate(); } -void NodeParamViewItem::SetExpanded(bool e) -{ - setWidget(e ? body_ : hidden_body_); - title_bar_->SetExpanded(e); - - emit ExpandedChanged(e); -} - -bool NodeParamViewItem::IsExpanded() const -{ - return body_->isVisible(); -} - int NodeParamViewItem::GetElementY(const NodeInput &c) const { if (IsExpanded()) { return body_->GetElementY(c); } else { // Not expanded, put keyframes at the titlebar Y - return mapToGlobal(title_bar_->rect().center()).y(); + return mapToGlobal(title_bar()->rect().center()).y(); } } -void NodeParamViewItem::ToggleExpanded() +void NodeParamViewItem::SetInputChecked(const NodeInput &input, bool e) { - SetExpanded(!IsExpanded()); + body_->SetInputChecked(input, e); } -NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : +NodeParamViewItemBody::NodeParamViewItemBody(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget *parent) : QWidget(parent), - draw_border_(true) -{ - QHBoxLayout* layout = new QHBoxLayout(this); - - collapse_btn_ = new CollapseButton(); - connect(collapse_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::ExpandedStateChanged); - layout->addWidget(collapse_btn_); - - lbl_ = new QLabel(); - layout->addWidget(lbl_); - - // Place next buttons on the far side - layout->addStretch(); - - QPushButton* pin_btn = new QPushButton(QStringLiteral("P")); - pin_btn->setCheckable(true); - pin_btn->setFixedSize(pin_btn->sizeHint().height(), pin_btn->sizeHint().height()); - layout->addWidget(pin_btn); - connect(pin_btn, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled); -} - -void NodeParamViewItemTitleBar::SetExpanded(bool e) -{ - draw_border_ = e; - collapse_btn_->setChecked(e); - - update(); -} - -void NodeParamViewItemTitleBar::paintEvent(QPaintEvent *event) -{ - QWidget::paintEvent(event); - - if (draw_border_) { - QPainter p(this); - - // Draw bottom border using text color - int bottom = height() - 1; - p.setPen(palette().text().color()); - p.drawLine(0, bottom, width(), bottom); - } -} - -void NodeParamViewItemTitleBar::mouseDoubleClickEvent(QMouseEvent *event) -{ - QWidget::mouseDoubleClickEvent(event); - - collapse_btn_->click(); -} - -NodeParamViewItemBody::NodeParamViewItemBody(Node* node, QWidget *parent) : - QWidget(parent), - node_(node) + node_(node), + create_checkboxes_(create_checkboxes) { QGridLayout* root_layout = new QGridLayout(this); int insert_row = 0; // Create widgets all root level components - foreach (const QString& input, node->inputs()) { - CreateWidgets(root_layout, node, input, -1, insert_row); + foreach (QString input, node->inputs()) { + Node *n = node; + while (NodeGroup *g = dynamic_cast(n)) { + const NodeInput &ni = g->GetInputPassthroughs().value(input); + n = ni.node(); + input = ni.input(); + } - insert_row++; - - if (node->InputIsArray(input)) { - // Insert here - QWidget* array_widget = new QWidget(); - - QGridLayout* array_layout = new QGridLayout(array_widget); - array_layout->setContentsMargins(QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral(" ")), 0, 0, 0); - - root_layout->addWidget(array_widget, insert_row, 1, 1, 10); - - // Start with zero elements for efficiency. We will make the widgets for them if the user - // requests the array UI to be expanded - int arr_sz = 0; - - // Add one last add button for appending to the array - NodeParamViewArrayButton* append_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd); - connect(append_btn, &NodeParamViewArrayButton::clicked, this, &NodeParamViewItemBody::ArrayAppendClicked); - array_layout->addWidget(append_btn, arr_sz, kArrayInsertColumn); - - array_widget->setVisible(false); - - array_ui_.insert({node, input}, {array_widget, arr_sz, append_btn}); + if (!(n->GetInputFlags(input) & kInputFlagHidden)) { + CreateWidgets(root_layout, n, input, -1, insert_row); insert_row++; + + if (n->InputIsArray(input)) { + // Insert here + QWidget* array_widget = new QWidget(); + + QGridLayout* array_layout = new QGridLayout(array_widget); + array_layout->setContentsMargins(QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral(" ")), 0, 0, 0); + + root_layout->addWidget(array_widget, insert_row, 1, 1, 10); + + // Start with zero elements for efficiency. We will make the widgets for them if the user + // requests the array UI to be expanded + int arr_sz = 0; + + // Add one last add button for appending to the array + NodeParamViewArrayButton* append_btn = new NodeParamViewArrayButton(NodeParamViewArrayButton::kAdd); + connect(append_btn, &NodeParamViewArrayButton::clicked, this, &NodeParamViewItemBody::ArrayAppendClicked); + array_layout->addWidget(append_btn, arr_sz, kArrayInsertColumn); + + array_widget->setVisible(false); + + array_ui_.insert({n, input}, {array_widget, arr_sz, append_btn}); + + insert_row++; + } } } @@ -277,11 +153,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 +179,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); @@ -412,6 +299,12 @@ int NodeParamViewItemBody::GetElementY(NodeInput c) const c.set_element(-1); } + while (NodeGroup *g = dynamic_cast(c.node())) { + const NodeInput &passthrough = g->GetInputPassthroughs().value(c.input()); + c.set_node(passthrough.node()); + c.set_input(passthrough.input()); + } + // Find its row in the parameters QLabel* lbl = input_ui_map_.value(c).main_label; @@ -448,6 +341,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()); + } } } @@ -463,7 +361,13 @@ void NodeParamViewItemBody::PlaceWidgetsFromBridge(QGridLayout* layout, NodePara void NodeParamViewItemBody::InputArraySizeChangedInternal(Node *node, const QString &input, int size) { - ArrayUI& array_ui = array_ui_[{node, input}]; + NodeInputPair nip = {node, input}; + + if (!array_ui_.contains(nip)) { + return; + } + + ArrayUI& array_ui = array_ui_[nip]; if (size != array_ui.count) { QGridLayout* grid = static_cast(array_ui.widget->layout()); @@ -575,6 +479,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 +502,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..9e45f55f7 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.h +++ b/app/widget/nodeparamview/nodeparamviewitem.h @@ -21,7 +21,7 @@ #ifndef NODEPARAMVIEWITEM_H #define NODEPARAMVIEWITEM_H -#include +#include #include #include #include @@ -32,50 +32,24 @@ #include "nodeparamviewarraywidget.h" #include "nodeparamviewconnectedlabel.h" #include "nodeparamviewkeyframecontrol.h" +#include "nodeparamviewitembase.h" #include "nodeparamviewwidgetbridge.h" #include "widget/clickablelabel/clickablelabel.h" #include "widget/collapsebutton/collapsebutton.h" +#include "widget/keyframeview/keyframeview.h" namespace olive { -class NodeParamViewItemTitleBar : public QWidget -{ - Q_OBJECT -public: - NodeParamViewItemTitleBar(QWidget* parent = nullptr); - - void SetExpanded(bool e); - - void SetText(const QString& s) - { - lbl_->setText(s); - lbl_->setToolTip(s); - lbl_->setMinimumWidth(1); - } - -signals: - void ExpandedStateChanged(bool e); - - void PinToggled(bool e); - -protected: - virtual void paintEvent(QPaintEvent *event) override; - - virtual void mouseDoubleClickEvent(QMouseEvent *event) override; - -private: - bool draw_border_; - - QLabel* lbl_; - - CollapseButton* collapse_btn_; - +enum NodeParamViewCheckBoxBehavior { + kNoCheckBoxes, + kCheckBoxesOn, + kCheckBoxesOnNonConnected }; 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 +60,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 +71,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 +92,7 @@ private: QGridLayout* layout; int row; QPushButton *extra_btn; + QCheckBox *optional_checkbox; NodeParamViewArrayButton* array_insert_btn; NodeParamViewArrayButton* array_remove_btn; @@ -135,6 +114,8 @@ private: rational timebase_; + NodeParamViewCheckBoxBehavior create_checkboxes_; + /** * @brief The column to place the keyframe controls in * @@ -147,6 +128,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,74 +152,72 @@ private slots: void ShowSpeedDurationDialogForNode(); + void OptionalCheckBoxClicked(bool e); + }; -class NodeParamViewItem : public QDockWidget +class NodeParamViewItem : public NodeParamViewItemBase { Q_OBJECT public: - NodeParamViewItem(Node* node, QWidget* parent = nullptr); + NodeParamViewItem(Node* node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget* parent = nullptr); - void SetTimeTarget(Node* target); - - void SetTime(const rational& time); - - // Set the timebase of the NodeParamViewItemBody - void SetTimebase(const rational& timebase); - - Node* GetNode() const; - - bool IsExpanded() const; - - void SetHighlighted(bool e) + void SetTimeTarget(Node* target) { - highlighted_ = e; + body_->SetTimeTarget(target); + } - update(); + void SetTime(const rational& time) + { + time_ = time; + + body_->SetTime(time_); + } + + void SetTimebase(const rational& timebase) + { + body_->SetTimebase(timebase); + } + + Node* GetNode() const + { + return node_; } int GetElementY(const NodeInput& c) const; -public slots: - void SetExpanded(bool e); + void SetInputChecked(const NodeInput &input, bool e); - void ToggleExpanded(); + const KeyframeView::NodeConnections &GetKeyframeConnections() const + { + return keyframe_connections_; + } + + void SetKeyframeConnections(const KeyframeView::NodeConnections &c) + { + keyframe_connections_ = c; + } signals: void RequestSetTime(const rational& time); void RequestSelectNode(const QVector& node); - void PinToggled(bool e); - - void ExpandedChanged(bool e); - void ArrayExpandedChanged(bool e); - void Moved(); + void InputCheckedChanged(const NodeInput &input, bool e); -protected: - virtual void changeEvent(QEvent *e) override; - - virtual void paintEvent(QPaintEvent *event) override; - - virtual void moveEvent(QMoveEvent *event) override; +protected slots: + virtual void Retranslate() override; private: - NodeParamViewItemTitleBar* title_bar_; - NodeParamViewItemBody* body_; - QWidget *hidden_body_; - Node* node_; rational time_; - bool highlighted_; - -private slots: - void Retranslate(); + KeyframeView::NodeConnections keyframe_connections_; }; diff --git a/app/widget/nodeparamview/nodeparamviewitembase.cpp b/app/widget/nodeparamview/nodeparamviewitembase.cpp new file mode 100644 index 000000000..5116d872a --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewitembase.cpp @@ -0,0 +1,114 @@ +/*** + + 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 "nodeparamviewitembase.h" + +#include +#include + +namespace olive { + +#define super QDockWidget + +NodeParamViewItemBase::NodeParamViewItemBase(QWidget *parent) : + super(parent), + highlighted_(false) +{ + // Create title bar widget + title_bar_ = new NodeParamViewItemTitleBar(this); + + // Add title bar to widget + this->setTitleBarWidget(title_bar_); + + // Connect title bar to this + connect(title_bar_, &NodeParamViewItemTitleBar::ExpandedStateChanged, this, &NodeParamViewItemBase::SetExpanded); + connect(title_bar_, &NodeParamViewItemTitleBar::PinToggled, this, &NodeParamViewItemBase::PinToggled); + + // Use dummy QWidget to retain width when not expanded (QDockWidget seems to ignore the titlebar + // size hints and will shrink as small as possible if the body is hidden) + hidden_body_ = new QWidget(this); + + setAutoFillBackground(true); + + setFocusPolicy(Qt::ClickFocus); +} + +bool NodeParamViewItemBase::IsExpanded() const +{ + return title_bar_->IsExpanded(); +} + +QString NodeParamViewItemBase::GetTitleBarTextFromNode(Node *n) +{ + if (n->GetLabel().isEmpty()) { + return n->Name(); + } else { + return tr("%1 (%2)").arg(n->GetLabel(), n->Name()); + } +} + +void NodeParamViewItemBase::SetBody(QWidget *body) +{ + body_ = body; + body_->setParent(this); + + if (title_bar_->IsExpanded()) { + setWidget(body_); + } +} + +void NodeParamViewItemBase::paintEvent(QPaintEvent *event) +{ + super::paintEvent(event); + + // Draw border if focused + if (highlighted_) { + QPainter p(this); + p.setBrush(Qt::NoBrush); + p.setPen(palette().highlight().color()); + p.drawRect(rect().adjusted(0, 0, -1, -1)); + } +} + +void NodeParamViewItemBase::SetExpanded(bool e) +{ + setWidget(e ? body_ : hidden_body_); + title_bar_->SetExpanded(e); + + emit ExpandedChanged(e); +} + +void NodeParamViewItemBase::changeEvent(QEvent *e) +{ + if (e->type() == QEvent::LanguageChange) { + Retranslate(); + } + + super::changeEvent(e); +} + +void NodeParamViewItemBase::moveEvent(QMoveEvent *event) +{ + super::moveEvent(event); + + emit Moved(); +} + +} diff --git a/app/widget/nodeparamview/nodeparamviewitembase.h b/app/widget/nodeparamview/nodeparamviewitembase.h new file mode 100644 index 000000000..d5f570656 --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewitembase.h @@ -0,0 +1,93 @@ +/*** + + 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 NODEPARAMVIEWITEMBASE_H +#define NODEPARAMVIEWITEMBASE_H + +#include + +#include "nodeparamviewitemtitlebar.h" +#include "node/node.h" + +namespace olive { + +class NodeParamViewItemBase : public QDockWidget +{ + Q_OBJECT +public: + NodeParamViewItemBase(QWidget* parent = nullptr); + + void SetHighlighted(bool e) + { + highlighted_ = e; + + update(); + } + + bool IsExpanded() const; + + static QString GetTitleBarTextFromNode(Node *n); + +public slots: + void SetExpanded(bool e); + + void ToggleExpanded() + { + SetExpanded(!IsExpanded()); + } + +signals: + void PinToggled(bool e); + + void ExpandedChanged(bool e); + + void Moved(); + +protected: + void SetBody(QWidget *body); + + virtual void paintEvent(QPaintEvent *event) override; + + NodeParamViewItemTitleBar* title_bar() const + { + return title_bar_; + } + + virtual void changeEvent(QEvent *e) override; + + virtual void moveEvent(QMoveEvent *event) override; + +protected slots: + virtual void Retranslate(){} + +private: + NodeParamViewItemTitleBar* title_bar_; + + QWidget *body_; + + QWidget *hidden_body_; + + bool highlighted_; + +}; + +} + +#endif // NODEPARAMVIEWITEMBASE_H diff --git a/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp new file mode 100644 index 000000000..6d61bd1ba --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewitemtitlebar.cpp @@ -0,0 +1,90 @@ +/*** + + 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 "nodeparamviewitemtitlebar.h" + +#include +#include + +#include "ui/icons/icons.h" + +namespace olive { + +NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) : + QWidget(parent), + draw_border_(true) +{ + QHBoxLayout* layout = new QHBoxLayout(this); + + collapse_btn_ = new CollapseButton(); + connect(collapse_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::ExpandedStateChanged); + layout->addWidget(collapse_btn_); + + lbl_ = new QLabel(); + layout->addWidget(lbl_); + + // Place next buttons on the far side + layout->addStretch(); + + add_fx_btn_ = new QPushButton(); + add_fx_btn_->setIcon(icon::AddEffect); + add_fx_btn_->setFixedSize(add_fx_btn_->sizeHint().height(), add_fx_btn_->sizeHint().height()); + add_fx_btn_->setVisible(false); + layout->addWidget(add_fx_btn_); + connect(add_fx_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::AddEffectButtonClicked); + + pin_btn_ = new QPushButton(QStringLiteral("P")); + pin_btn_->setCheckable(true); + pin_btn_->setFixedSize(pin_btn_->sizeHint().height(), pin_btn_->sizeHint().height()); + pin_btn_->setVisible(false); + layout->addWidget(pin_btn_); + connect(pin_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled); +} + +void NodeParamViewItemTitleBar::SetExpanded(bool e) +{ + draw_border_ = e; + collapse_btn_->setChecked(e); + + update(); +} + +void NodeParamViewItemTitleBar::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + if (draw_border_) { + QPainter p(this); + + // Draw bottom border using text color + int bottom = height() - 1; + p.setPen(palette().text().color()); + p.drawLine(0, bottom, width(), bottom); + } +} + +void NodeParamViewItemTitleBar::mouseDoubleClickEvent(QMouseEvent *event) +{ + QWidget::mouseDoubleClickEvent(event); + + collapse_btn_->click(); +} + +} diff --git a/app/widget/nodeparamview/nodeparamviewitemtitlebar.h b/app/widget/nodeparamview/nodeparamviewitemtitlebar.h new file mode 100644 index 000000000..7ddaac1aa --- /dev/null +++ b/app/widget/nodeparamview/nodeparamviewitemtitlebar.h @@ -0,0 +1,89 @@ +/*** + + 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 NODEPARAMVIEWITEMTITLEBAR_H +#define NODEPARAMVIEWITEMTITLEBAR_H + +#include +#include + +#include "widget/collapsebutton/collapsebutton.h" + +namespace olive { + +class NodeParamViewItemTitleBar : public QWidget +{ + Q_OBJECT +public: + NodeParamViewItemTitleBar(QWidget* parent = nullptr); + + bool IsExpanded() const + { + return collapse_btn_->isChecked(); + } + +public slots: + void SetExpanded(bool e); + + void SetText(const QString& s) + { + lbl_->setText(s); + lbl_->setToolTip(s); + lbl_->setMinimumWidth(1); + } + + void SetPinButtonVisible(bool e) + { + pin_btn_->setVisible(e); + } + + void SetAddEffectButtonVisible(bool e) + { + add_fx_btn_->setVisible(e); + } + +signals: + void ExpandedStateChanged(bool e); + + void PinToggled(bool e); + + void AddEffectButtonClicked(); + +protected: + virtual void paintEvent(QPaintEvent *event) override; + + virtual void mouseDoubleClickEvent(QMouseEvent *event) override; + +private: + bool draw_border_; + + QLabel* lbl_; + + CollapseButton* collapse_btn_; + + QPushButton *pin_btn_; + + QPushButton *add_fx_btn_; + +}; + +} + +#endif // NODEPARAMVIEWITEMTITLEBAR_H diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index ca983c581..1a8f7aff8 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -38,7 +38,6 @@ #include "widget/slider/floatslider.h" #include "widget/slider/integerslider.h" #include "widget/slider/rationalslider.h" -#include "widget/videoparamedit/videoparamedit.h" namespace olive { @@ -89,6 +88,8 @@ void NodeParamViewWidgetBridge::CreateWidgets() case NodeValue::kShaderJob: case NodeValue::kSampleJob: case NodeValue::kGenerateJob: + case NodeValue::kVideoParams: + case NodeValue::kAudioParams: break; case NodeValue::kInt: { @@ -156,19 +157,6 @@ void NodeParamViewWidgetBridge::CreateWidgets() connect(font_combobox, &QFontComboBox::currentFontChanged, this, &NodeParamViewWidgetBridge::WidgetCallback); break; } - case NodeValue::kVideoParams: - { - VideoParamEdit* edit = new VideoParamEdit(); - edit->SetColorManager(input_.node()->project()->color_manager()); - widgets_.append(edit); - connect(edit, &VideoParamEdit::Changed, this, &NodeParamViewWidgetBridge::WidgetCallback); - break; - } - case NodeValue::kAudioParams: - { - // FIXME: Create audio param widget - break; - } } // Check all properties @@ -278,6 +266,8 @@ void NodeParamViewWidgetBridge::WidgetCallback() case NodeValue::kShaderJob: case NodeValue::kSampleJob: case NodeValue::kGenerateJob: + case NodeValue::kVideoParams: + case NodeValue::kAudioParams: break; case NodeValue::kInt: { @@ -388,15 +378,6 @@ void NodeParamViewWidgetBridge::WidgetCallback() SetInputValue(index, 0); break; } - case NodeValue::kVideoParams: - { - VideoParamEdit* edit = static_cast(sender()); - SetInputValue(QVariant::fromValue(edit->GetVideoParams()), 0); - break; - } - case NodeValue::kAudioParams: - // FIXME: No audio param widget yet - break; } } @@ -434,6 +415,8 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() case NodeValue::kShaderJob: case NodeValue::kSampleJob: case NodeValue::kGenerateJob: + case NodeValue::kVideoParams: + case NodeValue::kAudioParams: break; case NodeValue::kInt: { @@ -528,15 +511,6 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues() cb->blockSignals(false); break; } - case NodeValue::kVideoParams: - { - VideoParamEdit* edit = static_cast(widgets_.first()); - edit->SetVideoParams(input_.GetValueAtTime(node_time).value()); - break; - } - case NodeValue::kAudioParams: - // FIXME: No audio param widget - break; } } @@ -669,42 +643,12 @@ void NodeParamViewWidgetBridge::PropertyChanged(const QString& input, const QStr break; } } else if (key == QStringLiteral("offset")) { - switch (data_type) { - case NodeValue::kInt: - static_cast(widgets_.first())->SetOffset(value); - break; - case NodeValue::kFloat: - static_cast(widgets_.first())->SetOffset(value); - break; - case NodeValue::kRational: - static_cast(widgets_.first())->SetOffset(value); - break; - case NodeValue::kVec2: - { - QVector2D offs = value.value(); - static_cast(widgets_.at(0))->SetOffset(offs.x()); - static_cast(widgets_.at(1))->SetOffset(offs.y()); - break; - } - case NodeValue::kVec3: - { - QVector3D offs = value.value(); - static_cast(widgets_.at(0))->SetOffset(offs.x()); - static_cast(widgets_.at(1))->SetOffset(offs.y()); - static_cast(widgets_.at(2))->SetOffset(offs.z()); - break; - } - case NodeValue::kVec4: - { - QVector4D offs = value.value(); - static_cast(widgets_.at(0))->SetOffset(offs.x()); - static_cast(widgets_.at(1))->SetOffset(offs.y()); - static_cast(widgets_.at(2))->SetOffset(offs.z()); - static_cast(widgets_.at(3))->SetOffset(offs.w()); - break; - } - default: - break; + int tracks = NodeValue::get_number_of_keyframe_tracks(data_type); + + QVector offsets = NodeValue::split_normal_value_into_track_values(data_type, value); + + for (int i=0; i(widgets_.at(i))->SetOffset(offsets.at(i)); } UpdateWidgetValues(); @@ -794,15 +738,6 @@ void NodeParamViewWidgetBridge::PropertyChanged(const QString& input, const QStr ff->SetDirectoryMode(value.toBool()); } } - - // Parameters for video param objects - if (data_type == NodeValue::kVideoParams) { - VideoParamEdit* edit = static_cast(widgets_.first()); - - if (key == QStringLiteral("mask")) { - edit->SetParameterMask(value.toULongLong()); - } - } } void NodeParamViewWidgetBridge::InputDataTypeChanged(const QString &input, NodeValue::Type type) diff --git a/app/widget/nodetreeview/nodetreeview.cpp b/app/widget/nodetreeview/nodetreeview.cpp index a1ee354a3..fd0afc2d9 100644 --- a/app/widget/nodetreeview/nodetreeview.cpp +++ b/app/widget/nodetreeview/nodetreeview.cpp @@ -27,7 +27,8 @@ namespace olive { NodeTreeView::NodeTreeView(QWidget *parent) : QTreeWidget(parent), only_show_keyframable_(false), - show_keyframe_tracks_as_rows_(false) + show_keyframe_tracks_as_rows_(false), + checkboxes_enabled_(false) { connect(this, &NodeTreeView::itemChanged, this, &NodeTreeView::ItemCheckStateChanged); connect(this, &NodeTreeView::itemSelectionChanged, this, &NodeTreeView::SelectionChanged); @@ -67,12 +68,14 @@ void NodeTreeView::SetNodes(const QVector &nodes) foreach (Node* n, nodes_) { QTreeWidgetItem* node_item = new QTreeWidgetItem(); node_item->setText(0, n->Name()); - node_item->setCheckState(0, disabled_nodes_.contains(n) ? Qt::Unchecked : Qt::Checked); + if (checkboxes_enabled_) { + node_item->setCheckState(0, disabled_nodes_.contains(n) ? Qt::Unchecked : Qt::Checked); + } node_item->setData(0, kItemType, kItemTypeNode); node_item->setData(0, kItemNodePointer, Node::PtrToValue(n)); foreach (const QString& input, n->inputs()) { - if (only_show_keyframable_ && !n->IsInputKeyframable(input)) { + if (n->IsInputHidden(input) || (only_show_keyframable_ && !n->IsInputKeyframable(input))) { continue; } @@ -105,7 +108,6 @@ void NodeTreeView::SetNodes(const QVector &nodes) CreateItemsForTracks(element_item, input_ref, key_tracks.size()); } } - } // Add at the end to prevent unnecessary signalling while we're setting these objects up @@ -115,6 +117,8 @@ void NodeTreeView::SetNodes(const QVector &nodes) delete node_item; } } + + expandAll(); } void NodeTreeView::changeEvent(QEvent *e) @@ -153,6 +157,8 @@ NodeKeyframeTrackReference NodeTreeView::GetSelectedInput() if (item->data(0, kItemType).toInt() == kItemTypeInput) { selected_ref = item->data(0, kItemInputReference).value(); + } else { + selected_ref = NodeKeyframeTrackReference(NodeInput(Node::ValueToPtr(item->data(0, kItemNodePointer)), QString())); } } @@ -164,21 +170,27 @@ QTreeWidgetItem* NodeTreeView::CreateItem(QTreeWidgetItem *parent, const NodeKey QTreeWidgetItem* input_item = new QTreeWidgetItem(parent); QString item_name; - if (ref.track() == -1 || NodeValue::get_number_of_keyframe_tracks(ref.input().GetDataType()) == 1) { - item_name = ref.input().name(); + if (ref.track() == -1 + || NodeValue::get_number_of_keyframe_tracks(ref.input().GetDataType()) == 1 + || (ref.input().IsArray() && ref.input().element() == -1)) { + if (ref.input().element() == -1) { + item_name = ref.input().name(); + } else { + item_name = QString::number(ref.input().element()); + } } else { switch (ref.track()) { case 0: - item_name = tr("X"); + item_name = UseRGBAOverXYZW(ref) ? tr("R") : tr("X"); break; case 1: - item_name = tr("Y"); + item_name = UseRGBAOverXYZW(ref) ? tr("G") : tr("Y"); break; case 2: - item_name = tr("Z"); + item_name = UseRGBAOverXYZW(ref) ? tr("B") : tr("Z"); break; case 3: - item_name = tr("W"); + item_name = UseRGBAOverXYZW(ref) ? tr("A") : tr("W"); break; default: item_name = QString::number(ref.track()); @@ -186,7 +198,9 @@ QTreeWidgetItem* NodeTreeView::CreateItem(QTreeWidgetItem *parent, const NodeKey } input_item->setText(0, item_name); - input_item->setCheckState(0, disabled_inputs_.contains(ref) ? Qt::Unchecked : Qt::Checked); + if (checkboxes_enabled_) { + input_item->setCheckState(0, disabled_inputs_.contains(ref) ? Qt::Unchecked : Qt::Checked); + } input_item->setData(0, kItemType, kItemTypeInput); input_item->setData(0, kItemInputReference, QVariant::fromValue(ref)); @@ -206,6 +220,11 @@ void NodeTreeView::CreateItemsForTracks(QTreeWidgetItem *parent, const NodeInput } } +bool NodeTreeView::UseRGBAOverXYZW(const NodeKeyframeTrackReference &ref) +{ + return ref.input().GetDataType() == NodeValue::kColor; +} + void NodeTreeView::ItemCheckStateChanged(QTreeWidgetItem *item, int column) { Q_UNUSED(column) diff --git a/app/widget/nodetreeview/nodetreeview.h b/app/widget/nodetreeview/nodetreeview.h index 3528b2822..067a4c82a 100644 --- a/app/widget/nodetreeview/nodetreeview.h +++ b/app/widget/nodetreeview/nodetreeview.h @@ -37,6 +37,11 @@ public: bool IsInputEnabled(const NodeKeyframeTrackReference& ref) const; + void SetCheckBoxesEnabled(bool e) + { + checkboxes_enabled_ = e; + } + void SetKeyframeTrackColor(const NodeKeyframeTrackReference& ref, const QColor& color); void SetOnlyShowKeyframable(bool e) @@ -75,6 +80,8 @@ private: void CreateItemsForTracks(QTreeWidgetItem* parent, const NodeInput& input, int track_count); + static bool UseRGBAOverXYZW(const NodeKeyframeTrackReference &ref); + enum ItemType { kItemTypeNode, kItemTypeInput @@ -98,6 +105,8 @@ private: QHash keyframe_colors_; + bool checkboxes_enabled_; + private slots: void ItemCheckStateChanged(QTreeWidgetItem* item, int column); diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index e73798ae5..cf3a2375e 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -19,10 +19,14 @@ set(OLIVE_SOURCES widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h widget/nodeview/nodeviewcommon.h + widget/nodeview/nodeviewcontext.cpp + widget/nodeview/nodeviewcontext.h widget/nodeview/nodeviewedge.cpp widget/nodeview/nodeviewedge.h widget/nodeview/nodeviewitem.cpp widget/nodeview/nodeviewitem.h + widget/nodeview/nodeviewitemconnector.cpp + widget/nodeview/nodeviewitemconnector.h widget/nodeview/nodeviewminimap.cpp widget/nodeview/nodeviewminimap.h widget/nodeview/nodeviewscene.cpp @@ -31,5 +35,7 @@ set(OLIVE_SOURCES widget/nodeview/nodeviewtoolbar.h widget/nodeview/nodeviewundo.cpp widget/nodeview/nodeviewundo.h + widget/nodeview/nodewidget.cpp + widget/nodeview/nodewidget.h PARENT_SCOPE ) diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index a375ede4f..481a88b71 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -21,14 +21,16 @@ #include "nodeview.h" #include +#include #include #include +#include -#include "core.h" #include "nodeviewundo.h" #include "node/audio/volume/volume.h" #include "node/distort/transform/transformdistortnode.h" #include "node/factory.h" +#include "node/group/group.h" #include "node/traverser.h" #include "widget/menu/menushared.h" #include "widget/timebased/timebasedview.h" @@ -41,15 +43,11 @@ const double NodeView::kMinimumScale = 0.1; NodeView::NodeView(QWidget *parent) : HandMovableView(parent), - graph_(nullptr), drop_edge_(nullptr), create_edge_(nullptr), - create_edge_dst_(nullptr), - create_edge_dst_temp_expanded_(false), - paste_command_(nullptr), - filter_mode_(kFilterShowSelective), - scale_(1.0), - queue_reposition_contexts_(false) + create_edge_output_item_(nullptr), + create_edge_input_item_(nullptr), + scale_(1.0) { setScene(&scene_); SetDefaultDragMode(RubberBandDrag); @@ -62,7 +60,7 @@ NodeView::NodeView(QWidget *parent) : ConnectSelectionChangedSignal(); - SetFlowDirection(NodeViewCommon::kTopToBottom); + SetFlowDirection(NodeViewCommon::kLeftToRight); UpdateSceneBoundingRect(); connect(&scene_, &QGraphicsScene::changed, this, &NodeView::UpdateSceneBoundingRect); @@ -83,129 +81,60 @@ NodeView::~NodeView() ClearGraph(); } -void NodeView::SetGraph(NodeGraph *graph, const QVector &nodes) +void NodeView::SetContexts(const QVector &nodes) { - bool graph_changed = graph_ != graph; - bool context_changed = last_set_filter_nodes_ != nodes; - - if (graph_changed || context_changed) { - // Clear nodes if necessary - bool refresh_required = (graph_changed && filter_mode_ == kFilterShowAll) - || (context_changed && filter_mode_ == kFilterShowSelective); - bool nodes_visible = (graph && filter_mode_ == kFilterShowAll) - || (!nodes.isEmpty() && filter_mode_ == kFilterShowSelective); - - if (refresh_required) { - DeselectAll(); - positions_.clear(); - scene_.clear(); - context_offsets_.clear(); - } - - // Handle graph change - if (graph_changed) { - if (graph_) { - // Disconnect from current graph - disconnect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); - disconnect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); - disconnect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); - disconnect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); - disconnect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); - } - - graph_ = graph; - - if (graph_) { - // Connect to new graph - connect(graph_, &NodeGraph::NodeRemoved, this, &NodeView::RemoveNode); - connect(graph_, &NodeGraph::InputConnected, this, &NodeView::AddEdge); - connect(graph_, &NodeGraph::InputDisconnected, this, &NodeView::RemoveEdge); - connect(graph_, &NodeGraph::NodePositionAdded, this, &NodeView::AddNodePosition); - connect(graph_, &NodeGraph::NodePositionRemoved, this, &NodeView::RemoveNodePosition); - } - } - - if (context_changed) { - last_set_filter_nodes_ = nodes; - - if (filter_mode_ == kFilterShowSelective) { - filter_nodes_ = nodes; - } - } - - if (refresh_required && nodes_visible) { - if (filter_mode_ == kFilterShowAll) { - // Just make the filter nodes all of the graph's contexts - filter_nodes_ = graph->GetPositionMap().keys().toVector(); - } - - RepositionContexts(); - - // Center on something - QMetaObject::invokeMethod(this, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection); + // 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 (!contexts_.contains(n)) { + AddContext(n); + } + } + + contexts_ = nodes; + + CenterOnItemsBoundingRect(); +} + +void NodeView::CloseContextsBelongingToProject(Project *project) +{ + QVector 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() { - SetGraph(nullptr, QVector()); + SetContexts(QVector()); } void NodeView::DeleteSelected() { - if (!graph_) { - return; + NodeViewDeleteCommand* command = new NodeViewDeleteCommand(); + + foreach (NodeViewContext *ctx, scene_.context_map()) { + ctx->DeleteSelected(command); } - MultiUndoCommand* command = new MultiUndoCommand(); - - { - // First remove any selected edges - QVector selected_edges = scene_.GetSelectedEdges(); - - if (!selected_edges.isEmpty()) { - Node::OutputConnections removed_connections(selected_edges.size()); - - for (int i=0; iadd_child(new NodeEdgeRemoveCommand(edge->output(), edge->input())); - removed_connections[i] = {edge->output(), edge->input()}; - } - - // Update contexts - UpdateContextsFromEdgeRemove(command, removed_connections); - } - } - - { - // Secondly remove any nodes - QVector selected_nodes = scene_.GetSelectedNodes(); - - // Ensure no nodes are "undeletable" - for (int i=0;iCanBeDeleted()) { - selected_nodes.removeAt(i); - i--; - } - } - - if (!selected_nodes.isEmpty()) { - for (Node* node : qAsConst(selected_nodes)) { - command->add_child(new NodeRemoveAndDisconnectCommand(node)); - } - } - } - - Core::instance()->undo_stack()->pushIfHasChildren(command); + Core::instance()->undo_stack()->push(command); } void NodeView::SelectAll() { - if (!graph_) { - return; - } - // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); @@ -214,25 +143,12 @@ void NodeView::SelectAll() ConnectSelectionChangedSignal(); - // Determine which nodes aren't selected and add them to a separate vector - QVector new_selection; - for (auto it=scene_.item_map().cbegin(); it!=scene_.item_map().cend(); it++) { - Node *n = it.key(); - if (!selected_nodes_.contains(n)) { - new_selection.append(n); - } - } - - // Add this vector to our total selection vector - selected_nodes_.append(new_selection); - - // Signal new nodes - emit NodesSelected(new_selection); + UpdateSelectionCache(); } void NodeView::DeselectAll() { - if (!graph_ || selected_nodes_.isEmpty()) { + if (selected_nodes_.isEmpty()) { return; } @@ -249,12 +165,8 @@ void NodeView::DeselectAll() selected_nodes_.clear(); } -void NodeView::Select(QVector nodes, bool center_view_on_item) +void NodeView::Select(const QVector &nodes, bool center_view_on_item) { - if (!graph_) { - return; - } - // Optimization: rather than respond to every single item being selected, ignore the signal and // then handle them all at the end. DisconnectSelectionChangedSignal(); @@ -264,89 +176,27 @@ void NodeView::Select(QVector nodes, bool center_view_on_item) scene_.DeselectAll(); - // Remove any duplicates - QVector processed; - - NodeViewItem *first_item = nullptr; - - for (Node* n : qAsConst(nodes)) { - if (processed.contains(n)) { - continue; - } - - processed.append(n); - - NodeViewItem* item = scene_.NodeToUIObject(n); - - if (item) { - item->setSelected(true); - - if (!first_item) { - first_item = item; - } - - if (deselections.contains(n)) { - deselections.removeOne(n); - } else { - new_selections.append(n); - } - } + foreach (NodeViewContext *context, scene_.context_map()) { + context->Select(nodes); } // Center on something - if (center_view_on_item && first_item) { - centerOn(first_item); + if (center_view_on_item && !nodes.isEmpty()) { + QMetaObject::invokeMethod(this, "CenterOnNode", Qt::QueuedConnection, OLIVE_NS_ARG(Node*, nodes.first())); } ConnectSelectionChangedSignal(); - // Emit deselect signal for any nodes that weren't in the list - if (!deselections.isEmpty()) { - emit NodesDeselected(deselections); - } - - // Emit select signal for any nodes that weren't in the list - if (!new_selections.isEmpty()) { - emit NodesSelected(new_selections); - } - - // Update selected list to the list we received - selected_nodes_ = nodes; -} - -void NodeView::SelectWithDependencies(QVector nodes, bool center_view_on_item) -{ - if (!graph_) { - return; - } - - int original_length = nodes.size(); - for (int i=0;i dependencies = nodes.at(i)->GetDependencies(); - - foreach (Node *d, dependencies) { - if (scene_.item_map().contains(d) && !nodes.contains(d)) { - nodes.append(d); - } - } - } - - Select(nodes, center_view_on_item); + UpdateSelectionCache(); } void NodeView::CopySelected(bool cut) { - if (!graph_) { + if (selected_nodes_.isEmpty()) { return; } - QVector selected = scene_.GetSelectedNodes(); - - if (selected.isEmpty()) { - return; - } - - CopyNodesToClipboard(selected); + CopyNodesToClipboard(selected_nodes_); if (cut) { DeleteSelected(); @@ -360,7 +210,7 @@ void NodeView::Paste() void NodeView::Duplicate() { - PasteNodesInternal(scene_.GetSelectedNodes()); + PasteNodesInternal(selected_nodes_); } void NodeView::SetColorLabel(int index) @@ -392,56 +242,46 @@ void NodeView::keyPressEvent(QKeyEvent *event) case Qt::Key_Up: case Qt::Key_Down: { - if (graph_) { - MultiUndoCommand *pos_command = new MultiUndoCommand(); - for (Node *n : qAsConst(selected_nodes_)) { - for (Node *context : qAsConst(filter_nodes_)) { - if (graph_->GetNodesForContext(context).contains(n)) { - QPointF old_pos = graph_->GetNodePosition(n, context); + 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_; + // 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, false)); + // 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()->pushIfHasChildren(pos_command); } + Core::instance()->undo_stack()->pushIfHasChildren(pos_command); break; } case Qt::Key_Escape: if (!attached_items_.isEmpty()) { DetachItemsFromCursor(); - - // We undo the last action which SHOULD be adding the node - if (paste_command_) { - paste_command_->undo_now(); - delete paste_command_; - paste_command_ = nullptr; - } - break; } @@ -454,32 +294,74 @@ void NodeView::keyPressEvent(QKeyEvent *event) void NodeView::mousePressEvent(QMouseEvent *event) { + // Handle mouse press event if (HandPress(event)) return; - if (event->button() == Qt::LeftButton) { - // See if we're dragging the arrow of an edge - QPointF scene_pt = mapToScene(event->pos()); + // Get the item that the user clicked on, if any + QGraphicsItem* item = itemAt(event->pos()); - for (NodeViewEdge *edge_item : scene_.edges()) { - if (edge_item->arrow_bounding_rect().contains(scene_pt)) { - create_edge_src_ = scene_.NodeToUIObject(edge_item->output()); - create_edge_ = edge_item; - create_edge_already_exists_ = true; - return; + 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(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); } } - // See if we're dragging the arrow of a node - for (NodeViewItem *node_item : scene_.item_map()) { - if (node_item->GetOutputTriangle().boundingRect().translated(node_item->pos()).contains(scene_pt)) { - CreateNewEdge(node_item, event->pos()); - return; + if (!create_edge_output_item_ && !create_edge_input_item_) { + // Determine if user clicked on a connector + if (NodeViewItemConnector *connector = dynamic_cast(item)) { + NodeViewItem *attached = static_cast(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; + } } - QGraphicsItem* item = itemAt(event->pos()); - + // 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 @@ -494,15 +376,17 @@ void NodeView::mousePressEvent(QMouseEvent *event) } } - if (event->modifiers() & Qt::ControlModifier) { - NodeViewItem* node_item = dynamic_cast(item); - if (node_item) { - CreateNewEdge(node_item, event->pos()); - return; + // 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()); } } - - super::mousePressEvent(event); } void NodeView::mouseMoveEvent(QMouseEvent *event) @@ -593,23 +477,17 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) if (HandRelease(event)) return; if (create_edge_) { - // We are creating a new edge or moving an existing one + // Check if the edge was reconnected to the same place as before MultiUndoCommand* command = new MultiUndoCommand(); - Node::OutputConnections removed_edges; - Node::OutputConnection added_edge; - bool reconnected_to_itself = false; if (create_edge_already_exists_) { - if (create_edge_dst_input_ == create_edge_->input()) { + 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())); - - // Update contexts for edge removal - removed_edges.push_back({create_edge_->output(), create_edge_->input()}); } } else { // We're creating a new edge, which means this UI object is only temporary @@ -618,167 +496,160 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event) create_edge_ = nullptr; - if (create_edge_dst_) { - // Clear highlight - create_edge_dst_->SetHighlightedIndex(-1); + // 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); + } - // Collapse if we expanded it - if (create_edge_dst_temp_expanded_) { - create_edge_dst_->SetExpanded(false); - create_edge_dst_->setZValue(0); - } - - NodeInput &creating_input = create_edge_dst_input_; + if (create_edge_output_item_ && create_edge_input_item_) { + NodeInput &creating_input = create_edge_input_; if (creating_input.IsValid()) { // Make connection if (!reconnected_to_itself) { - Node *creating_output = create_edge_src_->GetNode(); + Node *creating_output = create_edge_output_item_->GetNode(); + + while (NodeGroup *output_group = dynamic_cast(creating_output)) { + creating_output = output_group->GetOutputPassthrough(); + } + + while (NodeGroup *input_group = dynamic_cast(creating_input.node())) { + creating_input = input_group->GetInputPassthroughs().value(creating_input.input()); + } if (creating_input.IsConnected()) { Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input}; command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second)); - removed_edges.push_back(existing_edge_to_remove); } command->add_child(new NodeEdgeAddCommand(creating_output, creating_input)); - added_edge = {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_dst_ = nullptr; } - // Update contexts - if (!removed_edges.empty()) { - UpdateContextsFromEdgeRemove(command, removed_edges); - } + create_edge_output_item_ = nullptr; + create_edge_input_item_ = nullptr; - if (added_edge.first) { - UpdateContextsFromEdgeAdd(command, added_edge, removed_edges); + // 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()->pushIfHasChildren(command); - return; } MultiUndoCommand* command = new MultiUndoCommand(); if (!attached_items_.isEmpty()) { - if (paste_command_) { - // We've already "done" this command, but MultiUndoCommand prevents "redoing" twice, so we - // add it to this command (which may have extra commands added too) so that it all gets undone - // in the same action - command->add_child(paste_command_); - paste_command_ = nullptr; - } - } + Node *context = nullptr; - { - // If any node positions changed, set them in their contexts now - MultiUndoCommand *set_pos_command = new MultiUndoCommand(); - for (auto it=positions_.begin(); it!=positions_.end(); it++) { - NodeViewItem *item = it.key(); - Position &pos_data = it.value(); - QPointF current_item_pos = item->GetNodePosition(); - Node *node = pos_data.node; - - if (pos_data.original_item_pos != current_item_pos) { - QPointF diff = current_item_pos - pos_data.original_item_pos; - - for (Node *context : qAsConst(filter_nodes_)) { - if (graph_->ContextContainsNode(node, context)) { - QPointF current_node_pos_in_context = graph_->GetNodePosition(node, context); - current_node_pos_in_context += diff; - set_pos_command->add_child(new NodeSetPositionCommand(node, context, current_node_pos_in_context, false)); - } - } - - pos_data.original_item_pos = current_item_pos; - } - } - if (set_pos_command->child_count()) { - set_pos_command->redo_now(); - command->add_child(set_pos_command); - } else { - delete set_pos_command; - } - } - - - if (!attached_items_.isEmpty()) { - { - // Dropped attached item onto an edge, connect it between them - MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); - if (attached_items_.size() == 1) { - Node* dropping_node = attached_items_.first().item->GetNode(); - - if (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; + QList items_at_cursor = this->items(event->pos()); + foreach (QGraphicsItem *i, items_at_cursor) { + if (NodeViewContext *context_item = dynamic_cast(i)) { + context = context_item->GetContext(); + break; } } - { - // Remove from context any nodes that don't specifically output to said context - MultiUndoCommand *remove_pos_command = new MultiUndoCommand(); + if (context) { + { + MultiUndoCommand *add_command = new MultiUndoCommand(); - for (const AttachedItem &attached : qAsConst(attached_items_)) { - MultiUndoCommand *remove_pos_subcommand = new MultiUndoCommand(); - Node *attached_node = scene_.item_map().key(attached.item); + foreach (const AttachedItem &ai, attached_items_) { + // Add node to the same graph that the context is in + add_command->add_child(new NodeAddCommand(context->parent(), ai.node)); - bool removed = false; - QVector relevant_contexts; - for (Node *context : qAsConst(filter_nodes_)) { - if (attached_node->OutputsTo(context, true)) { - relevant_contexts.append(context); - } else { - remove_pos_subcommand->add_child(new NodeRemovePositionFromContextCommand(attached_node, context)); - removed = true; + // Add node to the context + if (ai.item) { + qDebug() << "Placing an item!"; + add_command->add_child(new NodeSetPositionCommand(ai.node, context, scene_.context_map().value(context)->MapScenePosToNodePosInContext(ai.item->pos()))); } } - if (removed && !relevant_contexts.isEmpty()) { - for (Node *relevant : qAsConst(relevant_contexts)) { - remove_pos_subcommand->add_child(new NodeSetPositionCommand(attached_node, relevant, GetEstimatedPositionForContext(attached.item, relevant), false)); - } - - remove_pos_command->add_child(remove_pos_subcommand); + if (add_command->child_count()) { + add_command->redo_now(); + command->add_child(add_command); } else { - delete remove_pos_subcommand; + delete add_command; } } - if (remove_pos_command->child_count()) { - remove_pos_command->redo_now(); - command->add_child(remove_pos_command); - } else { - delete remove_pos_command; - } - } + { + // Dropped attached item onto an edge, connect it between them + MultiUndoCommand *drop_edge_command = new MultiUndoCommand(); + if (attached_items_.size() == 1) { + Node* dropping_node = nullptr; - DetachItemsFromCursor(); + foreach (const AttachedItem &ai, attached_items_) { + if (ai.item) { + 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); + } 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)); + } + } + dragging_items_.clear(); + Core::instance()->undo_stack()->pushIfHasChildren(command); super::mouseReleaseEvent(event); } +void NodeView::mouseDoubleClickEvent(QMouseEvent *event) +{ + super::mouseDoubleClickEvent(event); + + if (!(event->modifiers() & Qt::ControlModifier)) { + NodeViewItem *item_at_cursor = dynamic_cast(itemAt(event->pos())); + if (item_at_cursor) { + item_at_cursor->ToggleExpanded(); + } + } +} + void NodeView::resizeEvent(QResizeEvent *event) { super::resizeEvent(event); @@ -788,49 +659,55 @@ void NodeView::resizeEvent(QResizeEvent *event) void NodeView::UpdateSelectionCache() { - QVector current_selection = scene_.GetSelectedNodes(); + QVector current_selection = scene_.GetSelectedItems(); QVector selected; QVector deselected; // Determine which nodes are newly selected - if (selected_nodes_.isEmpty()) { - // All nodes in the current selection have just been selected - selected = current_selection; - } else { - for (Node* n : qAsConst(current_selection)) { - if (!selected_nodes_.contains(n)) { - selected.append(n); - } + foreach (NodeViewItem* i, current_selection) { + Node *n = i->GetNode(); + if (!selected_nodes_.contains(n)) { + selected.append(n); + selected_nodes_.append(n); } } // Determine which nodes are newly deselected if (current_selection.isEmpty()) { - // All nodes that were selected have been deselected + // All nodes that were selected have been deselected, so we'll just set them all to `deselected` deselected = selected_nodes_; + selected_nodes_.clear(); } else { - for (Node* n : qAsConst(selected_nodes_)) { - if (!current_selection.contains(n)) { + 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); } } } - selected_nodes_ = current_selection; - - if (!selected.isEmpty()) { - emit NodesSelected(selected); - } - if (!deselected.isEmpty()) { emit NodesDeselected(deselected); } + + if (!selected.isEmpty()) { + emit NodesSelected(selected); + } } void NodeView::ShowContextMenu(const QPoint &pos) { - if (!graph_) { + if (contexts_.isEmpty()) { return; } @@ -846,20 +723,33 @@ void NodeView::ShowContextMenu(const QPoint &pos) // Label node action QAction* label_action = m.addAction(tr("Label")); - connect(label_action, &QAction::triggered, this, [this](){ - Core::instance()->LabelNodes(scene_.GetSelectedNodes()); - }); + connect(label_action, &QAction::triggered, this, &NodeView::LabelSelectedNodes); + + // Grouping + if (selected.size() == 1 && dynamic_cast(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); - ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode()); - if (viewer) { + // Show in Viewer option for nodes based on Viewer + if (ViewerOutput* viewer = dynamic_cast(selected.first()->GetNode())) { 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(); + + // 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")); @@ -873,17 +763,6 @@ void NodeView::ShowContextMenu(const QPoint &pos) m.addSeparator(); - Menu* filter_menu = new Menu(tr("Filter"), &m); - m.addMenu(filter_menu); - - filter_menu->AddActionWithData(tr("Show All Nodes"), kFilterShowAll, filter_mode_); - - filter_menu->AddActionWithData(tr("Show Selected"), kFilterShowSelective, filter_mode_); - - connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); - - - Menu* direction_menu = new Menu(tr("Direction"), &m); m.addMenu(direction_menu); @@ -917,22 +796,14 @@ void NodeView::ShowContextMenu(const QPoint &pos) void NodeView::CreateNodeSlot(QAction *action) { - if (!graph_) { - return; - } - Node* new_node = NodeFactory::CreateFromMenuAction(action); if (new_node) { - paste_command_ = new MultiUndoCommand(); - paste_command_->add_child(new NodeAddCommand(graph_, new_node)); - for (Node *context : qAsConst(filter_nodes_)) { - paste_command_->add_child(new NodeSetPositionCommand(new_node, context, QPointF(0, 0), false)); - } - paste_command_->add_child(new NodeViewAttachNodesToCursor(this, {new_node})); - paste_command_->redo_now(); + NodeViewItem *new_item = new NodeViewItem(new_node, nullptr); + new_item->SetFlowDirection(scene_.GetFlowDirection()); + scene_.addItem(new_item); - this->setFocus(); + SetAttachedItems({{new_item, new_node, QPointF(0, 0)}}); } } @@ -941,127 +812,13 @@ void NodeView::ContextMenuSetDirection(QAction *action) SetFlowDirection(static_cast(action->data().toInt())); } -void NodeView::ContextMenuFilterChanged(QAction *action) -{ - FilterMode mode = static_cast(action->data().toInt()); - - if (filter_mode_ != mode) { - // Store temporary graph variables - NodeGraph *graph = graph_; - QVector nodes = last_set_filter_nodes_; - - // Unset graph with current filter mode - ClearGraph(); - - // Change filter mode - filter_mode_ = mode; - - // Re-set graph with new filter mode - SetGraph(graph, nodes); - } -} - void NodeView::OpenSelectedNodeInViewer() { - QVector selected = scene_.GetSelectedNodes(); - ViewerOutput* viewer = selected.isEmpty() ? nullptr : dynamic_cast(selected.first()); - - if (viewer) { - Core::instance()->OpenNodeInViewer(viewer); - } -} - -// Commenting out because there shouldn't be any situations where a node would be added without -// being in a context. We're keeping RemoveNode as a fail-safe because it could provide crash -// resistance where RemoveNodePosition might be missed. -//void NodeView::AddNode(Node *node) -//{ -// if (filter_mode_ == kFilterShowAll) { -// scene_.AddNode(node); -// } -//} - -void NodeView::RemoveNode(Node *node) -{ - for (auto it=attached_items_.begin(); it!=attached_items_.end(); ) { - if (it->item->GetNode() == node) { - it = attached_items_.erase(it); - } else { - it++; - } - } - for (const Node::OutputConnection &oc : node->output_connections()) { - scene_.RemoveEdge(oc.first, oc.second); - } - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - scene_.RemoveEdge(it->second, it->first); - } - positions_.remove(scene_.item_map().value(node)); - scene_.RemoveNode(node); -} - -void NodeView::AddEdge(Node *output, const NodeInput &input) -{ - Node *output_node = output; - Node *input_node = input.node(); - - if (scene_.item_map().contains(output_node) && scene_.item_map().contains(input_node)) { - scene_.AddEdge(output, input); - } -} - -void NodeView::RemoveEdge(Node *output, const NodeInput &input) -{ - scene_.RemoveEdge(output, input); -} - -void NodeView::AddNodePosition(Node *node, Node *relative) -{ - bool listening_to_node = filter_nodes_.contains(relative); - - if (!listening_to_node) { - if (filter_mode_ == kFilterShowAll) { - // We're not listening to this context, but because we're showing all, add it - filter_nodes_.append(relative); - } else { - // Ignore signal - return; - } - } - - // Reposition contexts because one of their heights may have changed or a new one may have been - // added - UpdateNodeItem(node); - - if (filter_mode_ == kFilterShowAll) { - queue_reposition_contexts_ = true; - viewport()->update(); - } -} - -void NodeView::RemoveNodePosition(Node *node, Node *relative) -{ - if (filter_nodes_.contains(relative)) { - NodeViewItem *item = scene_.item_map().value(node); - - if (item && !item->GetPreventRemoving()) { - // Determine if any other contexts have this node - bool found = false; - - for (Node *context : qAsConst(filter_nodes_)) { - if (graph_->ContextContainsNode(node, context)) { - found = true; - break; - } - } - - if (!found) { - RemoveNode(node); - } - } - - if (filter_mode_ == kFilterShowAll) { - RepositionContexts(); + // Find first viewer in list of selected nodes and open it + foreach (Node *n, selected_nodes_) { + if (ViewerOutput* viewer = dynamic_cast(n)) { + Core::instance()->OpenNodeInViewer(viewer); + break; } } } @@ -1083,6 +840,16 @@ 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()) { @@ -1117,32 +884,25 @@ void NodeView::MoveToScenePoint(const QPointF &pos) centerOn(pos); } -void NodeView::AttachNodesToCursor(const QVector &nodes) +void NodeView::NodeRemovedFromGraph() { - QVector items(nodes.size()); + Node *context = static_cast(sender()); - for (int i=0; i& items) +void NodeView::DetachItemsFromCursor(bool delete_nodes_too) { - DetachItemsFromCursor(); + foreach (const AttachedItem &ai, attached_items_) { + delete ai.item; - if (!items.isEmpty()) { - for (NodeViewItem* i : items) { - attached_items_.append({i, i->pos() - items.first()->pos()}); + if (delete_nodes_too) { + delete ai.node; } - - MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); } -} -void NodeView::DetachItemsFromCursor() -{ attached_items_.clear(); } @@ -1156,7 +916,9 @@ void NodeView::MoveAttachedNodesToCursor(const QPoint& p) QPointF item_pos = mapToScene(p); for (const AttachedItem& i : qAsConst(attached_items_)) { - i.item->setPos(item_pos + i.original_pos); + if (i.item) { + i.item->setPos(item_pos + i.original_pos); + } } } @@ -1207,13 +969,6 @@ bool NodeView::event(QEvent *event) bool NodeView::eventFilter(QObject *object, QEvent *event) { - if (object == viewport() && event->type() == QEvent::Paint) { - if (queue_reposition_contexts_) { - RepositionContexts(); - queue_reposition_contexts_ = false; - } - } - return super::eventFilter(object, event); } @@ -1222,14 +977,18 @@ void NodeView::CopyNodesToClipboardInternal(QXmlStreamWriter *writer, const QVec writer->writeStartElement(QStringLiteral("pos")); for (Node *n : nodes) { - NodeViewItem *item = scene_.item_map().value(n); - QPointF pos = item->GetNodePosition(); + NodeViewItem *item = GetAssumedItemForSelectedNode(n); - writer->writeStartElement(QStringLiteral("node")); - writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(n))); - writer->writeTextElement(QStringLiteral("x"), QString::number(pos.x())); - writer->writeTextElement(QStringLiteral("y"), QString::number(pos.y())); - writer->writeEndElement(); // node + if (item) { + Node::Position pos = item->GetNodePositionData(); + + writer->writeStartElement(QStringLiteral("node")); + writer->writeAttribute(QStringLiteral("ptr"), QString::number(reinterpret_cast(n))); + writer->writeTextElement(QStringLiteral("x"), QString::number(pos.position.x())); + writer->writeTextElement(QStringLiteral("y"), QString::number(pos.position.y())); + writer->writeTextElement(QStringLiteral("expanded"), QString::number(pos.expanded)); + writer->writeEndElement(); // node + } } writer->writeEndElement(); // pos @@ -1237,14 +996,14 @@ void NodeView::CopyNodesToClipboardInternal(QXmlStreamWriter *writer, const QVec void NodeView::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, void *userdata) { - NodeGraph::PositionMap *map = static_cast(userdata); + Node::PositionMap *map = static_cast(userdata); while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("pos")) { while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("node")) { Node *n = nullptr; - QPointF pos; + Node::Position pos; XMLAttributeLoop(reader, attr) { if (attr.name() == QStringLiteral("ptr")) { @@ -1255,9 +1014,11 @@ void NodeView::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNode while (XMLReadNextStartElement(reader)) { if (reader->name() == QStringLiteral("x")) { - pos.setX(reader->readElementText().toDouble()); + pos.position.setX(reader->readElementText().toDouble()); } else if (reader->name() == QStringLiteral("y")) { - pos.setY(reader->readElementText().toDouble()); + pos.position.setY(reader->readElementText().toDouble()); + } else if (reader->name() == QStringLiteral("expanded")) { + pos.expanded = reader->readElementText().toInt(); } else { reader->skipCurrentElement(); } @@ -1276,6 +1037,13 @@ void NodeView::PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNode } } +void NodeView::changeEvent(QEvent *e) +{ + // Add translation code + + super::changeEvent(e); +} + void NodeView::ZoomFromKeyboard(double multiplier) { QPoint cursor_pos = mapFromGlobal(QCursor::pos()); @@ -1288,145 +1056,10 @@ void NodeView::ZoomFromKeyboard(double multiplier) ZoomIntoCursorPosition(nullptr, multiplier, cursor_pos); } -bool NodeView::DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge) +void NodeView::ClearCreateEdgeInputIfNecessary() { - // Determines whether `node` outputs to another node in `context` besides `source` - for (const Node::OutputConnection &conn : node->output_connections()) { - Node *output_candidate = conn.second.node(); - - if (output_candidate == source) { - continue; - } - - if (graph_->ContextContainsNode(output_candidate, context)) { - if (!output_candidate->OutputsTo(source, true, removed_edges, added_edge)) { - return true; - } - } - } - - return false; -} - -void NodeView::UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges) -{ - // For each edge we remove, determine if we should remove the node from a context as well - for (const Node::OutputConnection &edge : remove_edges) { - Node *output_node = edge.first; - QVector contexts_to_remove_from; - int contexts_containing = 0; - - for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { - Node *context = it.key(); - - if (it.value().contains(output_node)) { - bool currently_outputs = output_node->OutputsTo(context, true); - bool will_output_after_operation = output_node->OutputsTo(context, true, remove_edges); - - if (currently_outputs && !will_output_after_operation) { - // Will remove - contexts_to_remove_from.append(context); - } - - contexts_containing++; - } - } - - // Removing from all current contexts, convert to a floating node (i.e. don't remove from the context) - if (contexts_to_remove_from.size() != contexts_containing) { - // Not removing from all contexts, can remove - bool removing_from_all_current_contexts = true; - - for (Node *context : qAsConst(filter_nodes_)) { - if (graph_->ContextContainsNode(output_node, context)) { - if (!contexts_to_remove_from.contains(context)) { - removing_from_all_current_contexts = false; - break; - } - } - } - - for (Node *context : qAsConst(contexts_to_remove_from)) { - RecursivelyRemoveFloatingNodeFromContext(command, output_node, context, output_node, remove_edges, Node::OutputConnection(), removing_from_all_current_contexts); - } - } - } -} - -void NodeView::RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing) -{ - if (prevent_removing) { - command->add_child(new NodeViewItemPreventRemovingCommand(this, node, true)); - } - - command->add_child(new NodeRemovePositionFromContextCommand(node, context)); - - // Remove any dependency from the context that's also floating - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - Node *dependency = it->second; - - // Determine if this node happens to output to anything else in the context (which may be - // another floating node that won't be removed by this operation) - if (!DetermineIfNodeIsFloatingInContext(dependency, context, source, removed_edges, added_edge)) { - RecursivelyRemoveFloatingNodeFromContext(command, dependency, context, source, removed_edges, added_edge, prevent_removing); - } - } -} - -void NodeView::RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context) -{ - NodeViewItem *item = scene_.item_map().value(node); - - if (item) { - command->add_child(new NodeSetPositionCommand(node, context, GetEstimatedPositionForContext(item, context), false)); - - // Add dependency - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - Node *dependency = it->second; - RecursivelyAddNodeToContext(command, dependency, context); - } - } -} - -void NodeView::UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges) -{ - // Determine if node currently does NOT output to a context that it WILL after this operation - QVector contexts_to_add_to; - Node *connecting_node = added_edge.first; - Node *input_node = added_edge.second.node(); - for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { - if (it.value().contains(input_node)) { - contexts_to_add_to.append(it.key()); - } - } - - if (!contexts_to_add_to.isEmpty()) { - // Determine whether the node is currently "floating", i.e. it outputs to none of the contexts - // that it currently belongs to. If so, we will take ownership of it with this node. - bool node_is_floating = true; - QVector current_contexts; - for (auto it=graph_->GetPositionMap().cbegin(); it!=graph_->GetPositionMap().cend(); it++) { - if (it.value().contains(connecting_node)) { - if (connecting_node->OutputsTo(it.key(), true, removed_edges)) { - node_is_floating = false; - break; - } else { - current_contexts.append(it.key()); - } - } - } - - if (node_is_floating) { - // This action will unfloat this node, so remove it from all current contexts - for (Node *context : qAsConst(current_contexts)) { - RecursivelyRemoveFloatingNodeFromContext(command, connecting_node, context, connecting_node, removed_edges, added_edge, false); - } - } - - // Add nodes to contexts - for (Node *context : qAsConst(contexts_to_add_to)) { - RecursivelyAddNodeToContext(command, connecting_node, context); - } + if (create_edge_from_output_ && create_edge_input_.IsValid()) { + create_edge_input_.Reset(); } } @@ -1435,6 +1068,29 @@ QPointF NodeView::GetEstimatedPositionForContext(NodeViewItem *item, Node *conte 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; +} + +Node::Position NodeView::GetAssumedPositionForSelectedNode(Node *node) +{ + if (NodeViewItem *item = GetAssumedItemForSelectedNode(node)) { + return item->GetNodePositionData(); + } else { + return Node::Position(); + } +} + Menu *NodeView::CreateAddMenu(Menu *parent) { Menu* add_menu = NodeFactory::CreateMenu(parent); @@ -1443,20 +1099,6 @@ Menu *NodeView::CreateAddMenu(Menu *parent) return add_menu; } -void NodeView::CreateNewEdge(NodeViewItem *output_item, const QPoint &mouse_pos) -{ - create_edge_ = new NodeViewEdge(); - create_edge_src_ = output_item; - create_edge_already_exists_ = false; - - create_edge_->SetCurved(scene_.GetEdgesAreCurved()); - create_edge_->SetFlowDirection(scene_.GetFlowDirection()); - - scene_.addItem(create_edge_); - - PositionNewEdge(mouse_pos); -} - void NodeView::PositionNewEdge(const QPoint &pos) { // Determine scene coordinate @@ -1465,276 +1107,321 @@ void NodeView::PositionNewEdge(const QPoint &pos) // Find if the cursor is currently inside an item NodeViewItem* item_at_cursor = dynamic_cast(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 == create_edge_src_) { + if (item_at_cursor && item_at_cursor->GetNode() == source_item->GetNode()) { item_at_cursor = nullptr; } - // Filter out connecting to a node that connects to us - if (item_at_cursor && item_at_cursor->GetNode()->OutputsTo(create_edge_src_->GetNode(), true)) { + // 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->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_ && item_at_cursor->GetNode()->OutputsTo(source_item->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 != create_edge_dst_) { + if (item_at_cursor != opposing_item) { // If we had a destination active, disconnect from it since the item has changed - if (create_edge_dst_) { - create_edge_dst_->SetHighlightedIndex(-1); - - if (create_edge_dst_temp_expanded_) { - // We expanded this item, so we can un-expand it - create_edge_dst_->SetExpanded(false); - create_edge_dst_->setZValue(0); - } + if (opposing_item) { + opposing_item->SetHighlighted(false); + opposing_item = nullptr; } - // Set destination - create_edge_dst_ = item_at_cursor; + // Clear cached input + ClearCreateEdgeInputIfNecessary(); - // If our destination is an item, ensure it's expanded - if (create_edge_dst_) { - if ((create_edge_dst_temp_expanded_ = (!create_edge_dst_->IsExpanded()))) { - create_edge_dst_->SetExpanded(true, true); - create_edge_dst_->setZValue(100); // Ensure item is in front + // 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(); } } } - // If we have a destination, highlight the appropriate input - int highlight_index = -1; - if (create_edge_dst_) { - highlight_index = create_edge_dst_->GetIndexAt(scene_pt); - create_edge_dst_->SetHighlightedIndex(highlight_index); - } + 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; - if (highlight_index >= 0) { - create_edge_dst_input_ = create_edge_dst_->GetInputAtIndex(highlight_index); - create_edge_->SetPoints(create_edge_src_->GetOutputPoint(), - create_edge_dst_->GetInputPoint(create_edge_dst_input_.input(), create_edge_dst_input_.element(), create_edge_src_->pos()), - true); - } else { - create_edge_dst_input_.Reset(); - create_edge_->SetPoints(create_edge_src_->GetOutputPoint(), - scene_pt, - false); - } - - // Set connected to whether we have a valid input destination - create_edge_->SetConnected(create_edge_dst_input_.IsValid()); + create_edge_->SetPoints(output_point, input_point); + create_edge_->SetConnected(create_edge_output_item_ && create_edge_input_.IsValid()); } -void NodeView::RepositionContexts() +void NodeView::GroupNodes() { - // Determine which contexts are root-level - QVector processing_filters = filter_nodes_; + // Get items + QVector items = scene_.GetSelectedItems(); + if (items.isEmpty()) { + return; + } - // Level counter as we iterate through the list a few times - int level = 0; + // 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; + } - // Root-level positioning variables - qreal last_offset = 0; - int additional_spacing = 0; + avg_pos += items.at(i)->GetNodePosition(); + } + avg_pos /= items.size(); - while (!processing_filters.isEmpty()) { - QVector contexts_on_this_level; + // Create group + NodeGroup *group = new NodeGroup(); - for (int i=0; i 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))); - if (i != j && graph_->ContextContainsNode(context, other_context)) { - this_level = false; + 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 + foreach (Node *potential_in, nodes_to_group) { + if (potential_in != n && !n->OutputsTo(potential_in, false)) { + output_passthrough = n; break; } } - - if (this_level) { - contexts_on_this_level.append(context); - } } - - for (Node *context : qAsConst(contexts_on_this_level)) { - if (level == 0) { - const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); - - // First determine the total "height" of this graph and how much we need to offset it - qreal top = 0; - qreal bottom = 0; - for (auto it=map.cbegin(); it!=map.cend(); it++) { - const QPointF &node_pos_in_context = it.value(); - top = qMin(node_pos_in_context.y(), top); - bottom = qMax(node_pos_in_context.y(), bottom); - } - - last_offset += (additional_spacing + (bottom - top)); - additional_spacing = 1; - context_offsets_.insert(context, QPointF(0, last_offset)); - } else { - // Create/update item - NodeViewItem *item = UpdateNodeItem(context, true); - - // Get position generated by UpdateNodeItem - QPointF context_pos = item->GetNodePosition(); - - // Adjust by the context node's position in its own context (this will usually be 0,0) - context_pos -= graph_->GetNodesForContext(context).value(context); - - // Insert this context's offset - context_offsets_.insert(context, context_pos); - } - - // Remove from list so we don't process again - processing_filters.removeOne(context); - } - - level++; } - // Now that we've positioned all the contexts, position all other nodes relative to those contexts - for (Node *context : qAsConst(filter_nodes_)) { - const NodeGraph::PositionMap &map = graph_->GetNodesForContext(context); - for (auto it=map.cbegin(); it!=map.cend(); it++) { - UpdateNodeItem(it.key()); + // 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); +} + +void NodeView::UngroupNodes() +{ + NodeViewItem *group_item = nullptr; + QVector items = scene_.GetSelectedItems(); + if (items.isEmpty()) { + return; + } + + NodeGroup *group; + foreach (NodeViewItem *i, items) { + if ((group = dynamic_cast(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); +} + +void NodeView::ShowNodeProperties() +{ + Node *first_node = selected_nodes_.first(); + + if (NodeGroup *group = dynamic_cast(first_node)) { + emit NodeGroupOpenRequested(group); + } else { + LabelSelectedNodes(); + } } -NodeViewItem *NodeView::UpdateNodeItem(Node *node, bool ignore_own_context) +void NodeView::LabelSelectedNodes() { - // Get UI item or create if it doesn't exist - NodeViewItem *item = scene_.item_map().value(node); - if (!item) { - item = scene_.AddNode(node); - - for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { - if (scene_.item_map().contains(it->second)) { - scene_.AddEdge(it->second, it->first); - } - } - - for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { - if (scene_.item_map().contains(it->second.node())) { - scene_.AddEdge(it->first, it->second); - } - } - } - - // Determine "view" position by averaging the Y value and "min"ing the X value of all contexts - QPointF item_pos(std::numeric_limits::max(), 0.0); - int average_count = 0; - for (Node *context : qAsConst(filter_nodes_)) { - if (context == node && ignore_own_context) { - continue; - } - - if (graph_->GetNodesForContext(context).contains(node)) { - QPointF this_context_pos = graph_->GetNodePosition(node, context); - this_context_pos += context_offsets_.value(context); - - item_pos.setX(qMin(item_pos.x(), this_context_pos.x())); - item_pos.setY(item_pos.y() + this_context_pos.y()); - average_count++; - } - } - item_pos.setY(item_pos.y() / average_count); - - // Set position - item->SetNodePosition(item_pos); - positions_.insert(item, {node, item_pos}); - - return item; + Core::instance()->LabelNodes(selected_nodes_); } void NodeView::PasteNodesInternal(const QVector &duplicate_nodes) { // If no graph, do nothing - if (!graph_) { + if (contexts_.isEmpty()) { return; } - paste_command_ = new MultiUndoCommand(); - // If duplicating nodes, duplicate, otherwise paste QVector new_nodes; - NodeGraph::PositionMap map; + Node::PositionMap map; if (duplicate_nodes.isEmpty()) { - new_nodes = PasteNodesFromClipboard(graph_, paste_command_, &map); - - for (auto it=new_nodes.cbegin(); it!=new_nodes.cend(); it++) { - for (Node *context : qAsConst(filter_nodes_)) { - paste_command_->add_child(new NodeSetPositionCommand(*it, context, map.value(*it), false)); - } - } + new_nodes = PasteNodesFromClipboard(nullptr, nullptr, &map); } else { - new_nodes = Node::CopyDependencyGraph(duplicate_nodes, paste_command_); + new_nodes.resize(selected_nodes_.size()); - for (int i=0; iGetNodePosition(); - paste_command_->add_child(new NodeSetPositionCommand(copy, context, p, false)); - } + for (int i=0; icopy(); + Node::CopyInputs(og, copy, false); + map.insert(copy, GetAssumedPositionForSelectedNode(og)); + new_nodes[i] = copy; } + + Node::CopyDependencyGraph(selected_nodes_, new_nodes, nullptr); } // If no nodes were retrieved, do nothing - if (new_nodes.isEmpty()) { - delete paste_command_; - paste_command_ = nullptr; - return; - } + if (!new_nodes.isEmpty()) { + QVector new_attached; - // Attach nodes to cursor - paste_command_->add_child(new NodeViewAttachNodesToCursor(this, new_nodes)); + NodeViewItem *first_item = nullptr; - paste_command_->redo_now(); -} + for (int i=0; i &nodes) : - view_(view), - nodes_(nodes) -{ -} + // Determine if item had a position, if not don't create an item for it + NodeViewItem *new_item; -void NodeView::NodeViewAttachNodesToCursor::redo() -{ - view_->AttachNodesToCursor(nodes_); -} + 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); -void NodeView::NodeViewAttachNodesToCursor::undo() -{ - view_->DetachItemsFromCursor(); -} + if (!first_item) { + first_item = new_item; + } + } else { + new_item = nullptr; + } -Project *NodeView::NodeViewAttachNodesToCursor::GetRelevantProject() const -{ - // Will either return a project or a nullptr which is also acceptable - return dynamic_cast(view_->graph_); -} + new_attached.append({new_item, node, QPointF(0, 0)}); + } -void NodeView::NodeViewItemPreventRemovingCommand::redo() -{ - NodeViewItem *item = view_->scene_.item_map().value(node_); + // Correct positions + if (first_item) { + for (int i=0; iGetPreventRemoving(); - item->SetPreventRemoving(new_prevent_removing_); + if (ai.item) { + ai.original_pos = first_item->pos() - ai.item->pos(); + } + } + } + + SetAttachedItems(new_attached); } } -void NodeView::NodeViewItemPreventRemovingCommand::undo() +void NodeView::AddContext(Node *n) { - NodeViewItem *item = view_->scene_.item_map().value(node_); + scene_.AddContext(n); + connect(n, &Node::RemovedFromGraph, this, &NodeView::NodeRemovedFromGraph); +} - if (item) { - item->SetPreventRemoving(old_prevent_removing_); +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::SetAttachedItems(const QVector &items) +{ + // Detach anything currently attached + DetachItemsFromCursor(); + + attached_items_ = items; + + // Move to cursor + MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); } } diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index db03afc08..77a188adc 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -24,9 +24,11 @@ #include #include +#include "core.h" #include "node/graph.h" #include "node/nodecopypaste.h" #include "nodeviewedge.h" +#include "nodeviewcontext.h" #include "nodeviewminimap.h" #include "nodeviewscene.h" #include "widget/handmovableview/handmovableview.h" @@ -44,16 +46,18 @@ class NodeView : public HandMovableView, public NodeCopyPasteService { Q_OBJECT public: - NodeView(QWidget* parent); + NodeView(QWidget* parent = nullptr); virtual ~NodeView() override; - NodeGraph* GetGraph() const + void SetContexts(const QVector &nodes); + + const QVector &GetContexts() const { - return graph_; + return contexts_; } - void SetGraph(NodeGraph *graph, const QVector &nodes); + void CloseContextsBelongingToProject(Project *project); void ClearGraph(); @@ -65,8 +69,7 @@ public: void SelectAll(); void DeselectAll(); - void Select(QVector nodes, bool center_view_on_item); - void SelectWithDependencies(QVector nodes, bool center_view_on_item); + void Select(const QVector &nodes, bool center_view_on_item); void CopySelected(bool cut); void Paste(); @@ -81,7 +84,7 @@ public: const QVector &GetCurrentContexts() const { - return filter_nodes_; + return contexts_; } public slots: @@ -97,17 +100,24 @@ public slots: delete m; } + void CenterOnItemsBoundingRect(); + + void CenterOnNode(olive::Node *n); + signals: void NodesSelected(const QVector& nodes); void NodesDeselected(const QVector& nodes); + void NodeGroupOpenRequested(NodeGroup *group); + protected: virtual void keyPressEvent(QKeyEvent *event) override; virtual void mousePressEvent(QMouseEvent *event) override; virtual void mouseMoveEvent(QMouseEvent *event) override; virtual void mouseReleaseEvent(QMouseEvent* event) override; + virtual void mouseDoubleClickEvent(QMouseEvent* event) override; virtual void resizeEvent(QResizeEvent *event) override; @@ -120,12 +130,10 @@ protected: virtual void CopyNodesToClipboardInternal(QXmlStreamWriter *writer, const QVector &nodes, void* userdata) override; virtual void PasteNodesFromClipboardInternal(QXmlStreamReader *reader, XMLNodeData &xml_node_data, void* userdata) override; + virtual void changeEvent(QEvent *e) override; + private: - void AttachNodesToCursor(const QVector &nodes); - - void AttachItemsToCursor(const QVector &items); - - void DetachItemsFromCursor(); + void DetachItemsFromCursor(bool delete_nodes_too = true); void SetFlowDirection(NodeViewCommon::FlowDirection dir); @@ -136,120 +144,64 @@ private: void ZoomFromKeyboard(double multiplier); - bool DetermineIfNodeIsFloatingInContext(Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge); - void UpdateContextsFromEdgeRemove(MultiUndoCommand *command, const Node::OutputConnections &remove_edges); - void UpdateContextsFromEdgeAdd(MultiUndoCommand *command, const Node::OutputConnection &added_edge, const Node::OutputConnections &removed_edges = Node::OutputConnections()); - void RecursivelyAddNodeToContext(MultiUndoCommand *command, Node *node, Node *context); - void RecursivelyRemoveFloatingNodeFromContext(MultiUndoCommand *command, Node *node, Node *context, Node *source, const Node::OutputConnections &removed_edges, const Node::OutputConnection &added_edge, bool prevent_removing); + void ClearCreateEdgeInputIfNecessary(); QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const; - Menu *CreateAddMenu(Menu *parent); + NodeViewItem *GetAssumedItemForSelectedNode(Node *node); + Node::Position GetAssumedPositionForSelectedNode(Node *node); - void CreateNewEdge(NodeViewItem *output_item, const QPoint &mouse_pos); + Menu *CreateAddMenu(Menu *parent); void PositionNewEdge(const QPoint &pos); - NodeViewItem *UpdateNodeItem(Node *node, bool ignore_own_context = false); - void PasteNodesInternal(const QVector &duplicate_nodes = QVector()); - class NodeViewAttachNodesToCursor : public UndoCommand - { - public: - NodeViewAttachNodesToCursor(NodeView* view, const QVector& nodes); + void AddContext(Node *n); - virtual Project * GetRelevantProject() const override; + void RemoveContext(Node *n); - protected: - virtual void redo() override; + bool IsItemAttachedToCursor(NodeViewItem *item) const; - virtual void undo() override; + void ExpandItem(NodeViewItem *item); - private: - NodeView* view_; - - QVector nodes_; - - }; + void CollapseItem(NodeViewItem *item); NodeViewMiniMap *minimap_; - NodeGraph* graph_; - struct AttachedItem { NodeViewItem* item; + Node *node; QPointF original_pos; }; - class NodeViewItemPreventRemovingCommand : public UndoCommand - { - public: - NodeViewItemPreventRemovingCommand(NodeView *view, Node *node, bool prevent_removing) : - view_(view), - node_(node), - new_prevent_removing_(prevent_removing) - {} - - virtual Project * GetRelevantProject() const override - { - return node_->project(); - } - - protected: - virtual void redo() override; - - virtual void undo() override; - - private: - NodeView *view_; - Node *node_; - bool new_prevent_removing_; - bool old_prevent_removing_; - - }; - - QList attached_items_; + void SetAttachedItems(const QVector &items); + QVector attached_items_; NodeViewEdge* drop_edge_; NodeInput drop_input_; NodeViewEdge* create_edge_; - NodeViewItem* create_edge_src_; - NodeViewItem* create_edge_dst_; - NodeInput create_edge_dst_input_; - bool create_edge_dst_temp_expanded_; + NodeViewItem* create_edge_output_item_; + NodeViewItem* create_edge_input_item_; + NodeInput create_edge_input_; + bool create_edge_already_exists_; + bool create_edge_from_output_; + + QVector create_edge_expanded_items_; NodeViewScene scene_; - MultiUndoCommand* paste_command_; - QVector selected_nodes_; - enum FilterMode { - kFilterShowAll, - kFilterShowSelective - }; - - struct Position { - Node *node; - QPointF original_item_pos; - }; - - QMap positions_; - - FilterMode filter_mode_; - - QVector filter_nodes_; + QVector contexts_; QVector last_set_filter_nodes_; QMap context_offsets_; + QMap dragging_items_; + double scale_; - bool create_edge_already_exists_; - - bool queue_reposition_contexts_; - static const double kMinimumScale; private slots: @@ -273,35 +225,28 @@ private slots: */ void ContextMenuSetDirection(QAction* action); - /** - * @brief Receiver for the user changing the filter - */ - void ContextMenuFilterChanged(QAction* action); - /** * @brief Opens the selected node in a Viewer */ void OpenSelectedNodeInViewer(); - //void AddNode(Node *node); - void RemoveNode(Node *node); - void AddEdge(Node *output, const NodeInput& input); - void RemoveEdge(Node *output, const NodeInput& input); - - void AddNodePosition(Node *node, Node *relative); - void RemoveNodePosition(Node *node, Node *relative); - void UpdateSceneBoundingRect(); - void CenterOnItemsBoundingRect(); - void RepositionMiniMap(); void UpdateViewportOnMiniMap(); void MoveToScenePoint(const QPointF &pos); - void RepositionContexts(); + void NodeRemovedFromGraph(); + + void GroupNodes(); + + void UngroupNodes(); + + void ShowNodeProperties(); + + void LabelSelectedNodes(); }; diff --git a/app/widget/nodeview/nodeviewcommon.h b/app/widget/nodeview/nodeviewcommon.h index 03c5f8dcf..f8ffa4583 100644 --- a/app/widget/nodeview/nodeviewcommon.h +++ b/app/widget/nodeview/nodeviewcommon.h @@ -30,6 +30,7 @@ namespace olive { class NodeViewCommon { public: enum FlowDirection { + kInvalidDirection = -1, kTopToBottom, kBottomToTop, kLeftToRight, @@ -44,6 +45,16 @@ public: } } + static bool IsFlowVertical(FlowDirection dir) + { + return dir == kTopToBottom || dir == kBottomToTop; + } + + static bool IsFlowHorizontal(FlowDirection dir) + { + return dir == kLeftToRight || dir == kRightToLeft; + } + static bool DirectionsAreOpposing(FlowDirection a, FlowDirection b) { return ((a == NodeViewCommon::kLeftToRight && b == NodeViewCommon::kRightToLeft) || (a == NodeViewCommon::kRightToLeft && b == NodeViewCommon::kLeftToRight) diff --git a/app/widget/nodeview/nodeviewcontext.cpp b/app/widget/nodeview/nodeviewcontext.cpp new file mode 100644 index 000000000..054624a11 --- /dev/null +++ b/app/widget/nodeview/nodeviewcontext.cpp @@ -0,0 +1,339 @@ +#include "nodeviewcontext.h" + +#include +#include +#include +#include +#include +#include + +#include "core.h" +#include "node/block/block.h" +#include "node/graph.h" +#include "node/output/track/track.h" +#include "node/project/sequence/sequence.h" +#include "nodeviewitem.h" +#include "ui/colorcoding.h" + +namespace olive { + +#define super QGraphicsRectItem + +NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item) : + super(item), + context_(context) +{ + if (Block *block = dynamic_cast(context_)) { + rational timebase = block->track()->sequence()->GetVideoParams().frame_rate_as_time_base(); + lbl_ = QCoreApplication::translate("NodeViewContext", + "%1 [%2] :: %3 - %4").arg(block->GetLabelAndName(), + Track::Reference::TypeToTranslatedString(block->track()->type()), + Timecode::time_to_timecode(block->in(), timebase, Core::instance()->GetTimecodeDisplay()), + Timecode::time_to_timecode(block->out(), timebase, Core::instance()->GetTimecodeDisplay())); + } else { + lbl_ = context_->GetLabelAndName(); + } + + const Node::PositionMap &map = context_->GetContextPositions(); + for (auto it=map.cbegin(); it!=map.cend(); it++) { + AddChild(it.key()); + } + + connect(context_, &Node::NodeAddedToContext, this, &NodeViewContext::AddChild, Qt::DirectConnection); + connect(context_, &Node::NodePositionInContextChanged, this, &NodeViewContext::SetChildPosition, Qt::DirectConnection); + connect(context_, &Node::NodeRemovedFromContext, this, &NodeViewContext::RemoveChild, Qt::DirectConnection); +} + +void NodeViewContext::AddChild(Node *node) +{ + if (!context_) { + return; + } + + NodeViewItem *item = new NodeViewItem(node, context_, this); + item->SetFlowDirection(flow_dir_); + + AddNodeInternal(node, item); + + if (NodeGroup *group = dynamic_cast(node)) { + for (auto it=group->GetContextPositions().cbegin(); it!=group->GetContextPositions().cend(); it++) { + // Use this item as the representative for all of these nodes too + AddNodeInternal(it.key(), item); + } + + connect(group, &NodeGroup::NodeAddedToContext, this, &NodeViewContext::GroupAddedNode); + connect(group, &NodeGroup::NodeRemovedFromContext, this, &NodeViewContext::GroupRemovedNode); + } + + UpdateRect(); +} + +void NodeViewContext::SetChildPosition(Node *node, const QPointF &pos) +{ + item_map_.value(node)->SetNodePosition(pos); +} + +void NodeViewContext::RemoveChild(Node *node) +{ + disconnect(node, &Node::InputConnected, this, &NodeViewContext::ChildInputConnected); + disconnect(node, &Node::InputDisconnected, this, &NodeViewContext::ChildInputDisconnected); + + if (NodeGroup *group = dynamic_cast(node)) { + disconnect(group, &NodeGroup::NodeAddedToContext, this, &NodeViewContext::GroupAddedNode); + disconnect(group, &NodeGroup::NodeRemovedFromContext, this, &NodeViewContext::GroupRemovedNode); + } + + NodeViewItem *item = item_map_.take(node); + + // Delete edges first because the edge destructor will try to reference item (maybe that should + // be changed...) + QVector edges_to_remove = item->GetAllEdgesRecursively(); + foreach (NodeViewEdge *edge, edges_to_remove) { + if (node == item->GetNode() || edge->output() == node || edge->input().node() == node) { + ChildInputDisconnected(edge->output(), edge->input()); + } + } + + // Check if this item is specifically for this node and the node is a group. If so, remove it for + // all other entries in the map. + if (item->GetNode() == node) { + if (dynamic_cast(item->GetNode())) { + for (auto it=item_map_.begin(); it!=item_map_.end(); ) { + if (it.value() == item) { + it = item_map_.erase(it); + } else { + it++; + } + } + } + + delete item; + } +} + +void NodeViewContext::ChildInputConnected(Node *output, const NodeInput &input) +{ + // Add edge + if (!input.IsHidden()) { + if (NodeViewItem* output_item = item_map_.value(output)) { + AddEdgeInternal(output, input, output_item, item_map_.value(input.node())->GetItemForInput(input)); + } + } +} + +bool NodeViewContext::ChildInputDisconnected(Node *output, const NodeInput &input) +{ + // Remove edge + for (int i=0; ioutput() == output && e->input() == input) { + delete e; + edges_.removeAt(i); + return true; + } + } + + return false; +} + +qreal GetTextOffset(const QFontMetricsF &fm) +{ + return fm.height()/2; +} + +void NodeViewContext::UpdateRect() +{ + QFont f; + QFontMetricsF fm(f); + qreal lbl_offset = GetTextOffset(fm); + + QRectF cbr = childrenBoundingRect(); + QRectF rect = cbr; + int pad = NodeViewItem::DefaultItemHeight(); + rect.adjust(-pad, - lbl_offset*2 - fm.height() - pad, pad, pad); + setRect(rect); + + last_titlebar_height_ = rect.y() + (cbr.y() - rect.y()) - pad; +} + +void NodeViewContext::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + flow_dir_ = dir; + + foreach (NodeViewItem *item, item_map_) { + item->SetFlowDirection(dir); + } +} + +void NodeViewContext::SetCurvedEdges(bool e) +{ + curved_edges_ = e; + + foreach (NodeViewEdge *edge, edges_) { + edge->SetCurved(e); + } +} + +void NodeViewContext::DeleteSelected(NodeViewDeleteCommand *command) +{ + // Delete any selected edges + foreach (NodeViewEdge *edge, edges_) { + if (edge->isSelected()) { + command->AddEdge(edge->output(), edge->input()); + } + } + + // Delete any selected nodes + foreach (NodeViewItem *node, item_map_) { + if (node->isSelected()) { + command->AddNode(node->GetNode(), context_); + } + } + + UpdateRect(); +} + +void NodeViewContext::Select(const QVector &nodes) +{ + foreach (Node *n, nodes) { + if (NodeViewItem *item = item_map_.value(n)) { + item->setSelected(true); + } + } +} + +QVector NodeViewContext::GetSelectedItems() const +{ + QVector items; + + for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { + if (it.value()->isSelected()) { + if (!items.contains(it.value())) { + items.append(it.value()); + } + } + } + + return items; +} + +QPointF NodeViewContext::MapScenePosToNodePosInContext(const QPointF &pos) const +{ + for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { + QPointF pos_inside_parent = it.value()->mapToParent(it.value()->mapFromScene(pos)); + return NodeViewItem::ScreenToNodePoint(pos_inside_parent, flow_dir_); + } + return QPointF(0, 0); +} + +void NodeViewContext::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + // Set pen and brush + Color color = context_->color(); + QColor c = color.toQColor(); + QPen pen(c, 2); + if (option->state & QStyle::State_Selected) { + pen.setStyle(Qt::DotLine); + } + painter->setPen(pen); + + QColor bg = c; + bg.setAlpha(128); + painter->setBrush(bg); + + // Draw semi-transparent rect for whole item + int rounded = painter->fontMetrics().height(); + painter->drawRoundedRect(rect(), rounded, rounded); + + // Draw solid background for titlebar + QRectF titlebar_rect = rect(); + titlebar_rect.setHeight(last_titlebar_height_ - rect().top()); + painter->setClipRect(titlebar_rect); + painter->setBrush(c); + painter->drawRoundedRect(rect(), rounded, rounded); + painter->setClipping(false); + + // Draw titlebar text + painter->setPen(ColorCoding::GetUISelectorColor(color)); + + int offset = GetTextOffset(painter->fontMetrics()); + + QRectF text_rect = rect(); + text_rect.adjust(offset, offset, -offset, -offset); + painter->drawText(text_rect, lbl_); +} + +QVariant NodeViewContext::itemChange(GraphicsItemChange change, const QVariant &value) +{ + return super::itemChange(change, value); +} + +void NodeViewContext::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + bool clicked_inside_titlebar = (event->pos().y() < last_titlebar_height_); + + setFlag(ItemIsMovable, clicked_inside_titlebar); + setFlag(ItemIsSelectable, clicked_inside_titlebar); + + super::mousePressEvent(event); +} + +void NodeViewContext::AddNodeInternal(Node *node, NodeViewItem *item) +{ + connect(node, &Node::InputConnected, this, &NodeViewContext::ChildInputConnected); + connect(node, &Node::InputDisconnected, this, &NodeViewContext::ChildInputDisconnected); + + item_map_.insert(node, item); + + if (node == context_) { + item->SetLabelAsOutput(true); + } + + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (!it->second.IsHidden()) { + if (NodeViewItem *other_item = item_map_.value(it->second.node())) { + AddEdgeInternal(node, it->second, item, other_item->GetItemForInput(it->second)); + } + } + } + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (!it->first.IsHidden()) { + if (NodeViewItem *other_item = item_map_.value(it->second)) { + AddEdgeInternal(it->second, it->first, other_item, item->GetItemForInput(it->first)); + } + } + } +} + +void NodeViewContext::AddEdgeInternal(Node *output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) +{ + if (from == to) { + return; + } + + NodeViewEdge* edge_ui = new NodeViewEdge(output, input, from, to, this); + + edge_ui->Adjust(); + edge_ui->SetCurved(curved_edges_); + + edges_.append(edge_ui); +} + +void NodeViewContext::GroupAddedNode(Node *node) +{ + NodeGroup *group = static_cast(sender()); + + AddNodeInternal(node, item_map_.value(group)); +} + +void NodeViewContext::GroupRemovedNode(Node *node) +{ + NodeGroup *group = static_cast(sender()); + + if (item_map_.value(node) == item_map_.value(group)) { + item_map_.remove(node); + } +} + +} diff --git a/app/widget/nodeview/nodeviewcontext.h b/app/widget/nodeview/nodeviewcontext.h new file mode 100644 index 000000000..6d191d916 --- /dev/null +++ b/app/widget/nodeview/nodeviewcontext.h @@ -0,0 +1,90 @@ +#ifndef NODEVIEWCONTEXT_H +#define NODEVIEWCONTEXT_H + +#include +#include + +#include "node/node.h" +#include "nodeviewcommon.h" +#include "nodeviewedge.h" +#include "nodeviewundo.h" + +namespace olive { + +class NodeViewContext : public QObject, public QGraphicsRectItem +{ + Q_OBJECT +public: + NodeViewContext(Node *context, QGraphicsItem *item = nullptr); + + Node *GetContext() const + { + return context_; + } + + void UpdateRect(); + + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + void SetCurvedEdges(bool e); + + void DeleteSelected(NodeViewDeleteCommand *command); + + void Select(const QVector &nodes); + + QVector GetSelectedItems() const; + + 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: + void AddChild(Node *node); + + void SetChildPosition(Node *node, const QPointF &pos); + + void RemoveChild(Node *node); + + void ChildInputConnected(Node *output, const NodeInput& input); + + bool ChildInputDisconnected(Node *output, const NodeInput& input); + +protected: + virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; + + virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + +private: + void AddNodeInternal(Node *node, NodeViewItem *item); + + void AddEdgeInternal(Node *output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to); + + Node *context_; + + QString lbl_; + + NodeViewCommon::FlowDirection flow_dir_; + + bool curved_edges_; + + int last_titlebar_height_; + + QMap item_map_; + + QVector edges_; + +private slots: + void GroupAddedNode(Node *node); + + void GroupRemovedNode(Node *node); + +}; + +} + +#endif // NODEVIEWCONTEXT_H diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp index 42dd19e06..0cf408ea1 100644 --- a/app/widget/nodeview/nodeviewedge.cpp +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -46,6 +46,9 @@ NodeViewEdge::NodeViewEdge(Node *output, const NodeInput &input, { Init(); SetConnected(true); + + from_item_->AddEdge(this); + to_item_->AddEdge(this); } NodeViewEdge::NodeViewEdge(QGraphicsItem *parent) : @@ -56,12 +59,51 @@ NodeViewEdge::NodeViewEdge(QGraphicsItem *parent) : Init(); } +NodeViewEdge::~NodeViewEdge() +{ + if (from_item_) { + from_item_->RemoveEdge(this); + } + + if (to_item_) { + to_item_->RemoveEdge(this); + } +} + +void NodeViewEdge::set_from_item(NodeViewItem *i) +{ + if (from_item_) { + from_item_->RemoveEdge(this); + } + + from_item_ = i; + + if (from_item_) { + from_item_->AddEdge(this); + } + + Adjust(); +} + +void NodeViewEdge::set_to_item(NodeViewItem *i) +{ + if (to_item_) { + to_item_->RemoveEdge(this); + } + + to_item_ = i; + + if (to_item_) { + to_item_->AddEdge(this); + } + + Adjust(); +} + void NodeViewEdge::Adjust() { // Draw a line between the two - SetPoints(from_item()->GetOutputPoint(), - to_item()->GetInputPoint(input_.input(), input_.element(), from_item()->pos()), - to_item()->IsExpanded()); + SetPoints(from_item()->GetOutputPoint(), to_item()->GetInputPoint()); } void NodeViewEdge::SetConnected(bool c) @@ -78,24 +120,14 @@ void NodeViewEdge::SetHighlighted(bool e) update(); } -void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool input_is_expanded) +void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end) { cached_start_ = start; cached_end_ = end; - cached_input_is_expanded_ = input_is_expanded; UpdateCurve(); } -void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) -{ - flow_dir_ = dir; - - if (from_item_ && to_item_) { - Adjust(); - } -} - void NodeViewEdge::SetCurved(bool e) { curved_ = e; @@ -126,18 +158,12 @@ void NodeViewEdge::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti painter->setPen(QPen(edge_color, edge_width_)); painter->setBrush(Qt::NoBrush); painter->drawPath(path()); - - // Draw arrow - painter->setPen(Qt::NoPen); - painter->setBrush(edge_color); - painter->drawPolygon(arrow_); } void NodeViewEdge::Init() { connected_ = false; highlighted_ = false; - flow_dir_ = NodeViewCommon::kLeftToRight; curved_ = true; setFlag(QGraphicsItem::ItemIsSelectable); @@ -147,14 +173,12 @@ void NodeViewEdge::Init() // Use font metrics to set edge width for basic high DPI support edge_width_ = QFontMetrics(QFont()).height() / 12; - arrow_size_ = QFontMetrics(QFont()).height() / 2; } void NodeViewEdge::UpdateCurve() { const QPointF &start = cached_start_; const QPointF &end = cached_end_; - const bool input_is_expanded = cached_input_is_expanded_; QPainterPath path; path.moveTo(start); @@ -168,13 +192,26 @@ void NodeViewEdge::UpdateCurve() QPointF cp1, cp2; - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + NodeViewCommon::FlowDirection from_flow = from_item_ ? from_item_->GetFlowDirection() : NodeViewCommon::kInvalidDirection; + NodeViewCommon::FlowDirection to_flow = to_item_ ? to_item_->GetFlowDirection() : NodeViewCommon::kInvalidDirection; + + if (from_flow == NodeViewCommon::kInvalidDirection && to_flow == NodeViewCommon::kInvalidDirection) { + // This is a technically unsupported scenario, but to avoid issues, we'll use a fallback + from_flow = NodeViewCommon::kLeftToRight; + to_flow = NodeViewCommon::kLeftToRight; + } else if (from_flow == NodeViewCommon::kInvalidDirection) { + from_flow = to_flow; + } else if (to_flow == NodeViewCommon::kInvalidDirection) { + to_flow = from_flow; + } + + if (NodeViewCommon::GetFlowOrientation(from_flow) == Qt::Horizontal) { cp1 = QPointF(half_x, start.y()); } else { cp1 = QPointF(start.x(), half_y); } - if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal || input_is_expanded) { + if (NodeViewCommon::GetFlowOrientation(to_flow) == Qt::Horizontal) { cp2 = QPointF(half_x, end.y()); } else { cp2 = QPointF(end.x(), half_y); @@ -183,7 +220,7 @@ void NodeViewEdge::UpdateCurve() path.cubicTo(cp1, cp2, end); if (!qFuzzyCompare(start.x(), end.x())) { - double continue_x = end.x() - qCos(angle)*arrow_size_; + double continue_x = end.x() - qCos(angle); double x1 = start.x(); double x2 = cp1.x(); @@ -213,18 +250,7 @@ void NodeViewEdge::UpdateCurve() } - setPath(path); - - const double arrow_angle = 150.0 * M_PI / 180.0; - QVector arrow_points(4); - arrow_points[0] = end; - arrow_points[1] = end + QPointF(qCos(angle + arrow_angle) * arrow_size_, qSin(angle + arrow_angle) * arrow_size_); - arrow_points[2] = end + QPointF(qCos(angle - arrow_angle) * arrow_size_, qSin(angle - arrow_angle) * arrow_size_); - arrow_points[3] = end; - - arrow_ = QPolygonF(arrow_points); - arrow_bounding_rect_ = arrow_.boundingRect(); - arrow_bounding_rect_.adjust(-arrow_size_, -arrow_size_, arrow_size_, arrow_size_); + setPath(mapFromScene(path)); } } diff --git a/app/widget/nodeview/nodeviewedge.h b/app/widget/nodeview/nodeviewedge.h index 6240aeb64..b149b3680 100644 --- a/app/widget/nodeview/nodeviewedge.h +++ b/app/widget/nodeview/nodeviewedge.h @@ -45,6 +45,8 @@ public: NodeViewEdge(QGraphicsItem* parent = nullptr); + virtual ~NodeViewEdge() override; + Node *output() const { return output_; @@ -70,10 +72,9 @@ public: return to_item_; } - const QRectF arrow_bounding_rect() const - { - return arrow_bounding_rect_; - } + void set_from_item(NodeViewItem *i); + + void set_to_item(NodeViewItem *i); void Adjust(); @@ -104,12 +105,7 @@ public: /** * @brief Set points to create curve from */ - void SetPoints(const QPointF& start, const QPointF& end, bool input_is_expanded); - - /** - * @brief Sets the direction nodes are flowing - */ - void SetFlowDirection(NodeViewCommon::FlowDirection dir); + void SetPoints(const QPointF& start, const QPointF& end); /** * @brief Set whether edges should be drawn as curved or as straight lines @@ -140,19 +136,10 @@ private: bool highlighted_; - NodeViewCommon::FlowDirection flow_dir_; - bool curved_; - QPolygonF arrow_; - - int arrow_size_; - - QRectF arrow_bounding_rect_; - QPointF cached_start_; QPointF cached_end_; - bool cached_input_is_expanded_; }; diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp index 73e2d8019..01c1c6eb6 100644 --- a/app/widget/nodeview/nodeviewitem.cpp +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -39,20 +39,18 @@ namespace olive { -NodeViewItem::NodeViewItem(QGraphicsItem *parent) : +NodeViewItem::NodeViewItem(Node *node, const QString &input, int element, Node *context, QGraphicsItem *parent) : QGraphicsRectItem(parent), - node_(nullptr), + node_(node), + input_(input), + element_(element), + context_(context), expanded_(false), - hide_titlebar_(false), - highlighted_index_(-1), - flow_dir_(NodeViewCommon::kLeftToRight), - prevent_removing_(false) + highlighted_(false), + flow_dir_(NodeViewCommon::kInvalidDirection), + arrow_click_(false), + label_as_output_(false) { - // Set flags for this widget - setFlag(QGraphicsItem::ItemIsMovable); - setFlag(QGraphicsItem::ItemIsSelectable); - setFlag(QGraphicsItem::ItemSendsGeometryChanges); - // // We use font metrics to set all the UI measurements for DPI-awareness // @@ -60,13 +58,48 @@ NodeViewItem::NodeViewItem(QGraphicsItem *parent) : // Set border width node_border_width_ = DefaultItemBorder(); - int widget_width = DefaultItemWidth(); - int widget_height = DefaultItemHeight(); + // Set rect size to default + SetRectSize(); - title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); - setRect(title_bar_rect_); + // Create connector + input_connector_ = new NodeViewItemConnector(false, this); + output_connector_ = new NodeViewItemConnector(true, this); - output_triangle_.resize(3); + connect(node_, &Node::LabelChanged, this, &NodeViewItem::NodeAppearanceChanged); + connect(node_, &Node::ColorChanged, this, &NodeViewItem::NodeAppearanceChanged); + + if (IsOutputItem()) { + connect(node_, &Node::InputAdded, this, &NodeViewItem::RepopulateInputs); + connect(node_, &Node::InputRemoved, this, &NodeViewItem::RepopulateInputs); + RepopulateInputs(); + + // Set flags for this widget + setFlag(QGraphicsItem::ItemSendsGeometryChanges); + setFlag(QGraphicsItem::ItemIsMovable); + setFlag(QGraphicsItem::ItemIsSelectable); + + if (context_) { + SetNodePosition(context_->GetNodePositionDataInContext(node_)); + } + } else { + output_connector_->setVisible(false); + + connect(node_, &Node::InputArraySizeChanged, this, &NodeViewItem::InputArraySizeChanged); + connect(node_, &Node::InputArraySizeChanged, this, &NodeViewItem::InputArraySizeChanged); + } + + // This should be set during runtime, but just in case here's a default fallback + SetFlowDirection(NodeViewCommon::kLeftToRight); +} + +NodeViewItem::~NodeViewItem() +{ + Q_ASSERT(edges_.isEmpty()); +} + +Node::Position NodeViewItem::GetNodePositionData() const +{ + return Node::Position(GetNodePosition(), IsExpanded()); } QPointF NodeViewItem::GetNodePosition() const @@ -81,6 +114,23 @@ void NodeViewItem::SetNodePosition(const QPointF &pos) UpdateNodePosition(); } +void NodeViewItem::SetNodePosition(const Node::Position &pos) +{ + SetNodePosition(pos.position); + SetExpanded(pos.expanded); +} + +QVector NodeViewItem::GetAllEdgesRecursively() const +{ + QVector list = edges_; + + foreach (NodeViewItem *item, children_) { + list.append(item->GetAllEdgesRecursively()); + } + + return list; +} + int NodeViewItem::DefaultTextPadding() { return QFontMetrics(QFont()).height() / 4; @@ -119,6 +169,8 @@ QPointF NodeViewItem::NodeToScreenPoint(QPointF p, NodeViewCommon::FlowDirection // Swap X/Y and invert Y p = QPointF(p.y(), -p.x()); break; + case NodeViewCommon::kInvalidDirection: + break; } // Multiply by item sizes for this direction @@ -144,12 +196,14 @@ QPointF NodeViewItem::ScreenToNodePoint(QPointF p, NodeViewCommon::FlowDirection break; case NodeViewCommon::kTopToBottom: // Swap X/Y - p = QPointF(p.y(), p.x()); + p = QPointF(p.y(), p.x()); break; case NodeViewCommon::kBottomToTop: // Swap X/Y and invert Y p = QPointF(-p.y(), p.x()); break; + case NodeViewCommon::kInvalidDirection: + break; } return p; @@ -193,66 +247,81 @@ void NodeViewItem::RemoveEdge(NodeViewEdge *edge) edges_.removeOne(edge); } -int NodeViewItem::GetIndexAt(QPointF pt) const -{ - pt -= pos(); - - for (int i=0; iRetranslate(); - - foreach (const QString& input, node_->inputs()) { - if (node_->IsInputConnectable(input)) { - node_inputs_.append(input); - } - } - } - - update(); -} - void NodeViewItem::SetExpanded(bool e, bool hide_titlebar) { - if (node_inputs_.isEmpty() - || (expanded_ == e && hide_titlebar_ == hide_titlebar)) { + if (!CanBeExpanded() || (expanded_ == e)) { return; } expanded_ = e; - hide_titlebar_ = hide_titlebar; - if (expanded_ && !node_inputs_.isEmpty()) { - // Create new rect - QRectF new_rect = title_bar_rect_; - - if (hide_titlebar_) { - new_rect.setHeight(new_rect.height() * node_inputs_.size()); - } else { - new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1)); - } - - setRect(new_rect); - } else { - setRect(title_bar_rect_); + if (context_) { + context_->SetNodeExpandedInContext(node_, e); } - update(); + if (IsOutputItem()) { + // We don't have to check has_connectable_inputs_ here because we did it at the top + input_connector_->setVisible(!expanded_); + } + + if (expanded_) { + node_->Retranslate(); + + if (IsOutputItem()) { + // Create items for each input of the node + int i = 1; + foreach (const QString &input, node_->inputs()) { + if (IsInputValid(input)) { + NodeViewItem *item = new NodeViewItem(node_, input, -1, context_, this); + children_.append(item); + i++; + } + } + + QVector edges = edges_; + for (auto it=edges.cbegin(); it!=edges.cend(); it++) { + if ((*it)->to_item() == this) { + (*it)->set_to_item(GetItemForInput((*it)->input())); + } + } + } else { + // Create items for each element of the input array + int arr_sz = node_->InputArraySize(input_); + children_.resize(arr_sz); + for (int i=0; i edges = edges_; + for (auto it=edges.cbegin(); it!=edges.cend(); it++) { + if ((*it)->to_item() == this) { + (*it)->set_to_item(GetItemForInput((*it)->input())); + } + } + } + } else { + foreach (NodeViewItem *child, children_) { + QVector child_edges = child->edges(); + foreach (NodeViewEdge *edge, child_edges) { + edge->set_to_item(this); + } + delete child; + } + children_.clear(); + } + + UpdateChildrenPositions(); + + if (flow_dir_ == NodeViewCommon::kTopToBottom) { + UpdateOutputConnectorPosition(); + } ReadjustAllEdges(); + + UpdateContextRect(); + + update(); } void NodeViewItem::ToggleExpanded() @@ -266,114 +335,105 @@ void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti // has been slightly modified QPalette app_pal = Core::instance()->main_window()->palette(); - // Draw background rect if expanded - if (IsExpanded()) { + // We only draw a single unit's worth + QRectF single_unit_rect = rect(); + single_unit_rect.setHeight(DefaultItemHeight()); + + if (IsOutputItem()) { + // Set output item colors + painter->setPen(Qt::black); + painter->setBrush(node_->brush(single_unit_rect.top(), single_unit_rect.bottom())); + } else { + // Set input item colors painter->setPen(Qt::NoPen); - painter->setBrush(app_pal.color(QPalette::Window)); + painter->setBrush(element_ == -1 ? app_pal.color(QPalette::Window) : app_pal.color(QPalette::Base)); + } + + painter->drawRect(single_unit_rect); + + // Draw highlight if applicable + if (highlighted_) { + QColor highlight_col = app_pal.color(QPalette::Text); + highlight_col.setAlpha(64); + painter->setBrush(highlight_col); + painter->drawRect(rect()); + } + + // Determine what text to draw and whether to draw an arrow + QString node_label, node_name; + + if (IsOutputItem()) { + if (label_as_output_) { + node_name = QCoreApplication::translate("NodeViewItem", "Output"); + } else { + node_label = node_->GetLabel(); + node_name = node_->ShortName(); + } + } else { + if (element_ == -1) { + node_name = node_->GetInputName(input_); + } else { + node_name = QString::number(element_); + } + } + + // Draw arrow if necessary + int arrow_size = CanBeExpanded() ? DrawExpandArrow(painter) : 0; + + if (IsOutputItem()) { + // Determine the text color (automatically calculate from node background color) + painter->setPen(ColorCoding::GetUISelectorColor(node_->color())); + } else { + // Just use text item + painter->setPen(app_pal.text().color()); + } + + if (node_label.isEmpty()) { + // Draw name only + DrawNodeTitle(painter, node_name, single_unit_rect, Qt::AlignVCenter, arrow_size); + } else { + int text_pad = DefaultTextPadding()/2; + QRectF safe_label_bounds = single_unit_rect.adjusted(text_pad, text_pad, -text_pad, -text_pad); + QFont f; + qreal font_sz = f.pointSizeF(); + + // Draw label as larger/upper text + f.setPointSizeF(font_sz * 0.8); + painter->setFont(f); + DrawNodeTitle(painter, node_label, safe_label_bounds, Qt::AlignTop, arrow_size); + + // Draw node name as smaller/lower text + f.setPointSizeF(font_sz * 0.6); + painter->setFont(f); + DrawNodeTitle(painter, node_name, safe_label_bounds, Qt::AlignBottom, arrow_size); + } + + // Draw final border (output only) + if (IsOutputItem()) { + QPen border_pen; + border_pen.setWidth(node_border_width_); + + if (option->state & QStyle::State_Selected) { + border_pen.setColor(app_pal.color(QPalette::Highlight)); + } else { + border_pen.setColor(Qt::black); + } + + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); painter->drawRect(rect()); - - painter->setPen(app_pal.color(QPalette::Text)); - - for (int i=0;ifillRect(input_rect, highlight_col); - } - - painter->drawText(input_rect, Qt::AlignCenter, node_->GetInputName(node_inputs_.at(i))); - } } - - // Draw the titlebar - if (!hide_titlebar_ && node_) { - - painter->setPen(Qt::black); - painter->setBrush(node_->brush(title_bar_rect_.top(), title_bar_rect_.bottom())); - - painter->drawRect(title_bar_rect_); - - painter->setPen(app_pal.color(QPalette::Text)); - - QString node_label = node_->GetLabel(); - QString node_shortname = node_->ShortName(); - - int icon_size = painter->fontMetrics().height()/2; - - if (node_label.isEmpty()) { - // Draw shortname only - DrawNodeTitle(painter, node_shortname, title_bar_rect_, Qt::AlignVCenter, icon_size, true); - } else { - int text_pad = DefaultTextPadding()/2; - QRectF safe_label_bounds = title_bar_rect_.adjusted(text_pad, text_pad, -text_pad, -text_pad); - QFont f; - qreal font_sz = f.pointSizeF(); - f.setPointSizeF(font_sz * 0.8); - painter->setFont(f); - DrawNodeTitle(painter, node_label, safe_label_bounds, Qt::AlignTop, icon_size, true); - f.setPointSizeF(font_sz * 0.6); - painter->setFont(f); - DrawNodeTitle(painter, node_shortname, safe_label_bounds, Qt::AlignBottom, icon_size, false); - } - - } - - // Draw final border - QPen border_pen; - border_pen.setWidth(node_border_width_); - - if (option->state & QStyle::State_Selected) { - border_pen.setColor(app_pal.color(QPalette::Highlight)); - } else { - border_pen.setColor(Qt::black); - } - - painter->setPen(border_pen); - painter->setBrush(Qt::NoBrush); - - painter->drawRect(rect()); - - // Draw output triangle - painter->setPen(Qt::NoPen); - painter->setBrush(app_pal.color(QPalette::Text)); - int triangle_sz = title_bar_rect_.height() / 2; - int triangle_sz_half = triangle_sz / 2; - - switch (flow_dir_) { - case NodeViewCommon::kLeftToRight: - // Triangle pointing right - output_triangle_[0] = QPointF(rect().right(), rect().center().y() - triangle_sz_half); - output_triangle_[1] = QPointF(rect().right() + triangle_sz_half, rect().center().y()); - output_triangle_[2] = QPointF(rect().right(), rect().center().y() + triangle_sz_half); - break; - case NodeViewCommon::kTopToBottom: - // Triangle pointing down - output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().bottom()); - output_triangle_[1] = QPointF(rect().center().x(), rect().bottom() + triangle_sz_half); - output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().bottom()); - break; - case NodeViewCommon::kBottomToTop: - // Triangle pointing up - output_triangle_[0] = QPointF(rect().center().x() - triangle_sz_half, rect().top()); - output_triangle_[1] = QPointF(rect().center().x(), rect().top() - triangle_sz_half); - output_triangle_[2] = QPointF(rect().center().x() + triangle_sz_half, rect().top()); - break; - case NodeViewCommon::kRightToLeft: - // Triangle pointing left - output_triangle_[0] = QPointF(rect().left(), rect().center().y() - triangle_sz_half); - output_triangle_[1] = QPointF(rect().left() - triangle_sz_half, rect().center().y()); - output_triangle_[2] = QPointF(rect().left(), rect().center().y() + triangle_sz_half); - break; - } - - painter->drawPolygon(output_triangle_); } void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { + if (last_arrow_rect_.contains(event->pos().toPoint())) { + arrow_click_ = true; + ToggleExpanded(); + return; + } + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mousePressEvent(event); @@ -381,6 +441,10 @@ void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + if (arrow_click_) { + return; + } + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseMoveEvent(event); @@ -388,24 +452,22 @@ void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { + if (arrow_click_) { + arrow_click_ = false; + return; + } + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); QGraphicsRectItem::mouseReleaseEvent(event); } -void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) -{ - QGraphicsRectItem::mouseDoubleClickEvent(event); - - if (!(event->modifiers() & Qt::ControlModifier)) { - SetExpanded(!IsExpanded()); - } -} - QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionHasChanged && node_) { ReadjustAllEdges(); + + UpdateContextRect(); } return QGraphicsItem::itemChange(change, value); @@ -414,30 +476,37 @@ QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, cons void NodeViewItem::ReadjustAllEdges() { foreach (NodeViewEdge* edge, edges_) { + if (NodeViewItem *to_item = edge->to_item()) { + static_cast(to_item->parentItem())->UpdateFlowDirectionOfInputItem(to_item); + } + edge->Adjust(); } + foreach (NodeViewItem *child, children_) { + child->ReadjustAllEdges(); + } } -void NodeViewItem::DrawNodeTitle(QPainter* painter, QString text, const QRectF& rect, Qt::Alignment vertical_align, int icon_size, bool draw_arrow) +void NodeViewItem::UpdateContextRect() +{ + QGraphicsItem *item = parentItem(); + + while (item) { + if (NodeViewContext *ctx = dynamic_cast(item)) { + ctx->UpdateRect(); + break; + } + + item = item->parentItem(); + } +} + +void NodeViewItem::DrawNodeTitle(QPainter* painter, QString text, const QRectF& rect, Qt::Alignment vertical_align, int icon_full_size) { QFontMetrics fm = painter->fontMetrics(); - painter->setRenderHint(QPainter::SmoothPixmapTransform); - - // Draw right or down arrow based on expanded state - int icon_padding = title_bar_rect_.height() / 2 - icon_size / 2; - int icon_full_size = icon_size + icon_padding * 2; - if (draw_arrow) { - const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight; - int icon_size_scaled = icon_size * painter->transform().m11(); - painter->drawPixmap(QRect(title_bar_rect_.x() + icon_padding, - title_bar_rect_.y() + icon_padding, - icon_size, - icon_size), expand_icon.pixmap(QSize(icon_size_scaled, icon_size_scaled))); - } - // Calculate how much space we have for text - int item_width = title_bar_rect_.width(); + int item_width = this->rect().width(); int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size; int label_width = QtUtils::QFontMetricsWidth(fm, text); @@ -454,9 +523,6 @@ void NodeViewItem::DrawNodeTitle(QPainter* painter, QString text, const QRectF& text = concatenated; } - // Determine the text color (automatically calculate from node background color) - painter->setPen(ColorCoding::GetUISelectorColor(node_->color())); - // Determine X position (favors horizontal centering unless it'll overrun the arrow) QRectF text_rect = rect; Qt::Alignment text_align = Qt::AlignHCenter | vertical_align; @@ -472,78 +538,80 @@ void NodeViewItem::DrawNodeTitle(QPainter* painter, QString text, const QRectF& text); } -void NodeViewItem::SetHighlightedIndex(int index) +int NodeViewItem::DrawExpandArrow(QPainter *painter) { - if (highlighted_index_ == index) { - return; - } + // Draw right or down arrow based on expanded state + int icon_size = painter->fontMetrics().height()/2; + int icon_padding = DefaultItemHeight() / 2 - icon_size / 2; + int icon_full_size = icon_size + icon_padding * 2; - highlighted_index_ = index; + painter->setRenderHint(QPainter::SmoothPixmapTransform); + const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight; + int icon_size_scaled = icon_size * painter->transform().m11(); + + last_arrow_rect_ = QRect(this->rect().x() + icon_padding, + this->rect().y() + icon_padding, + icon_size, + icon_size); + + painter->drawPixmap(last_arrow_rect_, expand_icon.pixmap(QSize(icon_size_scaled, icon_size_scaled))); + + return icon_full_size; +} + +void NodeViewItem::SetLabelAsOutput(bool e) +{ + label_as_output_ = e; + output_connector_->setVisible(!e); update(); } -QRectF NodeViewItem::GetInputRect(int index) const +QPointF NodeViewItem::GetInputPoint() const { - QRectF r = title_bar_rect_; - - if (!hide_titlebar_) { - index++; - } - - if (IsExpanded()) { - r.translate(0, r.height() * index); - } - - return r; -} - -QPointF NodeViewItem::GetInputPoint(const QString &input, int element, const QPointF& source_pos) const -{ - return pos() + GetInputPointInternal(node_inputs_.indexOf(input), source_pos); + return input_connector_->scenePos(); } QPointF NodeViewItem::GetOutputPoint() const { + QPointF p = output_connector_->scenePos(); + QRectF r = output_connector_->boundingRect(); + switch (flow_dir_) { case NodeViewCommon::kLeftToRight: default: - return pos() + QPointF(rect().right(), rect().center().y()); + p.setX(p.x() + r.width()); + break; case NodeViewCommon::kRightToLeft: - return pos() + QPointF(rect().left(), rect().center().y()); + p.setX(p.x() - r.width()); + break; case NodeViewCommon::kTopToBottom: - return pos() + QPointF(rect().center().x(), rect().bottom()); + p.setY(p.y() + r.height()); + break; case NodeViewCommon::kBottomToTop: - return pos() + QPointF(rect().center().x(), rect().top()); + p.setY(p.y() - r.height()); + break; } + + return p; } void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) { - flow_dir_ = dir; + if (flow_dir_ != dir) { + flow_dir_ = dir; - UpdateNodePosition(); -} + input_connector_->SetFlowDirection(dir); + output_connector_->SetFlowDirection(dir); -QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos) const -{ - QRectF input_rect = GetInputRect(index); + UpdateInputConnectorPosition(); + UpdateOutputConnectorPosition(); - Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_); - - if (flow_orientation == Qt::Horizontal || IsExpanded()) { - if (flow_dir_ == NodeViewCommon::kLeftToRight - || (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) { - return QPointF(input_rect.left(), input_rect.center().y()); - } else { - return QPointF(input_rect.right(), input_rect.center().y()); - } - } else { - if (flow_dir_ == NodeViewCommon::kTopToBottom) { - return QPointF(input_rect.center().x(), input_rect.top()); - } else { - return QPointF(input_rect.center().x(), input_rect.bottom()); + if (IsOutputItem()) { + UpdateNodePosition(); } + + ReadjustAllEdges(); } } @@ -552,4 +620,196 @@ void NodeViewItem::UpdateNodePosition() setPos(NodeToScreenPoint(cached_node_pos_, flow_dir_)); } +void NodeViewItem::UpdateInputConnectorPosition() +{ + QRectF output_rect = input_connector_->boundingRect(); + + NodeViewCommon::FlowDirection using_flow_dir = flow_dir_; + + if (IsExpanded() && !NodeViewCommon::IsFlowHorizontal(flow_dir_)) { + if (edges_.isEmpty() || edges_.first()->from_item()->x() < this->x()) { + using_flow_dir = NodeViewCommon::kLeftToRight; + } else { + using_flow_dir = NodeViewCommon::kRightToLeft; + } + } + + // Input connector flow directions change conditionally + switch (using_flow_dir) { + case NodeViewCommon::kLeftToRight: + input_connector_->setPos(rect().left() - output_rect.width(), 0); + break; + case NodeViewCommon::kRightToLeft: + input_connector_->setPos(rect().right() + output_rect.width(), 0); + break; + case NodeViewCommon::kTopToBottom: + input_connector_->setPos(rect().center().x(), rect().top() - output_rect.height()); + break; + case NodeViewCommon::kBottomToTop: + input_connector_->setPos(rect().center().x(), rect().bottom() + output_rect.height()); + break; + case NodeViewCommon::kInvalidDirection: + break; + } +} + +void NodeViewItem::UpdateOutputConnectorPosition() +{ + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + output_connector_->setPos(rect().right(), 0); + break; + case NodeViewCommon::kRightToLeft: + output_connector_->setPos(rect().left(), 0); + break; + case NodeViewCommon::kTopToBottom: + output_connector_->setPos(rect().center().x(), rect().bottom()); + break; + case NodeViewCommon::kBottomToTop: + output_connector_->setPos(rect().center().x(), rect().top()); + break; + case NodeViewCommon::kInvalidDirection: + break; + } +} + +bool NodeViewItem::IsInputValid(const QString &input) +{ + return node_->IsInputConnectable(input) && !node_->IsInputHidden(input); +} + +void NodeViewItem::SetRectSize(int height_units) +{ + // Set rect + int widget_width = DefaultItemWidth(); + int widget_height = DefaultItemHeight(); + + setRect(QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height * height_units)); +} + +bool NodeViewItem::CanBeExpanded() const +{ + if (IsOutputItem()) { + return has_connectable_inputs_; + } else { + return node_->GetInputFlags(input_) & kInputFlagArray && element_ == -1 && !node_->IsInputConnected(input_); + } +} + +void NodeViewItem::UpdateChildrenPositions() +{ + int y = 1; + int h = DefaultItemHeight(); + + foreach (NodeViewItem *c, children_) { + c->setPos(QPointF(0, y * h)); + + y += c->GetLogicalHeightWithChildren(); + } + + SetRectSize(y); + + if (NodeViewItem *p = dynamic_cast(parentItem())) { + p->UpdateChildrenPositions(); + } +} + +int NodeViewItem::GetLogicalHeightWithChildren() const +{ + int h = 1; + + foreach (NodeViewItem *c, children_) { + h += c->GetLogicalHeightWithChildren(); + } + + return h; +} + +void NodeViewItem::UpdateFlowDirectionOfInputItem(NodeViewItem *child) +{ + if (!child->IsOutputItem()) { + if (NodeViewCommon::IsFlowVertical(flow_dir_)) { + if (!child->edges().isEmpty() && child->edges().first()->from_item()->scenePos().x() > child->scenePos().x()) { + child->SetFlowDirection(NodeViewCommon::kRightToLeft); + } else { + child->SetFlowDirection(NodeViewCommon::kLeftToRight); + } + } else { + child->SetFlowDirection(flow_dir_); + } + } +} + +void NodeViewItem::RepopulateInputs() +{ + if (IsOutputItem()) { + has_connectable_inputs_ = false; + + foreach (const QString& input, node_->inputs()) { + if (IsInputValid(input)) { + has_connectable_inputs_ = true; + break; + } + } + + input_connector_->setVisible(has_connectable_inputs_); + } + + if (IsExpanded() && (IsOutputItem() || element_ == -1)) { + // Create or remove inputs when necessary + // NOTE: This is not the most efficient thing in the world, but it does work + SetExpanded(false); + SetExpanded(true); + } +} + +void NodeViewItem::InputArraySizeChanged(const QString &input) +{ + if (input == input_) { + RepopulateInputs(); + } +} + +void NodeViewItem::NodeAppearanceChanged() +{ + update(); +} + +void NodeViewItem::SetHighlighted(bool e) +{ + highlighted_ = e; + update(); +} + +NodeViewItem *NodeViewItem::GetItemForInput(NodeInput input) +{ + if (NodeGroup *group = dynamic_cast(node_)) { + if (input.node() != group) { + // Translate input to group input + QString id = NodeGroup::GetGroupInputIDFromInput(input); + input.set_node(group); + input.set_input(id); + } + } + + if (IsExpanded()) { + if (input_.isEmpty()) { + // Look for the input in our children + foreach (NodeViewItem *i, children_) { + if (i->input_ == input.input()) { + return i->GetItemForInput(input); + } + } + } else { + // Look for element in our children + if (input.element() >= 0 && input.element() < children_.size()) { + return children_.at(input.element())->GetItemForInput(input); + } + } + } + + // Fallback to this object + return this; +} + } diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h index 3c611d481..d91733dcc 100644 --- a/app/widget/nodeview/nodeviewitem.h +++ b/app/widget/nodeview/nodeviewitem.h @@ -28,9 +28,11 @@ #include "node/node.h" #include "nodeviewcommon.h" +#include "nodeviewitemconnector.h" namespace olive { +class NodeViewItem; class NodeViewEdge; /** @@ -40,18 +42,24 @@ class NodeViewEdge; * * To retrieve the NodeViewItem for a certain Node, use NodeView::NodeToUIObject(). */ -class NodeViewItem : public QGraphicsRectItem +class NodeViewItem : public QObject, public QGraphicsRectItem { + Q_OBJECT public: - NodeViewItem(QGraphicsItem* parent = nullptr); + NodeViewItem(Node *node, const QString &input, int element, Node *context, QGraphicsItem* parent = nullptr); + NodeViewItem(Node *node, Node *context, QGraphicsItem* parent = nullptr) : + NodeViewItem(node, QString(), -1, context, parent) + { + } + virtual ~NodeViewItem() override; + + Node::Position GetNodePositionData() const; QPointF GetNodePosition() const; void SetNodePosition(const QPointF& pos); + void SetNodePosition(const Node::Position& pos); - /** - * @brief Set the Node to correspond to this widget - */ - void SetNode(Node* n); + QVector GetAllEdgesRecursively() const; /** * @brief Get currently attached node @@ -61,6 +69,16 @@ public: return node_; } + NodeInput GetInput() const + { + return NodeInput(node_, input_, element_); + } + + Node *GetContext() const + { + return context_; + } + /** * @brief Get expanded state */ @@ -69,17 +87,18 @@ public: return expanded_; } + const QVector &edges() const + { + return edges_; + } + /** * @brief Set expanded state */ void SetExpanded(bool e, bool hide_titlebar = false); void ToggleExpanded(); - /** - * @brief Returns GLOBAL point that edges should connect to for any NodeParam member of this object - */ - QPointF GetInputPoint(const QString& input, int element, const QPointF &source_pos) const; - + QPointF GetInputPoint() const; QPointF GetOutputPoint() const; /** @@ -87,6 +106,11 @@ public: */ void SetFlowDirection(NodeViewCommon::FlowDirection dir); + NodeViewCommon::FlowDirection GetFlowDirection() const + { + return flow_dir_; + } + static int DefaultTextPadding(); static int DefaultItemHeight(); @@ -106,29 +130,27 @@ public: void AddEdge(NodeViewEdge* edge); void RemoveEdge(NodeViewEdge* edge); - int GetIndexAt(QPointF pt) const; - - NodeInput GetInputAtIndex(int index) const + bool IsLabelledAsOutputOfContext() const { - return NodeInput(node_, node_inputs_.at(index)); + return label_as_output_; } - void SetHighlightedIndex(int index); + void SetLabelAsOutput(bool e); - void SetPreventRemoving(bool e) + void SetHighlighted(bool e); + + NodeViewItem *GetItemForInput(NodeInput input); + + bool IsOutputItem() const { - prevent_removing_ = e; + return input_.isEmpty(); } - bool GetPreventRemoving() const - { - return prevent_removing_; - } + void ReadjustAllEdges(); - const QPolygonF &GetOutputTriangle() const - { - return output_triangle_; - } + void UpdateFlowDirectionOfInputItem(NodeViewItem *child); + + bool CanBeExpanded() const; protected: virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; @@ -136,44 +158,45 @@ protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override; virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; - virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override; virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) override; private: - void ReadjustAllEdges(); + void UpdateContextRect(); - void DrawNodeTitle(QPainter *painter, QString text, const QRectF &rect, Qt::Alignment vertical_align, int icon_size, bool draw_arrow); + void DrawNodeTitle(QPainter *painter, QString text, const QRectF &rect, Qt::Alignment vertical_align, int icon_full_size); - /** - * @brief Returns local rect of a NodeInput in array node_inputs_[index] - */ - QRectF GetInputRect(int index) const; - - /** - * @brief Returns local point that edges should connect to for a NodeInput in array node_inputs_[index] - */ - QPointF GetInputPointInternal(int index, const QPointF &source_pos) const; + int DrawExpandArrow(QPainter *painter); /** * @brief Internal update function when logical position changes */ void UpdateNodePosition(); + void UpdateInputConnectorPosition(); + void UpdateOutputConnectorPosition(); + + bool IsInputValid(const QString &input); + + void SetRectSize(int height_units = 1); + + void UpdateChildrenPositions(); + + int GetLogicalHeightWithChildren() const; + /** * @brief Reference to attached Node */ - Node* node_; + Node *node_; + QString input_; + int element_; + + Node *context_; /** * @brief Cached list of node inputs */ - QVector node_inputs_; - - /** - * @brief Rectangle of the Node's title bar (equal to rect() when collapsed) - */ - QRectF title_bar_rect_; + QVector children_; /// Sizing variables to use when drawing int node_border_width_; @@ -183,9 +206,7 @@ private: */ bool expanded_; - bool hide_titlebar_; - - int highlighted_index_; + bool highlighted_; NodeViewCommon::FlowDirection flow_dir_; @@ -193,9 +214,22 @@ private: QPointF cached_node_pos_; - bool prevent_removing_; + QRect last_arrow_rect_; + bool arrow_click_; - QPolygonF output_triangle_; + NodeViewItemConnector *input_connector_; + NodeViewItemConnector *output_connector_; + + bool has_connectable_inputs_; + + bool label_as_output_; + +private slots: + void NodeAppearanceChanged(); + + void RepopulateInputs(); + + void InputArraySizeChanged(const QString &input); }; diff --git a/app/widget/nodeview/nodeviewitemconnector.cpp b/app/widget/nodeview/nodeviewitemconnector.cpp new file mode 100644 index 000000000..032b9903f --- /dev/null +++ b/app/widget/nodeview/nodeviewitemconnector.cpp @@ -0,0 +1,84 @@ +/*** + + 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 "nodeviewitemconnector.h" + +#include +#include +#include +#include + +#include "nodeviewitem.h" + +namespace olive { + +NodeViewItemConnector::NodeViewItemConnector(bool is_output, QGraphicsItem *parent) : + QGraphicsPolygonItem(parent), + output_(is_output) +{ + QColor c = qApp->palette().text().color(); + setPen(QPen(c, NodeViewItem::DefaultItemBorder())); + setBrush(c); +} + +void NodeViewItemConnector::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + QFont f; + QFontMetricsF fm(f); + + int triangle_sz = fm.height()/2; + int triangle_sz_half = triangle_sz / 2; + + QPolygonF p; + p.resize(3); + + switch (dir) { + case NodeViewCommon::kLeftToRight: + // Triangle pointing right + p[0] = QPointF(0, -triangle_sz_half); + p[1] = QPointF(triangle_sz_half, 0); + p[2] = QPointF(0, triangle_sz_half); + break; + case NodeViewCommon::kTopToBottom: + // Triangle pointing down + p[0] = QPointF(-triangle_sz_half, 0); + p[1] = QPointF(0, triangle_sz_half); + p[2] = QPointF(triangle_sz_half, 0); + break; + case NodeViewCommon::kBottomToTop: + // Triangle pointing up + p[0] = QPointF(-triangle_sz_half, 0); + p[1] = QPointF(0, -triangle_sz_half); + p[2] = QPointF(triangle_sz_half, 0); + break; + case NodeViewCommon::kRightToLeft: + // Triangle pointing left + p[0] = QPointF(0, -triangle_sz_half); + p[1] = QPointF(-triangle_sz_half, 0); + p[2] = QPointF(0, triangle_sz_half); + break; + case NodeViewCommon::kInvalidDirection: + break; + } + + setPolygon(p); +} + +} diff --git a/app/widget/nodeview/nodeviewitemconnector.h b/app/widget/nodeview/nodeviewitemconnector.h new file mode 100644 index 000000000..4da1e2fca --- /dev/null +++ b/app/widget/nodeview/nodeviewitemconnector.h @@ -0,0 +1,49 @@ +/*** + + 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 NODEVIEWITEMCONNECTOR_H +#define NODEVIEWITEMCONNECTOR_H + +#include + +#include "nodeviewcommon.h" + +namespace olive { + +class NodeViewItemConnector : public QGraphicsPolygonItem +{ +public: + NodeViewItemConnector(bool is_output, QGraphicsItem *parent = nullptr); + + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + bool IsOutput() const + { + return output_; + } + +private: + bool output_; + +}; + +} + +#endif // NODEVIEWITEMCONNECTOR_H diff --git a/app/widget/nodeview/nodeviewminimap.cpp b/app/widget/nodeview/nodeviewminimap.cpp index d1e20791c..8dc99c914 100644 --- a/app/widget/nodeview/nodeviewminimap.cpp +++ b/app/widget/nodeview/nodeviewminimap.cpp @@ -38,6 +38,7 @@ NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent) : setViewportUpdateMode(FullViewportUpdate); setFrameShape(QFrame::Panel); setFrameShadow(QFrame::Plain); + setMouseTracking(true); QMetaObject::invokeMethod(this, &NodeViewMiniMap::SetDefaultSize, Qt::QueuedConnection); @@ -87,7 +88,7 @@ void NodeViewMiniMap::resizeEvent(QResizeEvent *event) void NodeViewMiniMap::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { - if (event->pos().x() <= resize_triangle_sz_ && event->pos().y() <= resize_triangle_sz_) { + if (MouseInsideResizeTriangle(event)) { // Resizing! resizing_ = true; resize_anchor_ = QCursor::pos(); @@ -107,6 +108,8 @@ void NodeViewMiniMap::mouseMoveEvent(QMouseEvent *event) } else { EmitMoveSignal(event); } + } else { + setCursor(MouseInsideResizeTriangle(event) ? Qt::SizeFDiagCursor : Qt::ArrowCursor); } } @@ -135,6 +138,11 @@ void NodeViewMiniMap::SetDefaultSize() } } +bool NodeViewMiniMap::MouseInsideResizeTriangle(QMouseEvent *event) +{ + return event->pos().x() <= resize_triangle_sz_ && event->pos().y() <= resize_triangle_sz_; +} + void NodeViewMiniMap::EmitMoveSignal(QMouseEvent *event) { emit MoveToScenePoint(mapToScene(event->pos())); diff --git a/app/widget/nodeview/nodeviewminimap.h b/app/widget/nodeview/nodeviewminimap.h index a63db1993..e155b54f2 100644 --- a/app/widget/nodeview/nodeviewminimap.h +++ b/app/widget/nodeview/nodeviewminimap.h @@ -57,6 +57,8 @@ private slots: void SetDefaultSize(); private: + bool MouseInsideResizeTriangle(QMouseEvent *event); + void EmitMoveSignal(QMouseEvent *event); int resize_triangle_sz_; diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp index 878fd52a4..dc76509cc 100644 --- a/app/widget/nodeview/nodeviewscene.cpp +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -21,6 +21,7 @@ #include "nodeviewscene.h" #include "common/functiontimer.h" +#include "core.h" #include "node/project/sequence/sequence.h" #include "nodeviewedge.h" #include "nodeviewitem.h" @@ -38,203 +39,65 @@ void NodeViewScene::SetFlowDirection(NodeViewCommon::FlowDirection direction) { direction_ = direction; - { - // Iterate over node items setting direction - QHash::const_iterator i; - for (i=item_map_.constBegin(); i!=item_map_.constEnd(); i++) { - i.value()->SetFlowDirection(direction_); - } + foreach (NodeViewContext *ctx, context_map_) { + ctx->SetFlowDirection(direction_); } - - { - // Iterate over edge items setting direction - foreach (NodeViewEdge* edge, edges_) { - edge->SetFlowDirection(direction_); - } - } -} - -void NodeViewScene::clear() -{ - // Deselect everything (prevents signals that a selection has changed after deleting an object) - DeselectAll(); - - // HACK: QGraphicsScene contains some sort of internal caching of the selected items which doesn't update unless - // we call a function like this. That means even though we deselect all items above, QGraphicsScene will - // continue to incorrectly signal selectionChanged() when items that were selected (but are now not) get - // deleted. Calling this function appears to update the internal cache and prevent this. - selectedItems(); - - for (auto it=item_map_.cbegin(); it!=item_map_.cend(); it++) { - DisconnectNode(it.key()); - delete it.value(); - } - item_map_.clear(); - - qDeleteAll(edges_); - edges_.clear(); } void NodeViewScene::SelectAll() { - QList all_items = this->items(); - - foreach (QGraphicsItem* i, all_items) { + foreach (QGraphicsItem* i, items()) { i->setSelected(true); } } void NodeViewScene::DeselectAll() { - QList selected_items = this->selectedItems(); - - foreach (QGraphicsItem* i, selected_items) { + foreach (QGraphicsItem* i, items()) { i->setSelected(false); } } -NodeViewItem *NodeViewScene::NodeToUIObject(Node *n) -{ - return item_map_.value(n); -} - -NodeViewEdge *NodeViewScene::EdgeToUIObject(Node *output, const NodeInput& input) -{ - foreach (NodeViewEdge* edge, edges_) { - if (edge->output() == output && edge->input() == input) { - return edge; - } - } - - return nullptr; -} - -QVector NodeViewScene::GetSelectedNodes() const -{ - QHash::const_iterator iterator; - QVector selected; - - for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { - if (iterator.value()->isSelected()) { - selected.append(iterator.key()); - } - } - - return selected; -} - QVector NodeViewScene::GetSelectedItems() const { - QHash::const_iterator iterator; - QVector selected; + QVector items; - for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { - if (iterator.value()->isSelected()) { - selected.append(iterator.value()); + foreach (NodeViewContext *ctx, context_map_) { + items.append(ctx->GetSelectedItems()); + } + + return items; +} + +NodeViewContext *NodeViewScene::AddContext(Node *node) +{ + NodeViewContext *context_item = context_map_.value(node); + + if (!context_item) { + context_item = new NodeViewContext(node); + + context_item->SetFlowDirection(GetFlowDirection()); + context_item->SetCurvedEdges(GetEdgesAreCurved()); + + QPointF pos(0, 0); + QRectF item_rect = context_item->rect(); + while (!items(item_rect).isEmpty()) { + pos.setY(pos.y() + item_rect.height()); + item_rect = context_item->rect().translated(pos); } + context_item->setPos(pos); + + addItem(context_item); + + context_map_.insert(node, context_item); } - return selected; + return context_item; } -QVector NodeViewScene::GetSelectedEdges() const +void NodeViewScene::RemoveContext(Node *node) { - QVector edges; - - foreach (NodeViewEdge* e, edges_) { - if (e->isSelected()) { - edges.append(e); - } - } - - return edges; -} - -NodeViewItem* NodeViewScene::AddNode(Node* node) -{ - NodeViewItem* item = new NodeViewItem(); - - item->SetFlowDirection(direction_); - item->SetNode(node); - - addItem(item); - item_map_.insert(node, item); - - ConnectNode(node); - - return item; -} - -void NodeViewScene::RemoveNode(Node *node) -{ - DisconnectNode(node); - - delete item_map_.take(node); -} - -NodeViewEdge* NodeViewScene::AddEdge(Node *output, const NodeInput &input) -{ - NodeViewEdge *edge = EdgeToUIObject(output, input); - - if (!edge) { - edge = AddEdgeInternal(output, input, NodeToUIObject(output), NodeToUIObject(input.node())); - } - - return edge; -} - -void NodeViewScene::RemoveEdge(Node *output, const NodeInput &input) -{ - NodeViewEdge* edge = EdgeToUIObject(output, input); - if (edge) { - edge->from_item()->RemoveEdge(edge); - edge->to_item()->RemoveEdge(edge); - edges_.removeOne(edge); - delete edge; - } -} - -int NodeViewScene::DetermineWeight(Node *n) -{ - QVector inputs = n->GetImmediateDependencies(); - - int weight = 0; - - foreach (Node* i, inputs) { - if (i->GetNumberOfRoutesTo(n) == 1) { - weight += DetermineWeight(i); - } - } - - return qMax(1, weight); -} - -NodeViewEdge* NodeViewScene::AddEdgeInternal(Node *output, const NodeInput& input, NodeViewItem *from, NodeViewItem *to) -{ - NodeViewEdge* edge_ui = new NodeViewEdge(output, input, from, to); - - edge_ui->SetFlowDirection(direction_); - edge_ui->SetCurved(curved_edges_); - - from->AddEdge(edge_ui); - to->AddEdge(edge_ui); - - addItem(edge_ui); - edges_.append(edge_ui); - - return edge_ui; -} - -void NodeViewScene::ConnectNode(Node *n) -{ - connect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); - connect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); -} - -void NodeViewScene::DisconnectNode(Node *n) -{ - disconnect(n, &Node::ColorChanged, this, &NodeViewScene::NodeAppearanceChanged); - disconnect(n, &Node::LabelChanged, this, &NodeViewScene::NodeAppearanceChanged); + delete context_map_.take(node); } Qt::Orientation NodeViewScene::GetFlowOrientation() const @@ -242,26 +105,15 @@ Qt::Orientation NodeViewScene::GetFlowOrientation() const return NodeViewCommon::GetFlowOrientation(direction_); } -NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const -{ - return direction_; -} - void NodeViewScene::SetEdgesAreCurved(bool curved) { if (curved_edges_ != curved) { curved_edges_ = curved; - foreach (NodeViewEdge* e, edges_) { - e->SetCurved(curved_edges_); + foreach (NodeViewContext *ctx, context_map_) { + ctx->SetCurvedEdges(curved_edges_); } } } -void NodeViewScene::NodeAppearanceChanged() -{ - // Force item to update - item_map_.value(static_cast(sender()))->update(); -} - } diff --git a/app/widget/nodeview/nodeviewscene.h b/app/widget/nodeview/nodeviewscene.h index 29cf5bbb7..64a149bd3 100644 --- a/app/widget/nodeview/nodeviewscene.h +++ b/app/widget/nodeview/nodeviewscene.h @@ -25,8 +25,10 @@ #include #include "node/graph.h" +#include "nodeviewcontext.h" #include "nodeviewedge.h" #include "nodeviewitem.h" +#include "undo/undostack.h" namespace olive { @@ -36,42 +38,23 @@ class NodeViewScene : public QGraphicsScene public: NodeViewScene(QObject *parent = nullptr); - void clear(); - void SelectAll(); void DeselectAll(); - /** - * @brief Retrieve the graphical widget corresponding to a specific Node - * - * In situations where you know what Node you're working with but need the UI object (e.g. for positioning), this - * static function will retrieve the NodeViewItem (Node UI representation) connected to this Node in a certain - * QGraphicsScene. This can be called from any other UI object, since it'll have a reference to the QGraphicsScene - * through QGraphicsItem::scene(). - * - * If the scene does not contain a widget for this node (usually meaning the node's graph is not the active graph - * in this view/scene), this function returns nullptr. - */ - NodeViewItem* NodeToUIObject(Node* n); - NodeViewEdge *EdgeToUIObject(Node *output, const NodeInput &input); - - QVector GetSelectedNodes() const; QVector GetSelectedItems() const; - QVector GetSelectedEdges() const; - const QHash& item_map() const + const QHash &context_map() const { - return item_map_; - } - - const QVector& edges() const - { - return edges_; + return context_map_; } Qt::Orientation GetFlowOrientation() const; - NodeViewCommon::FlowDirection GetFlowDirection() const; + NodeViewCommon::FlowDirection GetFlowDirection() const + { + return direction_; + } + void SetFlowDirection(NodeViewCommon::FlowDirection direction); bool GetEdgesAreCurved() const @@ -80,24 +63,8 @@ public: } public slots: - /** - * @brief Slot when a Node is added to a graph (SetGraph() connects this) - * - * This should NEVER be called directly, only connected to a NodeGraph. To add a Node to the NodeGraph - * use NodeGraph::AddNode(). - */ - NodeViewItem *AddNode(Node* node); - - /** - * @brief Slot when a Node is removed from a graph (SetGraph() connects this) - * - * This should NEVER be called directly, only connected to a NodeGraph. To remove a Node from the NodeGraph - * use NodeGraph::RemoveNode(). - */ - void RemoveNode(Node* node); - - NodeViewEdge *AddEdge(Node *output, const NodeInput& input); - void RemoveEdge(Node *output, const NodeInput& input); + NodeViewContext *AddContext(Node *node); + void RemoveContext(Node *node); /** * @brief Set whether edges in this scene should be curved or not @@ -105,17 +72,7 @@ public slots: void SetEdgesAreCurved(bool curved); private: - static int DetermineWeight(Node* n); - - NodeViewEdge* AddEdgeInternal(Node *output, const NodeInput &input, NodeViewItem* from, NodeViewItem* to); - - void ConnectNode(Node *n); - - void DisconnectNode(Node *n); - - QHash item_map_; - - QVector edges_; + QHash context_map_; NodeGraph* graph_; @@ -123,12 +80,6 @@ private: bool curved_edges_; -private slots: - /** - * @brief Receiver for when a node's label has changed - */ - void NodeAppearanceChanged(); - }; } diff --git a/app/widget/nodeview/nodeviewundo.cpp b/app/widget/nodeview/nodeviewundo.cpp index 5d7af0263..54106070f 100644 --- a/app/widget/nodeview/nodeviewundo.cpp +++ b/app/widget/nodeview/nodeviewundo.cpp @@ -193,4 +193,100 @@ void NodeOverrideColorCommand::undo() node_->SetOverrideColor(old_index_); } +NodeViewDeleteCommand::NodeViewDeleteCommand() +{ +} + +void NodeViewDeleteCommand::AddNode(Node *node, Node *context) +{ + foreach (const NodePair &pair, nodes_) { + if (pair.first == node && pair.second == context) { + return; + } + } + + nodes_.append(NodePair({node, context})); + + for (auto it=node->input_connections().cbegin(); it!=node->input_connections().cend(); it++) { + if (context->ContextContainsNode(it->second)) { + AddEdge(it->second, it->first); + } + } + + for (auto it=node->output_connections().cbegin(); it!=node->output_connections().cend(); it++) { + if (context->ContextContainsNode(it->second.node())) { + AddEdge(it->first, it->second); + } + } +} + +void NodeViewDeleteCommand::AddEdge(Node *output, const NodeInput &input) +{ + foreach (const Node::OutputConnection &edge, edges_) { + if (edge.first == output && edge.second == input) { + return; + } + } + + edges_.append({output, input}); +} + +Project *NodeViewDeleteCommand::GetRelevantProject() const +{ + if (!nodes_.isEmpty()) { + return nodes_.first().first->project(); + } + + if (!edges_.isEmpty()) { + return edges_.first().first->project(); + } + + return nullptr; +} + +void NodeViewDeleteCommand::redo() +{ + foreach (const Node::OutputConnection &edge, edges_) { + Node::DisconnectEdge(edge.first, edge.second); + } + + foreach (const NodePair &pair, nodes_) { + RemovedNode rn; + + rn.node = pair.first; + rn.context = pair.second; + rn.pos = rn.context->GetNodePositionInContext(rn.node); + + rn.context->RemoveNodeFromContext(rn.node); + + // If node is no longer in any contexts and is not connected to anything, remove it + if (rn.node->parent()->GetNumberOfContextsNodeIsIn(rn.node, true) == 0 + && rn.node->input_connections().empty() + && rn.node->output_connections().empty()) { + rn.removed_from_graph = rn.node->parent(); + rn.node->setParent(&memory_manager_); + } else { + rn.removed_from_graph = nullptr; + } + + removed_nodes_.append(rn); + } +} + +void NodeViewDeleteCommand::undo() +{ + for (auto rn=removed_nodes_.crbegin(); rn!=removed_nodes_.crend(); rn++) { + if (rn->removed_from_graph) { + rn->node->setParent(rn->removed_from_graph); + } + + rn->context->SetNodePositionInContext(rn->node, rn->pos); + } + removed_nodes_.clear(); + + for (auto edge=edges_.crbegin(); edge!=edges_.crend(); edge++) { + Node::ConnectEdge(edge->first, edge->second); + } +} + } diff --git a/app/widget/nodeview/nodeviewundo.h b/app/widget/nodeview/nodeviewundo.h index 7ed3e434e..567198b5b 100644 --- a/app/widget/nodeview/nodeviewundo.h +++ b/app/widget/nodeview/nodeviewundo.h @@ -363,6 +363,42 @@ private: }; +class NodeViewDeleteCommand : public UndoCommand +{ +public: + NodeViewDeleteCommand(); + + void AddNode(Node *node, Node *context); + + void AddEdge(Node *output, const NodeInput &input); + + virtual Project * GetRelevantProject() const override; + +protected: + virtual void redo() override; + + virtual void undo() override; + +private: + using NodePair = QPair; + + QVector nodes_; + + QVector edges_; + + struct RemovedNode { + Node *node; + Node *context; + QPointF pos; + NodeGraph *removed_from_graph; + }; + + QVector removed_nodes_; + + QObject memory_manager_; + +}; + } #endif // NODEVIEWUNDO_H diff --git a/app/widget/nodeview/nodewidget.cpp b/app/widget/nodeview/nodewidget.cpp new file mode 100644 index 000000000..4fdb2dd06 --- /dev/null +++ b/app/widget/nodeview/nodewidget.cpp @@ -0,0 +1,51 @@ +/*** + + 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 "nodewidget.h" + +#include + +namespace olive { + +NodeWidget::NodeWidget(QWidget *parent) : + QWidget(parent) +{ + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setMargin(0); + + toolbar_ = new NodeViewToolBar(); + outer_layout->addWidget(toolbar_); + + // Create NodeView widget + node_view_ = new NodeView(this); + outer_layout->addWidget(node_view_); + + // Connect toolbar to NodeView + connect(toolbar_, &NodeViewToolBar::MiniMapEnabledToggled, node_view_, &NodeView::SetMiniMapEnabled); + connect(toolbar_, &NodeViewToolBar::AddNodeClicked, node_view_, &NodeView::ShowAddMenu); + + // Set defaults + toolbar_->SetMiniMapEnabled(true); + node_view_->SetMiniMapEnabled(true); + + setSizePolicy(node_view_->sizePolicy()); +} + +} diff --git a/app/dialog/nodeproperties/nodepropertiesdialog.h b/app/widget/nodeview/nodewidget.h similarity index 58% rename from app/dialog/nodeproperties/nodepropertiesdialog.h rename to app/widget/nodeview/nodewidget.h index 5375a1313..47762f499 100644 --- a/app/dialog/nodeproperties/nodepropertiesdialog.h +++ b/app/widget/nodeview/nodewidget.h @@ -18,35 +18,40 @@ ***/ -#ifndef NODEPROPERTIESDIALOG_H -#define NODEPROPERTIESDIALOG_H +#ifndef NODEWIDGET_H +#define NODEWIDGET_H -#include +#include -#include "widget/nodeparamview/nodeparamviewitem.h" +#include "nodeview.h" +#include "nodeviewtoolbar.h" namespace olive { -class NodePropertiesDialog : public QDialog +class NodeWidget : public QWidget { Q_OBJECT public: - NodePropertiesDialog(Node *node, const rational &timebase, QWidget *parent = nullptr); - NodePropertiesDialog(const QVector &node, const rational &timebase, QWidget *parent = nullptr) : - NodePropertiesDialog(node.first(), timebase, parent) + NodeWidget(QWidget *parent = nullptr); + + NodeView *view() const { + return node_view_; } -public slots: - virtual void accept() override; + void SetContexts(const QVector &nodes) + { + node_view_->SetContexts(nodes); + toolbar_->setEnabled(!nodes.isEmpty()); + } private: - Node *node_; + NodeView *node_view_; - QLineEdit *label_edit_; + NodeViewToolBar *toolbar_; }; } -#endif // NODEPROPERTIESDIALOG_H +#endif // NODEWIDGET_H diff --git a/app/widget/panel/panel.cpp b/app/widget/panel/panel.cpp index 3009a3fb3..4ce4cd405 100644 --- a/app/widget/panel/panel.cpp +++ b/app/widget/panel/panel.cpp @@ -28,6 +28,8 @@ #include #include +#include "panel/panelmanager.h" + namespace olive { PanelWidget::PanelWidget(const QString &object_name, QWidget *parent) : @@ -39,6 +41,13 @@ PanelWidget::PanelWidget(const QString &object_name, QWidget *parent) : setFocusPolicy(Qt::ClickFocus); connect(this, &PanelWidget::visibilityChanged, this, &PanelWidget::PanelVisibilityChanged); + + PanelManager::instance()->RegisterPanel(this); +} + +PanelWidget::~PanelWidget() +{ + PanelManager::instance()->UnregisterPanel(this); } void PanelWidget::SetMovementLocked(bool locked) diff --git a/app/widget/panel/panel.h b/app/widget/panel/panel.h index 87690bf0d..130e3eaf8 100644 --- a/app/widget/panel/panel.h +++ b/app/widget/panel/panel.h @@ -45,6 +45,8 @@ public: */ PanelWidget(const QString& object_name, QWidget* parent); + virtual ~PanelWidget() override; + /** * @brief Set whether panel movement is locked or not */ diff --git a/app/widget/projectexplorer/projectexplorer.cpp b/app/widget/projectexplorer/projectexplorer.cpp index 9326cd357..4482ac8d7 100644 --- a/app/widget/projectexplorer/projectexplorer.cpp +++ b/app/widget/projectexplorer/projectexplorer.cpp @@ -30,7 +30,7 @@ #include "common/define.h" #include "core.h" -#include "dialog/nodeproperties/nodepropertiesdialog.h" +#include "dialog/footageproperties/footageproperties.h" #include "dialog/sequence/sequence.h" #include "projectexplorerundo.h" #include "task/precache/precachetask.h" @@ -446,8 +446,8 @@ void ProjectExplorer::ShowItemPropertiesDialog() // FIXME: Support for multiple items if (dynamic_cast(sel)) { - NodePropertiesDialog npd(sel, static_cast(sel)->GetVideoParams().time_base(), this); - npd.exec(); + FootagePropertiesDialog fpd(this, static_cast(sel)); + fpd.exec(); } else if (dynamic_cast(sel)) { diff --git a/app/widget/slider/floatslider.cpp b/app/widget/slider/floatslider.cpp index 39c00f686..7510c36ef 100644 --- a/app/widget/slider/floatslider.cpp +++ b/app/widget/slider/floatslider.cpp @@ -78,29 +78,46 @@ void FloatSlider::SetDisplayType(const FloatSlider::DisplayType &type) } } -QString FloatSlider::ValueToString(double val, FloatSlider::DisplayType display, int decimal_places, bool autotrim_decimal_places) +double FloatSlider::TransformValueToDisplay(double val, DisplayType display) { switch (display) { case kNormal: - // Do nothing, skip to the return string at the end break; case kDecibel: - // Convert to decibels and return dB formatted string - - // Return negative infinity for zero volume - if (qIsNull(val)) { - return tr("\xE2\x88\x9E"); - } - val = Decibel::fromLinear(val); break; case kPercentage: - // Multiply value by 100 for user-friendly percentage val *= 100.0; break; } - return FloatToString(val, decimal_places, autotrim_decimal_places); + return val; +} + +double FloatSlider::TransformDisplayToValue(double val, DisplayType display) +{ + switch (display) { + case kNormal: + break; + case kDecibel: + val = Decibel::toLinear(val); + break; + case kPercentage: + val *= 0.01; + break; + } + + return val; +} + +QString FloatSlider::ValueToString(double val, FloatSlider::DisplayType display, int decimal_places, bool autotrim_decimal_places) +{ + // Return negative infinity for zero volume + if (display == kDecibel && qIsNull(val)) { + return tr("\xE2\x88\x9E"); + } + + return FloatToString(TransformValueToDisplay(val, display), decimal_places, autotrim_decimal_places); } QString FloatSlider::ValueToString(const QVariant &v) const @@ -110,46 +127,21 @@ QString FloatSlider::ValueToString(const QVariant &v) const QVariant FloatSlider::StringToValue(const QString &s, bool *ok) const { - switch (display_type_) { - case kNormal: - // Do nothing, skip to the return string at the end - break; - case kDecibel: - { - bool valid; + bool valid; + double val = s.toDouble(&valid); - // See if we can get a decimal number out of this - qreal decibels = s.toDouble(&valid); - - if (ok) *ok = valid; - - if (valid) { - // Convert from decibel scale to linear decimal - return Decibel::toLinear(decibels); - } - - break; - } - case kPercentage: - { - bool valid; - - // Try to get double value - double val = s.toDouble(&valid); - - if (ok) *ok = valid; - - // If we could get it, convert back to a 0.0 - 1.0 value and return - if (valid) { - return val * 0.01; - } - - break; - } + // If we were given an `ok` pointer, set it to `valid` + if (ok) { + *ok = valid; } - // Just try to convert the string to a double - return s.toDouble(ok) - GetOffset().toDouble(); + // If valid, transform it from display + if (valid) { + val = TransformDisplayToValue(val, display_type_); + } + + // Return un-offset value + return val - GetOffset().toDouble(); } QVariant FloatSlider::AdjustDragDistanceInternal(const QVariant &start, const double &drag) const diff --git a/app/widget/slider/floatslider.h b/app/widget/slider/floatslider.h index 4000e41bd..fe392a983 100644 --- a/app/widget/slider/floatslider.h +++ b/app/widget/slider/floatslider.h @@ -49,6 +49,10 @@ public: void SetDisplayType(const DisplayType& type); + static double TransformValueToDisplay(double val, DisplayType display); + + static double TransformDisplayToValue(double val, DisplayType display); + static QString ValueToString(double val, DisplayType display, int decimal_places, bool autotrim_decimal_places); protected: diff --git a/app/widget/timebased/CMakeLists.txt b/app/widget/timebased/CMakeLists.txt index e2a35686f..ed200fc08 100644 --- a/app/widget/timebased/CMakeLists.txt +++ b/app/widget/timebased/CMakeLists.txt @@ -18,6 +18,8 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/timebased/timebasedview.cpp widget/timebased/timebasedview.h + widget/timebased/timebasedviewselectionmanager.cpp + widget/timebased/timebasedviewselectionmanager.h widget/timebased/timebasedwidget.cpp widget/timebased/timebasedwidget.h widget/timebased/timescaledobject.cpp diff --git a/app/widget/timebased/timebasedview.h b/app/widget/timebased/timebasedview.h index 247948b61..a51728afb 100644 --- a/app/widget/timebased/timebasedview.h +++ b/app/widget/timebased/timebasedview.h @@ -53,11 +53,20 @@ public: return dragging_playhead_; } + // To be called only by selection managers + virtual void SelectionManagerSelectEvent(void *obj){} + virtual void SelectionManagerDeselectEvent(void *obj){} + public slots: void SetTime(const rational &time); void SetEndTime(const rational& length); + /** + * @brief Slot called whenever the view resizes or the scene contents change to enforce minimum scene sizes + */ + void UpdateSceneRect(); + signals: void TimeChanged(const rational& time); @@ -97,12 +106,6 @@ protected: y_axis_enabled_ = e; } -protected slots: - /** - * @brief Slot called whenever the view resizes or the scene contents change to enforce minimum scene sizes - */ - void UpdateSceneRect(); - private: qreal GetPlayheadX(); diff --git a/app/widget/timebased/timebasedviewselectionmanager.cpp b/app/widget/timebased/timebasedviewselectionmanager.cpp new file mode 100644 index 000000000..dd44a172e --- /dev/null +++ b/app/widget/timebased/timebasedviewselectionmanager.cpp @@ -0,0 +1,26 @@ +/*** + + 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 "timebasedviewselectionmanager.h" + +namespace olive { + + +} diff --git a/app/widget/timebased/timebasedviewselectionmanager.h b/app/widget/timebased/timebasedviewselectionmanager.h new file mode 100644 index 000000000..ae5e32230 --- /dev/null +++ b/app/widget/timebased/timebasedviewselectionmanager.h @@ -0,0 +1,325 @@ +/*** + + 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 TIMEBASEDVIEWSELECTIONMANAGER_H +#define TIMEBASEDVIEWSELECTIONMANAGER_H + +#include +#include +#include +#include + +#include "common/rational.h" +#include "common/timecodefunctions.h" +#include "timebasedview.h" + +namespace olive { + +template +class TimeBasedViewSelectionManager +{ +public: + TimeBasedViewSelectionManager(TimeBasedView *view) : + view_(view), + rubberband_(nullptr) + {} + + void ClearDrawnObjects() + { + drawn_objects_.clear(); + } + + void DeclareDrawnObject(T *object, const QRectF &pos) + { + drawn_objects_.append({object, pos}); + } + + bool Select(T *key) + { + Q_ASSERT(key); + + if (!IsSelected(key)) { + selected_.append(key); + return true; + } + + return false; + } + + bool Deselect(T *key) + { + Q_ASSERT(key); + + return selected_.removeOne(key); + } + + void ClearSelection() + { + selected_.clear(); + } + + bool IsSelected(T *key) const + { + return selected_.contains(key); + } + + const QVector &GetSelectedObjects() const + { + return selected_; + } + + void SetTimebase(const rational &tb) + { + timebase_ = tb; + } + + T *GetObjectAtPoint(const QPointF &scene_pt) + { + // Iterate in reverse order because the objects drawn later will appear on top to the user + for (auto it=drawn_objects_.crbegin(); it!=drawn_objects_.crend(); it++) { + const DrawnObject &kp = *it; + if (kp.second.contains(scene_pt)) { + return kp.first; + } + } + + return nullptr; + } + + T *GetObjectAtPoint(const QPoint &pt) + { + return GetObjectAtPoint(view_->mapToScene(pt)); + } + + T *MousePress(QMouseEvent *event) + { + T *key_under_cursor = nullptr; + + if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) { + // See if there's a keyframe in this position + key_under_cursor = GetObjectAtPoint(event->pos()); + + bool holding_shift = event->modifiers() & Qt::ShiftModifier; + + if (!key_under_cursor || !IsSelected(key_under_cursor)) { + if (!holding_shift) { + // If not already selecting and not holding shift, clear the current selection + ClearSelection(); + } + + // Add item to selection, either nothing if shift wasn't held, or the existing selection + if (key_under_cursor) { + Select(key_under_cursor); + view_->SelectionManagerSelectEvent(key_under_cursor); + } + } else if (holding_shift) { + // If selected and holding shift, de-select this item but do nothing else + Deselect(key_under_cursor); + view_->SelectionManagerDeselectEvent(key_under_cursor); + key_under_cursor = nullptr; + } + } + + return key_under_cursor; + } + + bool IsDragging() const + { + return !dragging_.isEmpty(); + } + + void DragStart(T *initial_item, QMouseEvent *event) + { + initial_drag_item_ = initial_item; + + dragging_.resize(selected_.size()); + for (int i=0; itime(), view_->TimeToScene(obj->time())}; + } + + drag_mouse_start_ = view_->mapToScene(event->pos()); + } + + void DragMove(QMouseEvent *event, const QString &tip_format) + { + QPointF diff = view_->mapToScene(event->pos()) - drag_mouse_start_; + + for (int i=0; iSceneToTimeNoGrid(dragging_.at(i).x + diff.x()); + T *sel = selected_.at(i); + + // Magic number: use interval of 1ms to avoid collisions + rational adj(1, 1000); + if (dragging_.at(i).time < proposed_time) { + adj = -adj; + } + + while (true) { + NodeKeyframe *key_at_time = sel->parent()->GetKeyframeAtTimeOnTrack(sel->input(), proposed_time, sel->track(), sel->element()); + if (!key_at_time || key_at_time == sel) { + break; + } + + proposed_time += adj; + } + + sel->set_time(proposed_time); + } + + // Show information about this keyframe + QString tip = Timecode::time_to_timecode(initial_drag_item_->time(), timebase_, + Core::instance()->GetTimecodeDisplay(), false); + + if (!tip_format.isEmpty()) { + tip = tip_format.arg(tip); + } + + QToolTip::hideText(); + QToolTip::showText(QCursor::pos(), tip); + } + + void DragStop(MultiUndoCommand *command) + { + QToolTip::hideText(); + + for (int i=0; iadd_child(new SetTimeCommand(selected_.at(i), selected_.at(i)->time(), dragging_.at(i).time)); + } + + dragging_.clear(); + } + + void RubberBandStart(QMouseEvent *event) + { + if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) { + rubberband_start_ = event->pos(); + + rubberband_ = new QRubberBand(QRubberBand::Rectangle, view_); + rubberband_->setGeometry(QRect(rubberband_start_.x(), rubberband_start_.y(), 0, 0)); + rubberband_->show(); + + rubberband_preselected_ = selected_; + } + } + + void RubberBandMove(QMouseEvent *event) + { + if (IsRubberBanding()) { + QRect band_rect = QRect(rubberband_start_, event->pos()).normalized(); + rubberband_->setGeometry(band_rect); + + QRectF scene_rect = view_->mapToScene(band_rect).boundingRect(); + + selected_ = rubberband_preselected_; + foreach (const DrawnObject &kp, drawn_objects_) { + if (scene_rect.intersects(kp.second)) { + Select(kp.first); + } + } + } + } + + void RubberBandStop() + { + if (IsRubberBanding()) { + delete rubberband_; + rubberband_ = nullptr; + } + } + + bool IsRubberBanding() const + { + return rubberband_; + } + +private: + class SetTimeCommand : public UndoCommand + { + public: + SetTimeCommand(T* key, const rational& time) + { + key_ = key; + new_time_ = time; + old_time_ = key_->time(); + } + + SetTimeCommand(T* key, const rational& new_time, const rational& old_time) + { + key_ = key; + new_time_ = new_time; + old_time_ = old_time; + } + + virtual Project* GetRelevantProject() const override + { + return key_->parent()->project(); + } + + protected: + virtual void redo() override + { + key_->set_time(new_time_); + } + + virtual void undo() override + { + key_->set_time(old_time_); + } + + private: + T* key_; + + rational old_time_; + rational new_time_; + + }; + + TimeBasedView *view_; + + using DrawnObject = QPair; + QVector drawn_objects_; + + QVector selected_; + + struct DragObject + { + rational time; + double x; + }; + + QVector dragging_; + + T *initial_drag_item_; + + QPointF drag_mouse_start_; + + rational timebase_; + + QRubberBand *rubberband_; + QPoint rubberband_start_; + QVector rubberband_preselected_; + +}; + +} + +#endif // TIMEBASEDVIEWSELECTIONMANAGER_H diff --git a/app/widget/timebased/timescaledobject.cpp b/app/widget/timebased/timescaledobject.cpp index 85b154a48..5ea4176aa 100644 --- a/app/widget/timebased/timescaledobject.cpp +++ b/app/widget/timebased/timescaledobject.cpp @@ -72,6 +72,13 @@ rational TimeScaledObject::SceneToTime(const double &x, const double &x_scale, c return rational(rounded_x_mvmt * timebase.numerator(), timebase.denominator()); } +rational TimeScaledObject::SceneToTimeNoGrid(const double &x, const double &x_scale) +{ + double unscaled_time = x / x_scale; + + return rational::fromDouble(unscaled_time); +} + double TimeScaledObject::TimeToScene(const rational &time) const { return time.toDouble() * scale_; @@ -82,6 +89,11 @@ rational TimeScaledObject::SceneToTime(const double &x, bool round) const return SceneToTime(x, scale_, timebase_, round); } +rational TimeScaledObject::SceneToTimeNoGrid(const double &x) const +{ + return SceneToTimeNoGrid(x, scale_); +} + void TimeScaledObject::SetMaximumScale(const double &max) { max_scale_ = max; diff --git a/app/widget/timebased/timescaledobject.h b/app/widget/timebased/timescaledobject.h index 8c858c8fc..acdc44acb 100644 --- a/app/widget/timebased/timescaledobject.h +++ b/app/widget/timebased/timescaledobject.h @@ -42,6 +42,7 @@ public: const double& timebase_dbl() const; static rational SceneToTime(const double &x, const double& x_scale, const rational& timebase, bool round = false); + static rational SceneToTimeNoGrid(const double &x, const double& x_scale); const double& GetScale() const; const double &GetMaximumScale() const { return max_scale_; } @@ -54,6 +55,7 @@ public: double TimeToScene(const rational& time) const; rational SceneToTime(const double &x, bool round = false) const; + rational SceneToTimeNoGrid(const double &x) const; protected: virtual void TimebaseChangedEvent(const rational&){} diff --git a/app/widget/timelinewidget/timelinewidget.cpp b/app/widget/timelinewidget/timelinewidget.cpp index 574bed300..1c4f02d7c 100644 --- a/app/widget/timelinewidget/timelinewidget.cpp +++ b/app/widget/timelinewidget/timelinewidget.cpp @@ -28,7 +28,6 @@ #include "core.h" #include "common/range.h" #include "common/timecodefunctions.h" -#include "dialog/nodeproperties/nodepropertiesdialog.h" #include "dialog/sequence/sequence.h" #include "dialog/speedduration/speeddurationdialog.h" #include "node/block/transition/transition.h" @@ -1027,17 +1026,7 @@ void TimelineWidget::ShowContextMenu() menu.addSeparator(); QAction* properties_action = menu.addAction(tr("Properties")); - connect(properties_action, &QAction::triggered, this, [this](){ - QVector block_items = GetSelectedBlocks(); - QVector nodes; - - foreach (Block* i, block_items) { - nodes.append(i); - } - - NodePropertiesDialog npd(nodes, timebase(), this); - npd.exec(); - }); + connect(properties_action, &QAction::triggered, this, &TimelineWidget::ShowSpeedDurationDialogForSelectedClips); } if (selected.isEmpty()) { diff --git a/app/widget/timelinewidget/tool/add.cpp b/app/widget/timelinewidget/tool/add.cpp index 07343ccd3..a9c9d3615 100644 --- a/app/widget/timelinewidget/tool/add.cpp +++ b/app/widget/timelinewidget/tool/add.cpp @@ -116,7 +116,7 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) NodeGraph* graph = static_cast(parent()->GetConnectedNode()->parent()); command->add_child(new NodeAddCommand(graph, clip)); - command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0))); command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), track.index(), clip, @@ -155,10 +155,10 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event) } if (node_to_add) { - QPointF extra_node_offset(-1, 0); + QPointF extra_node_offset(kDefaultDistanceFromOutput, 0); command->add_child(new NodeAddCommand(graph, node_to_add)); command->add_child(new NodeEdgeAddCommand(node_to_add, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionCommand(node_to_add, clip, extra_node_offset, false)); + command->add_child(new NodeSetPositionCommand(node_to_add, clip, extra_node_offset)); } Core::instance()->undo_stack()->push(command); diff --git a/app/widget/timelinewidget/tool/import.cpp b/app/widget/timelinewidget/tool/import.cpp index 5f379679f..f9ab4b6f8 100644 --- a/app/widget/timelinewidget/tool/import.cpp +++ b/app/widget/timelinewidget/tool/import.cpp @@ -355,7 +355,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeAddCommand(dst_graph, new_sequence)); command->add_child(new FolderAddChild(Core::instance()->GetSelectedFolderInActiveProject(), new_sequence)); - command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0), false)); + command->add_child(new NodeSetPositionCommand(new_sequence, new_sequence, QPointF(0, 0))); new_sequence->add_default_nodes(command); FootageToGhosts(0, dragged_footage_, new_sequence->GetVideoParams().time_base(), 0); @@ -394,10 +394,14 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeAddCommand(dst_graph, clip)); // Position clip in its own context - command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0), false)); + command->add_child(new NodeSetPositionCommand(clip, clip, QPointF(0, 0))); + + int dep_pos = kDefaultDistanceFromOutput; // Position footage in its context - command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(-2, 0), false)); + command->add_child(new NodeSetPositionCommand(footage_stream.footage, clip, QPointF(dep_pos, 0))); + + dep_pos++; switch (Track::Reference::TypeFromString(footage_stream.output)) { case Track::kVideo: @@ -409,7 +413,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(transform, TransformDistortNode::kTextureInput))); command->add_child(new NodeEdgeAddCommand(transform, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(-1, 0), false)); + command->add_child(new NodeSetPositionCommand(transform, clip, QPointF(dep_pos, 0))); break; } case Track::kAudio: @@ -421,7 +425,7 @@ void ImportTool::DropGhosts(bool insert) command->add_child(new NodeEdgeAddCommand(footage_stream.footage, NodeInput(volume_node, VolumeNode::kSamplesInput))); command->add_child(new NodeEdgeAddCommand(volume_node, NodeInput(clip, ClipBlock::kBufferIn))); - command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(-1, 0), false)); + command->add_child(new NodeSetPositionCommand(volume_node, clip, QPointF(dep_pos, 0))); break; } default: diff --git a/app/widget/timelinewidget/tool/tool.cpp b/app/widget/timelinewidget/tool/tool.cpp index 752409b78..2612b9697 100644 --- a/app/widget/timelinewidget/tool/tool.cpp +++ b/app/widget/timelinewidget/tool/tool.cpp @@ -24,6 +24,8 @@ namespace olive { +const int TimelineTool::kDefaultDistanceFromOutput = -4; + TimelineTool::TimelineTool(TimelineWidget *parent) : dragging_(false), parent_(parent) diff --git a/app/widget/timelinewidget/tool/tool.h b/app/widget/timelinewidget/tool/tool.h index d229e1ad3..c627f542e 100644 --- a/app/widget/timelinewidget/tool/tool.h +++ b/app/widget/timelinewidget/tool/tool.h @@ -84,6 +84,8 @@ protected: TimelineCoordinate drag_start_; + static const int kDefaultDistanceFromOutput; + private: TimelineWidget* parent_; diff --git a/app/widget/timelinewidget/tool/transition.cpp b/app/widget/timelinewidget/tool/transition.cpp index f1a1f4fdd..594659ccb 100644 --- a/app/widget/timelinewidget/tool/transition.cpp +++ b/app/widget/timelinewidget/tool/transition.cpp @@ -113,7 +113,7 @@ void TransitionTool::MouseRelease(TimelineViewMouseEvent *event) command->add_child(new NodeAddCommand(static_cast(parent()->GetConnectedNode()->parent()), transition)); - command->add_child(new NodeSetPositionCommand(transition, transition, QPointF(0, 0), false)); + command->add_child(new NodeSetPositionCommand(transition, transition, QPointF(0, 0))); command->add_child(new TrackPlaceBlockCommand(sequence()->track_list(track.type()), track.index(), @@ -138,8 +138,8 @@ void TransitionTool::MouseRelease(TimelineViewMouseEvent *event) command->add_child(new NodeEdgeAddCommand(in_block, NodeInput(transition, TransitionBlock::kInBlockInput))); - command->add_child(new NodeSetPositionCommand(out_block, transition, QPointF(-1, -0.5), false)); - command->add_child(new NodeSetPositionCommand(in_block, transition, QPointF(-1, 0.5), false)); + command->add_child(new NodeSetPositionCommand(out_block, transition, QPointF(-1, -0.5))); + command->add_child(new NodeSetPositionCommand(in_block, transition, QPointF(-1, 0.5))); } else { Block* block_to_transition = Node::ValueToPtr(ghost_->GetData(TimelineViewGhostItem::kAttachedBlock)); QString transition_input_to_connect; @@ -154,7 +154,7 @@ void TransitionTool::MouseRelease(TimelineViewMouseEvent *event) command->add_child(new NodeEdgeAddCommand(block_to_transition, NodeInput(transition, transition_input_to_connect))); - command->add_child(new NodeSetPositionCommand(block_to_transition, transition, QPointF(-1, 0), false)); + command->add_child(new NodeSetPositionCommand(block_to_transition, transition, QPointF(-1, 0))); } Core::instance()->undo_stack()->push(command); diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.cpp b/app/widget/timelinewidget/undo/timelineundogeneral.cpp index 63958f250..a64d1e342 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.cpp +++ b/app/widget/timelinewidget/undo/timelineundogeneral.cpp @@ -73,131 +73,121 @@ void BlockSetMediaInCommand::undo() // // TimelineAddTrackCommand // -TimelineAddTrackCommand::TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) +TimelineAddTrackCommand::TimelineAddTrackCommand(TrackList *timeline, bool automerge_tracks) : + timeline_(timeline), + merge_(nullptr), + position_command_(nullptr) { - timeline_ = timeline; - position_command_ = nullptr; - + // Create new track track_ = new Track(); track_->setParent(&memory_manager_); - if (timeline->GetTrackCount() > 0 && automerge_tracks) { - if (timeline_->type() == Track::kVideo) { - merge_ = new MergeNode(); - base_ = NodeInput(merge_, MergeNode::kBaseIn); - blend_ = NodeInput(merge_, MergeNode::kBlendIn); - } else if (timeline_->type() == Track::kAudio) { - merge_ = new MathNode(); - base_ = NodeInput(merge_, MathNode::kParamAIn); - blend_ = NodeInput(merge_, MathNode::kParamBIn); - } else { - merge_ = nullptr; - } - } else { - merge_ = nullptr; + // Determine what input to connect it to + QString relevant_input; + + if (timeline_->type() == Track::kVideo) { + relevant_input = Sequence::kTextureInput; + } else if (timeline_->type() == Track::kAudio) { + relevant_input = Sequence::kSamplesInput; } - if (merge_) { - merge_->setParent(&memory_manager_); + // If we have an input to connect to, set it as our `direct` connection + if (!relevant_input.isEmpty()) { + direct_ = NodeInput(timeline_->parent(), relevant_input); + + // If we're automerging and something is already connected, determine if/how to merge it + if (automerge_tracks && direct_.IsConnected()) { + if (timeline_->type() == Track::kVideo) { + // Use merge for video + merge_ = new MergeNode(); + base_ = NodeInput(merge_, MergeNode::kBaseIn); + blend_ = NodeInput(merge_, MergeNode::kBlendIn); + } else if (timeline_->type() == Track::kAudio) { + // Use math (add) for audio + merge_ = new MathNode(); + base_ = NodeInput(merge_, MathNode::kParamAIn); + blend_ = NodeInput(merge_, MathNode::kParamBIn); + } + + if (merge_) { + // If we got created a merge node, ensure it's parented + merge_->setParent(&memory_manager_); + } + } } } void TimelineAddTrackCommand::redo() { - // Add track + // Get sequence + Sequence* sequence = timeline_->parent(); + + // Add track to sequence track_->setParent(timeline_->GetParentGraph()); timeline_->ArrayAppend(); Node::ConnectEdge(track_, timeline_->track_input(timeline_->ArraySize() - 1)); + qreal position_factor = 0.5; + if (timeline_->type() == Track::kVideo) { + position_factor = -position_factor; + } + bool create_pos_command = (!position_command_ && (timeline_->type() == Track::kVideo || timeline_->type() == Track::kAudio)); + if (create_pos_command) { + position_command_ = new MultiUndoCommand(); + } + // Add merge if applicable - Track* last_track = nullptr; if (merge_) { + // Determine what was previously connected + Node *previous_connection = direct_.GetConnectedOutput(); + + // Add merge to graph merge_->setParent(timeline_->GetParentGraph()); - last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); - - // Whatever this track used to be connected to, connect the merge instead - const Node::OutputConnections edges = last_track->output_connections(); - for (const Node::OutputConnection& ic : edges) { - const NodeInput& i = ic.second; - - // Ignore the track input, but funnel everything else through our merge - if (i.node() != timeline_->parent() || i.input() != timeline_->track_input()) { - Node::DisconnectEdge(last_track, i); - Node::ConnectEdge(merge_, i); - } - } - - // Connect this as the "blend" track + // Connect merge between what used to be here + Node::DisconnectEdge(previous_connection, direct_); + Node::ConnectEdge(merge_, direct_); + Node::ConnectEdge(previous_connection, base_); Node::ConnectEdge(track_, blend_); - Node::ConnectEdge(last_track, base_); - } else if (timeline_->GetTrackCount() == 1) { - // If this was the first track we added, - QString relevant_input; - if (timeline_->type() == Track::kVideo) { - relevant_input = ViewerOutput::kTextureInput; - } else if (timeline_->type() == Track::kAudio) { - relevant_input = ViewerOutput::kSamplesInput; + if (create_pos_command) { + position_command_->add_child(new NodeSetPositionCommand(track_, sequence, sequence->GetNodePositionInContext(sequence) + QPointF(-1, -position_factor))); + position_command_->add_child(new NodeSetPositionCommand(merge_, sequence, sequence->GetNodePositionInContext(sequence))); + position_command_->add_child(new NodeSetPositionAndDependenciesRecursivelyCommand(merge_, sequence, sequence->GetNodePositionInContext(sequence) + QPointF(-1, position_factor * timeline_->GetTrackCount()))); } + } else if (direct_.IsValid() && !direct_.IsConnected()) { + // If no merge, we have a direct connection, and nothing else is connected, connect this + Node::ConnectEdge(track_, direct_); - if (!relevant_input.isEmpty() && !timeline_->parent()->IsInputConnected(relevant_input)) { - direct_ = NodeInput(timeline_->parent(), relevant_input); - - Node::ConnectEdge(track_, direct_); - } else { - direct_ = NodeInput(); + if (create_pos_command) { + // Just position directly next to the context node + position_command_->add_child(new NodeSetPositionCommand(track_, sequence, sequence->GetNodePositionInContext(sequence) + QPointF(-1, position_factor))); } } - // Position track in context - if (!position_command_) { - int track_count = timeline_->parent()->GetTracks().size(); - position_command_ = new MultiUndoCommand(); - - // Position either the merge or the track as an "element" - Node *node_to_position = merge_ ? merge_ : track_; - double node_index = track_count - 1; - if (merge_) { - node_index -= 1; - position_command_->add_child(new NodeRemovePositionFromContextCommand(last_track, timeline_->parent())); - } - - position_command_->add_child(new NodeSetPositionAsChildCommand(node_to_position, timeline_->parent(), timeline_->parent(), node_index, track_count, true)); - - // If we positioned a merge, position the tracks as children of the merge - if (merge_) { - // `last_track` should be non-null if `merge_` is non-null - position_command_->add_child(new NodeSetPositionAsChildCommand(last_track, merge_, timeline_->parent(), 0, 2, true)); - position_command_->add_child(new NodeSetPositionAsChildCommand(track_, merge_, timeline_->parent(), 1, 2, true)); - } + // Run position command if we created one + if (position_command_) { + position_command_->redo_now(); } - position_command_->redo_now(); } void TimelineAddTrackCommand::undo() { - position_command_->undo_now(); + if (position_command_) { + position_command_->undo_now(); + } // Remove merge if applicable if (merge_) { - // Assume whatever this merge is connected to USED to be connected to the last track - Track* last_track = timeline_->GetTrackAt(timeline_->GetTrackCount()-2); + Node *previous_connection = base_.GetConnectedOutput(); Node::DisconnectEdge(track_, blend_); - Node::DisconnectEdge(last_track, base_); - - // Make copy of edges since the node's internal array will change as we disconnect things - const Node::OutputConnections edges = merge_->output_connections(); - for (const Node::OutputConnection& ic : edges) { - const NodeInput& i = ic.second; - - Node::DisconnectEdge(merge_, i); - Node::ConnectEdge(last_track, i); - } + Node::DisconnectEdge(previous_connection, base_); + Node::DisconnectEdge(merge_, direct_); + Node::ConnectEdge(previous_connection, direct_); merge_->setParent(&memory_manager_); - } else if (direct_.IsValid()) { + } else if (direct_.IsValid() && direct_.GetConnectedOutput() == track_) { Node::DisconnectEdge(track_, direct_); } @@ -496,11 +486,6 @@ void TrackReplaceBlockWithGapCommand::redo() our_gap_->setParent(track_->parent()); track_->ReplaceBlock(block_, our_gap_); - - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(our_gap_, track_, track_, our_gap_->index(), track_->Blocks().size(), true); - } - position_command_->redo_now(); } track_->EndOperation(); @@ -533,8 +518,6 @@ void TrackReplaceBlockWithGapCommand::undo() track_->ReplaceBlock(our_gap_, block_); our_gap_->setParent(&memory_manager_); - position_command_->undo_now(); - } else { // If we're here, assume that we extended an existing gap diff --git a/app/widget/timelinewidget/undo/timelineundogeneral.h b/app/widget/timelinewidget/undo/timelineundogeneral.h index 423f4da84..490d9b082 100644 --- a/app/widget/timelinewidget/undo/timelineundogeneral.h +++ b/app/widget/timelinewidget/undo/timelineundogeneral.h @@ -203,16 +203,10 @@ public: existing_gap_(nullptr), existing_merged_gap_(nullptr), our_gap_(nullptr), - handle_transitions_(handle_transitions), - position_command_(nullptr) + handle_transitions_(handle_transitions) { } - virtual ~TrackReplaceBlockWithGapCommand() override - { - delete position_command_; - } - virtual Project* GetRelevantProject() const override { return block_->project(); @@ -236,8 +230,6 @@ private: bool handle_transitions_; - NodeSetPositionAsChildCommand* position_command_; - QObject memory_manager_; QVector transition_remove_commands_; diff --git a/app/widget/timelinewidget/undo/timelineundopointer.cpp b/app/widget/timelinewidget/undo/timelineundopointer.cpp index 4f5c448b6..0f6501b19 100644 --- a/app/widget/timelinewidget/undo/timelineundopointer.cpp +++ b/app/widget/timelinewidget/undo/timelineundopointer.cpp @@ -324,7 +324,6 @@ TrackPlaceBlockCommand::~TrackPlaceBlockCommand() { delete ripple_remove_command_; qDeleteAll(add_track_commands_); - qDeleteAll(position_commands_); } void TrackPlaceBlockCommand::redo() @@ -367,14 +366,6 @@ void TrackPlaceBlockCommand::redo() } track->AppendBlock(insert_); - - if (position_commands_.isEmpty()) { - // Create position commands for insert and gap if necessary - if (gap_) { - position_commands_.append(new NodeSetPositionAsChildCommand(gap_, track, track, gap_->index(), track->Blocks().size(), true)); - } - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } } else { // Place the Block at this point if (!ripple_remove_command_) { @@ -384,10 +375,6 @@ void TrackPlaceBlockCommand::redo() ripple_remove_command_->redo_now(); track->InsertBlockAfter(insert_, ripple_remove_command_->GetInsertionIndex()); - - if (position_commands_.isEmpty()) { - position_commands_.append(new NodeSetPositionAsChildCommand(insert_, track, track, insert_->index(), track->Blocks().size(), true)); - } } track->EndOperation(); @@ -397,18 +384,10 @@ void TrackPlaceBlockCommand::redo() foreach (const TimeRange &r, ranges_to_invalidate) { track->Node::InvalidateCache(r, Track::kBlockInput); } - - for (int i=0; iredo_now(); - } } void TrackPlaceBlockCommand::undo() { - for (int i=position_commands_.size()-1; i>=0; i--) { - position_commands_.at(i)->undo_now(); - } - Track* t = timeline_->GetTrackAt(track_index_); TimeRange insert_range(insert_->in(), insert_->out()); diff --git a/app/widget/timelinewidget/undo/timelineundopointer.h b/app/widget/timelinewidget/undo/timelineundopointer.h index fc7cf6566..4483d81d2 100644 --- a/app/widget/timelinewidget/undo/timelineundopointer.h +++ b/app/widget/timelinewidget/undo/timelineundopointer.h @@ -198,7 +198,6 @@ private: QVector add_track_commands_; QObject memory_manager_; TrackRippleRemoveAreaCommand* ripple_remove_command_; - QVector position_commands_; }; diff --git a/app/widget/timelinewidget/undo/timelineundosplit.cpp b/app/widget/timelinewidget/undo/timelineundosplit.cpp index 05c3b2707..06cdd1006 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.cpp +++ b/app/widget/timelinewidget/undo/timelineundosplit.cpp @@ -61,12 +61,6 @@ void BlockSplitCommand::redo() // Insert new block track->InsertBlockAfter(new_block(), block_); - // Position the block - if (!position_command_) { - position_command_ = new NodeSetPositionAsChildCommand(new_block(), track, track, new_block()->index(), track->Blocks().size(), true); - } - position_command_->redo_now(); - // If the block had an out transition, we move it to the new block moved_transition_ = NodeInput(); @@ -96,8 +90,6 @@ void BlockSplitCommand::undo() Node::ConnectEdge(block_, moved_transition_); } - position_command_->undo_now(); - block_->set_length_and_media_out(old_length_); track->RippleRemoveBlock(new_block()); diff --git a/app/widget/timelinewidget/undo/timelineundosplit.h b/app/widget/timelinewidget/undo/timelineundosplit.h index 82b57ebca..6984377f8 100644 --- a/app/widget/timelinewidget/undo/timelineundosplit.h +++ b/app/widget/timelinewidget/undo/timelineundosplit.h @@ -31,15 +31,13 @@ public: block_(block), new_block_(nullptr), point_(point), - reconnect_tree_command_(nullptr), - position_command_(nullptr) + reconnect_tree_command_(nullptr) { } virtual ~BlockSplitCommand() override { delete reconnect_tree_command_; - delete position_command_; } virtual Project* GetRelevantProject() const override @@ -70,8 +68,6 @@ private: NodeInput moved_transition_; - NodeSetPositionAsChildCommand* position_command_; - }; class BlockSplitPreservingLinksCommand : public UndoCommand { diff --git a/app/widget/videoparamedit/videoparamedit.cpp b/app/widget/videoparamedit/videoparamedit.cpp deleted file mode 100644 index 13ddfc825..000000000 --- a/app/widget/videoparamedit/videoparamedit.cpp +++ /dev/null @@ -1,391 +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 "videoparamedit.h" - -#include - -namespace olive { - -VideoParamEdit::VideoParamEdit(QWidget* parent) : - QWidget(parent), - color_manager_(nullptr), - mask_(0) -{ - QGridLayout* layout = new QGridLayout(this); - - layout->setMargin(0); - - int row = 0; - - // Enabled - enabled_lbl_ = new QLabel(tr("Enabled:")); - layout->addWidget(enabled_lbl_, row, 0); - enabled_box_ = new QCheckBox(); - connect(enabled_box_, &QCheckBox::clicked, this, &VideoParamEdit::Changed); - layout->addWidget(enabled_box_, row, 1); - - row++; - - // Width - width_lbl_ = new QLabel(tr("Width:")); - layout->addWidget(width_lbl_, row, 0); - - width_slider_ = new IntegerSlider(); - width_slider_->SetMinimum(1); - width_slider_->SetMaximum(32768); - connect(width_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(width_slider_, row, 1); - - row++; - - // Height - height_lbl_ = new QLabel(tr("Height:")); - layout->addWidget(height_lbl_, row, 0); - - height_slider_ = new IntegerSlider(); - height_slider_->SetMinimum(1); - height_slider_->SetMaximum(32768); - connect(height_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(height_slider_, row, 1); - - row++; - - // Depth - depth_lbl_ = new QLabel(tr("Depth:")); - layout->addWidget(depth_lbl_, row, 0); - - depth_slider_ = new IntegerSlider(); - depth_slider_->SetMinimum(1); - depth_slider_->SetMaximum(32768); - connect(depth_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(depth_slider_, row, 1); - - row++; - - // Pixel Format - format_lbl_ = new QLabel(tr("Format:")); - layout->addWidget(format_lbl_, row, 0); - format_combobox_ = new PixelFormatComboBox(true); - connect(format_combobox_, static_cast(&PixelFormatComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(format_combobox_, row, 1); - - row++; - - // Frame Rate - frame_rate_lbl_ = new QLabel(tr("Frame Rate:")); - layout->addWidget(frame_rate_lbl_, row, 0); - - frame_rate_combobox_ = new FrameRateComboBox(); - connect(frame_rate_combobox_, &FrameRateComboBox::FrameRateChanged, this, &VideoParamEdit::Changed); - layout->addWidget(frame_rate_combobox_, row, 1); - - frame_rate_slider_ = new RationalSlider(); - frame_rate_slider_->SetMinimum(0); - frame_rate_slider_->SetDecimalPlaces(3); - frame_rate_slider_->SetAutoTrimDecimalPlaces(true); - frame_rate_slider_->SetTimebase(rational(1, 1000)); // Drag interval - frame_rate_slider_->DisableDisplayType(RationalSlider::kTime); - connect(frame_rate_slider_, &RationalSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(frame_rate_slider_, row, 1); - - row++; - - // Pixel Aspect Ratio - pixel_aspect_lbl_ = new QLabel(tr("Pixel Aspect Ratio:")); - layout->addWidget(pixel_aspect_lbl_, row, 0); - - pixel_aspect_combobox_ = new PixelAspectRatioComboBox(); - connect(pixel_aspect_combobox_, static_cast(&PixelAspectRatioComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(pixel_aspect_combobox_, row, 1); - - row++; - - // Interlacing - interlaced_lbl_ = new QLabel(tr("Interlacing:")); - layout->addWidget(interlaced_lbl_, row, 0); - - interlaced_combobox_ = new InterlacedComboBox(); - connect(interlaced_combobox_, static_cast(&InterlacedComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(interlaced_combobox_, row, 1); - - row++; - - // Channel Count - channel_count_lbl_ = new QLabel(tr("Channel Count:")); - layout->addWidget(channel_count_lbl_, row, 0); - - channel_count_combobox_ = new QComboBox(); - channel_count_combobox_->addItem(tr("RGB"), VideoParams::kRGBChannelCount); - channel_count_combobox_->addItem(tr("RGBA"), VideoParams::kRGBAChannelCount); - connect(channel_count_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(channel_count_combobox_, row, 1); - - row++; - - // Divider - divider_lbl_ = new QLabel(tr("Divider:")); - layout->addWidget(divider_lbl_, row, 0); - - divider_combobox_ = new VideoDividerComboBox(); - connect(divider_combobox_, static_cast(&VideoDividerComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(divider_combobox_, row, 1); - - row++; - - // Stream Index - stream_index_lbl_ = new QLabel(tr("Stream Index:")); - layout->addWidget(stream_index_lbl_, row, 0); - stream_index_slider_ = new IntegerSlider(); - stream_index_slider_->SetMinimum(0); - connect(stream_index_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(stream_index_slider_, row, 1); - - row++; - - // Video type - video_type_lbl_ = new QLabel(tr("Video Type:")); - layout->addWidget(video_type_lbl_, row, 0); - video_type_combobox_ = new QComboBox(); - video_type_combobox_->addItem(tr("Video"), VideoParams::kVideoTypeVideo); - video_type_combobox_->addItem(tr("Still"), VideoParams::kVideoTypeStill); - video_type_combobox_->addItem(tr("Image Sequence"), VideoParams::kVideoTypeImageSequence); - connect(video_type_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(video_type_combobox_, row, 1); - - row++; - - // Start time (for image sequences) - start_time_lbl_ = new QLabel(tr("Start Time")); - layout->addWidget(start_time_lbl_, row, 0); - - start_time_slider_ = new IntegerSlider(); - start_time_slider_->SetMinimum(0); - connect(start_time_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(start_time_slider_, row, 1); - - row++; - - // End time (for image sequences) - end_time_lbl_ = new QLabel(tr("End Time")); - layout->addWidget(end_time_lbl_, row, 0); - - end_time_slider_ = new IntegerSlider(); - end_time_slider_->SetMinimum(0); - connect(end_time_slider_, &IntegerSlider::ValueChanged, this, &VideoParamEdit::Changed); - layout->addWidget(end_time_slider_, row, 1); - - row++; - - // Premultiplied alpha - premultiplied_alpha_lbl_ = new QLabel(tr("Premultiplied Alpha")); - layout->addWidget(premultiplied_alpha_lbl_, row, 0); - - premultiplied_alpha_box_ = new QCheckBox(); - connect(premultiplied_alpha_box_, &QCheckBox::clicked, this, &VideoParamEdit::Changed); - layout->addWidget(premultiplied_alpha_box_, row, 1); - - row++; - - // Colorspace - colorspace_lbl_ = new QLabel(tr("Colorspace")); - layout->addWidget(colorspace_lbl_, row, 0); - - colorspace_combobox_ = new QComboBox(); - connect(colorspace_combobox_, static_cast(&QComboBox::currentIndexChanged), this, &VideoParamEdit::Changed); - layout->addWidget(colorspace_combobox_, row, 1); -} - -void VideoParamEdit::SetParameterMask(uint64_t mask) -{ - mask_ = mask; - - width_lbl_->setVisible(mask & kWidthHeight); - width_slider_->setVisible(mask & kWidthHeight); - height_lbl_->setVisible(mask & kWidthHeight); - height_slider_->setVisible(mask & kWidthHeight); - - depth_lbl_->setVisible(mask & kDepth); - depth_slider_->setVisible(mask & kDepth); - - frame_rate_lbl_->setVisible(mask & kFrameRate); - frame_rate_combobox_->setVisible((mask & kFrameRate) && !(mask & kFrameRateIsArbitrary)); - frame_rate_slider_->setVisible((mask & kFrameRate) && (mask & kFrameRateIsArbitrary)); - - pixel_aspect_lbl_->setVisible(mask & kPixelAspect); - pixel_aspect_combobox_->setVisible(mask & kPixelAspect); - - interlaced_lbl_->setVisible(mask & kInterlacing); - interlaced_combobox_->setVisible(mask & kInterlacing); - - enabled_lbl_->setVisible(mask & kEnabled); - enabled_box_->setVisible(mask & kEnabled); - - format_lbl_->setVisible(mask & kFormat); - format_combobox_->setVisible(mask & kFormat); - - channel_count_lbl_->setVisible(mask & kChannelCount); - channel_count_combobox_->setVisible(mask & kChannelCount); - - divider_lbl_->setVisible(mask & kDivider); - divider_combobox_->setVisible(mask & kDivider); - - stream_index_lbl_->setVisible(mask & kStreamIndex); - stream_index_slider_->setVisible(mask & kStreamIndex); - - video_type_lbl_->setVisible(mask & kIsImageSequence); - video_type_combobox_->setVisible(mask & kIsImageSequence); - - start_time_lbl_->setVisible(mask & kStartTime); - start_time_slider_->setVisible(mask & kStartTime); - - end_time_lbl_->setVisible(mask & kEndTime); - end_time_slider_->setVisible(mask & kEndTime); - - premultiplied_alpha_lbl_->setVisible(mask & kPremultipliedAlpha); - premultiplied_alpha_box_->setVisible(mask & kPremultipliedAlpha); - - colorspace_lbl_->setVisible(mask & kColorspace); - colorspace_combobox_->setVisible(mask & kColorspace); -} - -VideoParams VideoParamEdit::GetVideoParams() const -{ - VideoParams p; - - p.set_enabled(enabled_box_->isChecked()); - p.set_width(width_slider_->GetValue()); - p.set_height(height_slider_->GetValue()); - p.set_depth(depth_slider_->GetValue()); - - { - rational using_frame_rate; - - if (mask_ & kFrameRateIsArbitrary) { - using_frame_rate = frame_rate_slider_->GetValue(); - } else { - using_frame_rate = frame_rate_combobox_->GetFrameRate(); - } - - p.set_frame_rate(using_frame_rate); - if (mask_ & kFrameRateIsNotTimebase) { - // Frame rate editor will only edit the frame rate - p.set_time_base(timebase_temp_); - } else { - p.set_time_base(using_frame_rate.flipped()); - } - } - - p.set_pixel_aspect_ratio(pixel_aspect_combobox_->GetPixelAspectRatio()); - p.set_interlacing(interlaced_combobox_->GetInterlaceMode()); - p.set_format(format_combobox_->GetPixelFormat()); - p.set_channel_count(channel_count_combobox_->currentData().toInt()); - p.set_divider(divider_combobox_->GetDivider()); - p.set_stream_index(stream_index_slider_->GetValue()); - p.set_video_type(static_cast(video_type_combobox_->currentData().toInt())); - p.set_start_time(start_time_slider_->GetValue()); - p.set_duration(end_time_slider_->GetValue() - start_time_slider_->GetValue() + 1); - p.set_premultiplied_alpha(premultiplied_alpha_box_->isChecked()); - p.set_colorspace(colorspace_combobox_->currentData().toString()); - - return p; -} - -void VideoParamEdit::SetVideoParams(const VideoParams &p) -{ - blockSignals(true); - - enabled_box_->setChecked(p.enabled()); - width_slider_->SetValue(p.width()); - height_slider_->SetValue(p.height()); - depth_slider_->SetValue(p.depth()); - - frame_rate_combobox_->SetFrameRate(p.frame_rate()); - frame_rate_slider_->SetValue(p.frame_rate()); - timebase_temp_ = p.time_base(); - - pixel_aspect_combobox_->SetPixelAspectRatio(p.pixel_aspect_ratio()); - interlaced_combobox_->SetInterlaceMode(p.interlacing()); - format_combobox_->SetPixelFormat(p.format()); - SetChannelCount(p.channel_count()); - divider_combobox_->SetDivider(p.divider()); - stream_index_slider_->SetValue(p.stream_index()); - SetVideoTypeComboBox(p.video_type()); - start_time_slider_->SetValue(p.start_time()); - end_time_slider_->SetValue(p.start_time() + p.duration() - 1); - premultiplied_alpha_box_->setChecked(p.premultiplied_alpha()); - - if (color_manager_) { - // Assume colorspace box has been populated correctly - for (int i=0; icount(); i++) { - if (colorspace_combobox_->itemData(i).toString() == p.colorspace()) { - colorspace_combobox_->setCurrentIndex(i); - break; - } - } - } else { - // Box is empty, fill with single option so that it gets preserved in GetVideoParams() - colorspace_combobox_->clear(); - colorspace_combobox_->addItem(p.colorspace(), p.colorspace()); - } - - blockSignals(false); -} - -void VideoParamEdit::SetColorManager(ColorManager *cm) -{ - color_manager_ = cm; - - // Re-populate colorspace combobox - colorspace_combobox_->clear(); - - if (color_manager_) { - // Add default colorspace - colorspace_combobox_->addItem(tr("Default (%1)").arg(color_manager_->GetDefaultInputColorSpace()), QString()); - - // Add remaining - QStringList spaces = color_manager_->ListAvailableColorspaces(); - foreach (const QString& s, spaces) { - colorspace_combobox_->addItem(s, s); - } - } -} - -void VideoParamEdit::SetChannelCount(int count) -{ - for (int i=0; icount(); i++) { - if (channel_count_combobox_->itemData(i).toInt() == count) { - channel_count_combobox_->setCurrentIndex(i); - break; - } - } -} - -void VideoParamEdit::SetVideoTypeComboBox(VideoParams::Type type) -{ - for (int i=0; icount(); i++) { - if (video_type_combobox_->itemData(i).toInt() == type) { - video_type_combobox_->setCurrentIndex(i); - break; - } - } -} - -} diff --git a/app/widget/videoparamedit/videoparamedit.h b/app/widget/videoparamedit/videoparamedit.h deleted file mode 100644 index 0aa372869..000000000 --- a/app/widget/videoparamedit/videoparamedit.h +++ /dev/null @@ -1,182 +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 . - -***/ - -#ifndef VIDEOPARAMEDIT_H -#define VIDEOPARAMEDIT_H - -#include -#include -#include - -#include "node/color/colormanager/colormanager.h" -#include "render/videoparams.h" -#include "widget/slider/integerslider.h" -#include "widget/slider/rationalslider.h" -#include "widget/standardcombos/frameratecombobox.h" -#include "widget/standardcombos/interlacedcombobox.h" -#include "widget/standardcombos/pixelaspectratiocombobox.h" -#include "widget/standardcombos/pixelformatcombobox.h" -#include "widget/standardcombos/videodividercombobox.h" - -namespace olive { - -class VideoParamEdit : public QWidget -{ - Q_OBJECT -public: - VideoParamEdit(QWidget* parent = nullptr); - - enum ParamMask { - kNone = 0x0, - kEnabled = 0x1, - kWidthHeight = 0x2, - kDepth = 0x4, - kFrameRate = 0x8, - kFormat = 0x10, - kChannelCount = 0x20, - kPixelAspect = 0x40, - kInterlacing = 0x80, - kDivider = 0x100, - kStreamIndex = 0x200, - kIsImageSequence = 0x400, - kStartTime = 0x800, - kEndTime = 0x1000, - kPremultipliedAlpha = 0x2000, - kColorspace = 0x4000, - kFrameRateIsNotTimebase = 0x8000, - kFrameRateIsArbitrary = 0x10000 - }; - - void SetParameterMask(uint64_t mask); - - VideoParams GetVideoParams() const; - void SetVideoParams(const VideoParams& p); - - /** - * @brief Set pointer to ColorManager - * - * Call this before calling SetVideoParams because it'll populate the colorspace list so it - * can correctly be chosen from in the UI. - */ - void SetColorManager(ColorManager* cm); - - int GetWidth() const - { - return width_slider_->GetValue(); - } - - void SetWidth(int w) - { - width_slider_->SetValue(w); - } - - int GetHeight() const - { - return height_slider_->GetValue(); - } - - void SetHeight(int h) - { - height_slider_->SetValue(h); - } - - rational GetFrameRate() const - { - return frame_rate_combobox_->GetFrameRate(); - } - - void SetFrameRate(const rational& r) - { - frame_rate_combobox_->SetFrameRate(r); - } - - rational GetPixelAspectRatio() const - { - return pixel_aspect_combobox_->GetPixelAspectRatio(); - } - - void SetPixelAspectRatio(const rational& r) - { - pixel_aspect_combobox_->SetPixelAspectRatio(r); - } - - VideoParams::Interlacing GetInterlaceMode() const - { - return interlaced_combobox_->GetInterlaceMode(); - } - - void SetInterlaceMode(VideoParams::Interlacing i) - { - interlaced_combobox_->SetInterlaceMode(i); - } - -signals: - void Changed(); - -private: - void SetChannelCount(int count); - - void SetVideoTypeComboBox(VideoParams::Type type); - - QLabel* enabled_lbl_; - QCheckBox* enabled_box_; - QLabel* width_lbl_; - IntegerSlider* width_slider_; - QLabel* height_lbl_; - IntegerSlider* height_slider_; - QLabel* depth_lbl_; - IntegerSlider* depth_slider_; - QLabel* frame_rate_lbl_; - FrameRateComboBox* frame_rate_combobox_; - RationalSlider* frame_rate_slider_; - QLabel* pixel_aspect_lbl_; - PixelAspectRatioComboBox* pixel_aspect_combobox_; - QLabel* interlaced_lbl_; - InterlacedComboBox* interlaced_combobox_; - QLabel* format_lbl_; - PixelFormatComboBox* format_combobox_; - QLabel* channel_count_lbl_; - QComboBox* channel_count_combobox_; - QLabel* divider_lbl_; - VideoDividerComboBox* divider_combobox_; - QLabel* stream_index_lbl_; - IntegerSlider* stream_index_slider_; - QLabel* video_type_lbl_; - QComboBox* video_type_combobox_; - QLabel* start_time_lbl_; - IntegerSlider* start_time_slider_; - QLabel* end_time_lbl_; - IntegerSlider* end_time_slider_; - QLabel* premultiplied_alpha_lbl_; - QCheckBox* premultiplied_alpha_box_; - QLabel* colorspace_lbl_; - QComboBox* colorspace_combobox_; - - ColorManager* color_manager_; - - rational timebase_temp_; - - uint64_t mask_; - -}; - -} - -#endif // VIDEOPARAMEDIT_H diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 4ab607154..f90ea032a 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -271,7 +271,9 @@ void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event) } else if (gizmo_click_) { // Handle gizmo - gizmos_->GizmoRelease(); + MultiUndoCommand *command = new MultiUndoCommand(); + gizmos_->GizmoRelease(command); + Core::instance()->undo_stack()->pushIfHasChildren(command); gizmo_click_ = false; } else { diff --git a/app/window/mainwindow/mainwindow.cpp b/app/window/mainwindow/mainwindow.cpp index 142058139..dfb4e34bb 100644 --- a/app/window/mainwindow/mainwindow.cpp +++ b/app/window/mainwindow/mainwindow.cpp @@ -78,25 +78,27 @@ MainWindow::MainWindow(QWidget *parent) : setStatusBar(status_bar); // Create standard panels - node_panel_ = PanelManager::instance()->CreatePanel(this); - footage_viewer_panel_ = PanelManager::instance()->CreatePanel(this); - param_panel_ = PanelManager::instance()->CreatePanel(this); - curve_panel_ = PanelManager::instance()->CreatePanel(this); - sequence_viewer_panel_ = PanelManager::instance()->CreatePanel(this); - pixel_sampler_panel_ = PanelManager::instance()->CreatePanel(this); + node_panel_ = new NodePanel(this); + footage_viewer_panel_ = new FootageViewerPanel(this); + param_panel_ = new ParamPanel(this); + curve_panel_ = new CurvePanel(this); + sequence_viewer_panel_ = new SequenceViewerPanel(this); + pixel_sampler_panel_ = new PixelSamplerPanel(this); AppendProjectPanel(); - tool_panel_ = PanelManager::instance()->CreatePanel(this); - task_man_panel_ = PanelManager::instance()->CreatePanel(this); + tool_panel_ = new ToolPanel(this); + task_man_panel_ = new TaskManagerPanel(this); AppendTimelinePanel(); - audio_monitor_panel_ = PanelManager::instance()->CreatePanel(this); + audio_monitor_panel_ = new AudioMonitorPanel(this); // Make node-related connections connect(node_panel_, &NodePanel::NodesSelected, param_panel_, &ParamPanel::SelectNodes); connect(node_panel_, &NodePanel::NodesDeselected, param_panel_, &ParamPanel::DeselectNodes); + connect(node_panel_, &NodePanel::NodeGroupOpenRequested, this, &MainWindow::NodeGroupRequested); connect(param_panel_, &ParamPanel::RequestSelectNode, this, [this](const QVector& target){ node_panel_->Select(target, true); }); connect(param_panel_, &ParamPanel::FocusedNodeChanged, sequence_viewer_panel_, &ViewerPanel::SetGizmos); + connect(param_panel_, &ParamPanel::FocusedNodeChanged, curve_panel_, &CurvePanel::SetNode); // Connect time signals together connect(sequence_viewer_panel_, &SequenceViewerPanel::TimeChanged, param_panel_, &ParamPanel::SetTime); @@ -106,9 +108,6 @@ MainWindow::MainWindow(QWidget *parent) : connect(curve_panel_, &ParamPanel::TimeChanged, sequence_viewer_panel_, &SequenceViewerPanel::SetTime); connect(curve_panel_, &ParamPanel::TimeChanged, param_panel_, &NodeTablePanel::SetTime); - // Connect node order signals - connect(param_panel_, &ParamPanel::NodeOrderChanged, curve_panel_, &CurvePanel::SetNodes); - connect(PanelManager::instance(), &PanelManager::FocusedPanelChanged, this, &MainWindow::FocusedPanelChanged); sequence_viewer_panel_->ConnectTimeBasedPanel(param_panel_); @@ -116,7 +115,7 @@ MainWindow::MainWindow(QWidget *parent) : UpdateTitle(); - QMetaObject::invokeMethod(this, "SetDefaultLayout", Qt::QueuedConnection); + QMetaObject::invokeMethod(this, &MainWindow::SetDefaultLayout, Qt::QueuedConnection); } MainWindow::~MainWindow() @@ -220,12 +219,7 @@ bool MainWindow::IsSequenceOpen(Sequence *sequence) const void MainWindow::FolderOpen(Project* p, Folder *i, bool floating) { - ProjectPanel* panel = PanelManager::instance()->CreatePanel(this); - - // Set custom name to distinguish it from regular ProjectPanels - panel->setObjectName(QStringLiteral("FolderPanel")); - - SetUniquePanelID(panel, folder_panels_); + ProjectPanel* panel = new ProjectPanel(this); panel->set_project(p); panel->set_root(i); @@ -263,7 +257,7 @@ void MainWindow::OpenNodeInViewer(ViewerOutput *node) viewer_panels_.value(node)->raise(); } else { // Create a viewer for this node - ViewerPanel* viewer = PanelManager::instance()->CreatePanel(this); + ViewerPanel* viewer = new ViewerPanel(this); viewer->SetSignalInsteadOfClose(true); viewer->setFloating(true); @@ -378,9 +372,7 @@ void MainWindow::ProjectClose(Project *p) } // Close project from NodeView - if (node_panel_->GetGraph() == p) { - node_panel_->ClearGraph(); - } + node_panel_->CloseContextsBelongingToProject(p); } void MainWindow::SetApplicationProgressStatus(ProgressStatus status) @@ -460,6 +452,17 @@ void MainWindow::StatusBarDoubleClicked() task_man_panel_->raise(); } +void MainWindow::NodeGroupRequested(NodeGroup *group) +{ + NodePanel *panel = new NodePanel(this); + panel->setFloating(true); + panel->setVisible(true); + panel->SetContexts({group}); + panel->SetSignalInsteadOfClose(true); + addDockWidget(Qt::LeftDockWidgetArea, panel); + connect(panel, &NodePanel::CloseRequested, panel, &NodePanel::deleteLater); +} + void MainWindow::TimelinePanelSelectionChanged(const QVector &blocks) { TimelinePanel *panel = static_cast(sender()); @@ -469,15 +472,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()) { @@ -574,7 +568,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; } @@ -731,11 +724,8 @@ void MainWindow::UpdateNodePanelContextFromTimelinePanel(TimelinePanel *panel) context.append(viewer); } - QVector old_contexts = node_panel_->GetCurrentContexts(); - node_panel_->SetGraph(viewer ? viewer->parent() : nullptr, context); - if (viewer && context != old_contexts) { - node_panel_->SelectAll(); - } + node_panel_->SetContexts(context); + param_panel_->SetContexts(context); } void MainWindow::FocusedPanelChanged(PanelWidget *panel) @@ -745,7 +735,17 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) UpdateAudioMonitorParams(tbp->GetConnectedViewer()); } - if (TimelinePanel* timeline = dynamic_cast(panel)) { + if (NodePanel *node_panel = dynamic_cast(panel)) { + // Set param view contexts to these + const QVector &new_ctxs = node_panel->GetContexts(); + + if (new_ctxs != param_panel_->GetContexts()) { + bool is_default_node_panel = node_panel == node_panel_; + param_panel_->SetIgnoreNodeFlags(!is_default_node_panel); + param_panel_->SetCreateCheckBoxes(is_default_node_panel ? kNoCheckBoxes : kCheckBoxesOnNonConnected); + param_panel_->SetContexts(node_panel->GetContexts()); + } + } else if (TimelinePanel* timeline = dynamic_cast(panel)) { // Signal timeline focus TimelineFocused(timeline->GetConnectedViewer()); @@ -753,10 +753,6 @@ void MainWindow::FocusedPanelChanged(PanelWidget *panel) } else if (ProjectPanel* project = dynamic_cast(panel)) { // Signal project panel focus UpdateTitle(); - if (Project *p = project->project()) { - node_panel_->SetGraph(p, {p->root()}); - node_panel_->Select({p->color_manager(), p->settings()}, true); - } } } @@ -770,7 +766,7 @@ void MainWindow::SetDefaultLayout() node_panel_->show(); tabifyDockWidget(param_panel_, node_panel_); - footage_viewer_panel_->raise(); + param_panel_->raise(); curve_panel_->hide(); curve_panel_->setFloating(true); @@ -842,9 +838,7 @@ void MainWindow::showEvent(QShowEvent *e) template T *MainWindow::AppendPanelInternal(QList& list) { - T* panel = PanelManager::instance()->CreatePanel(this); - - SetUniquePanelID(panel, list); + T* panel = new T(this); if (!list.isEmpty()) { tabifyDockWidget(list.last(), panel); @@ -862,19 +856,10 @@ T *MainWindow::AppendPanelInternal(QList& list) return panel; } -template -void MainWindow::SetUniquePanelID(T *panel, const QList &list) -{ - // Set unique object name so it can be identified by QMainWindow's save and restore state functions - panel->setObjectName(panel->objectName().append(QString::number(list.size()))); -} - template T *MainWindow::AppendFloatingPanelInternal(QList &list) { - T* panel = PanelManager::instance()->CreatePanel(this); - - SetUniquePanelID(panel, list); + T* panel = new T(this); panel->setFloating(true); panel->show(); diff --git a/app/window/mainwindow/mainwindow.h b/app/window/mainwindow/mainwindow.h index f8cf36d98..63dc4445f 100644 --- a/app/window/mainwindow/mainwindow.h +++ b/app/window/mainwindow/mainwindow.h @@ -187,14 +187,14 @@ private slots: void StatusBarDoubleClicked(); + void NodeGroupRequested(NodeGroup *group); + #ifdef Q_OS_LINUX void ShowNouveauWarning(); #endif void TimelinePanelSelectionChanged(const QVector &blocks); - void ProjectPanelSelectionChanged(const QVector &nodes); - void ShowWelcomeDialog(); };