From 8e7035bd4070f8b38af68ace815de2b4db6915ae Mon Sep 17 00:00:00 2001 From: itsmattkc Date: Tue, 19 Jan 2021 00:06:57 +1100 Subject: [PATCH] restored nodeview --- app/widget/nodeview/CMakeLists.txt | 7 + app/widget/nodeview/nodeview.cpp | 688 +++++++++++++++++++++++++- app/widget/nodeview/nodeview.h | 118 ++++- app/widget/nodeview/nodeviewcommon.h | 58 +++ app/widget/nodeview/nodeviewedge.cpp | 215 ++++++++ app/widget/nodeview/nodeviewedge.h | 143 ++++++ app/widget/nodeview/nodeviewitem.cpp | 485 ++++++++++++++++++ app/widget/nodeview/nodeviewitem.h | 174 +++++++ app/widget/nodeview/nodeviewscene.cpp | 295 +++++++++++ app/widget/nodeview/nodeviewscene.h | 138 ++++++ 10 files changed, 2307 insertions(+), 14 deletions(-) create mode 100644 app/widget/nodeview/nodeviewcommon.h create mode 100644 app/widget/nodeview/nodeviewedge.cpp create mode 100644 app/widget/nodeview/nodeviewedge.h create mode 100644 app/widget/nodeview/nodeviewitem.cpp create mode 100644 app/widget/nodeview/nodeviewitem.h create mode 100644 app/widget/nodeview/nodeviewscene.cpp create mode 100644 app/widget/nodeview/nodeviewscene.h diff --git a/app/widget/nodeview/CMakeLists.txt b/app/widget/nodeview/CMakeLists.txt index 567cf75b0..e8f0635af 100644 --- a/app/widget/nodeview/CMakeLists.txt +++ b/app/widget/nodeview/CMakeLists.txt @@ -18,6 +18,13 @@ set(OLIVE_SOURCES ${OLIVE_SOURCES} widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h + widget/nodeview/nodeviewcommon.h + widget/nodeview/nodeviewedge.cpp + widget/nodeview/nodeviewedge.h + widget/nodeview/nodeviewitem.cpp + widget/nodeview/nodeviewitem.h + widget/nodeview/nodeviewscene.cpp + widget/nodeview/nodeviewscene.h widget/nodeview/nodeviewundo.cpp widget/nodeview/nodeviewundo.h PARENT_SCOPE diff --git a/app/widget/nodeview/nodeview.cpp b/app/widget/nodeview/nodeview.cpp index 318412fc5..4b7b373bc 100644 --- a/app/widget/nodeview/nodeview.cpp +++ b/app/widget/nodeview/nodeview.cpp @@ -20,14 +20,696 @@ #include "nodeview.h" +#include +#include + +#include "core.h" +#include "nodeviewundo.h" +#include "node/factory.h" +#include "widget/menu/menushared.h" + +#define super HandMovableView + namespace olive { -NodeView::NodeView(QWidget* parent) : +NodeView::NodeView(QWidget *parent) : HandMovableView(parent), - graph_(nullptr) + graph_(nullptr), + drop_edge_(nullptr), + create_edge_(nullptr), + create_edge_dst_(nullptr), + create_edge_dst_input_(nullptr), + create_edge_dst_temp_expanded_(false), + filter_mode_(kFilterShowSelectedBlocks), + scale_(1.0) { + setScene(&scene_); + SetDefaultDragMode(RubberBandDrag); + setContextMenuPolicy(Qt::CustomContextMenu); + setMouseTracking(true); + setRenderHint(QPainter::Antialiasing); + setViewportUpdateMode(FullViewportUpdate); + connect(this, &NodeView::customContextMenuRequested, this, &NodeView::ShowContextMenu); + + ConnectSelectionChangedSignal(); + + SetFlowDirection(NodeViewCommon::kTopToBottom); + + // Set massive scene rect and hide the scrollbars to create an "infinite space" effect + scene_.setSceneRect(-1000000, -1000000, 2000000, 2000000); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); +} + +NodeView::~NodeView() +{ + // Unset the current graph + SetGraph(nullptr); +} + +void NodeView::SetGraph(NodeGraph *graph) +{ + if (graph_ == graph) { + return; + } + + if (graph_) { + disconnect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); + disconnect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); + disconnect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); + disconnect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); + } + + // Clear the scene of all UI objects + scene_.clear(); + + // Set reference to the graph + graph_ = graph; + scene_.SetGraph(graph_); + + // If the graph is valid, add UI objects for each of its Nodes + if (graph_) { + connect(graph_, &NodeGraph::NodeAdded, &scene_, &NodeViewScene::AddNode); + connect(graph_, &NodeGraph::NodeRemoved, &scene_, &NodeViewScene::RemoveNode); + connect(graph_, &NodeGraph::InputConnected, &scene_, &NodeViewScene::AddEdge); + connect(graph_, &NodeGraph::InputDisconnected, &scene_, &NodeViewScene::RemoveEdge); + + foreach (Node* n, graph_->nodes()) { + scene_.AddNode(n); + } + + foreach (Node* n, graph_->nodes()) { + foreach (NodeInput* input, n->parameters()) { + for (auto it=input->edges().cbegin(); it!=input->edges().cend(); it++) { + scene_.AddEdge(it->second, input, it->first); + } + } + } + } +} + +void NodeView::DeleteSelected() +{ + if (!graph_) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + { + QVector selected_edges = scene_.GetSelectedEdges(); + + foreach (NodeViewEdge* edge, selected_edges) { + new NodeEdgeRemoveCommand(edge->output(), edge->input(), edge->element(), command); + } + } + + { + QVector selected_nodes = scene_.GetSelectedNodes(); + + // Ensure no nodes are "undeletable" + for (int i=0;iCanBeDeleted()) { + selected_nodes.removeAt(i); + i--; + } + } + + if (!selected_nodes.isEmpty()) { + foreach (Node* node, selected_nodes) { + Node::RemoveNodeAndDisconnect(node, command); + } + } + } + + Core::instance()->undo_stack()->pushIfHasChildren(command); +} + +void NodeView::SelectAll() +{ + // Optimization: rather than respond to every single item being selected, ignore the signal and + // then handle them all at the end. + DisconnectSelectionChangedSignal(); + + scene_.SelectAll(); + + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); +} + +void NodeView::DeselectAll() +{ + // Optimization: rather than respond to every single item being selected, ignore the signal and + // then handle them all at the end. + DisconnectSelectionChangedSignal(); + + scene_.DeselectAll(); + + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); +} + +void NodeView::Select(const QVector &nodes) +{ + 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(); + + scene_.DeselectAll(); + + foreach (Node* n, nodes) { + NodeViewItem* item = scene_.NodeToUIObject(n); + + item->setSelected(true); + } + + ConnectSelectionChangedSignal(); + SceneSelectionChangedSlot(); +} + +void NodeView::SelectWithDependencies(QVector nodes) +{ + if (!graph_) { + return; + } + + int original_length = nodes.size(); + for (int i=0;iGetDependencies()); + } + + Select(nodes); +} + +void NodeView::CopySelected(bool cut) +{ + if (!graph_) { + return; + } + + QVector selected = scene_.GetSelectedNodes(); + + if (selected.isEmpty()) { + return; + } + + CopyNodesToClipboard(selected); + + if (cut) { + DeleteSelected(); + } +} + +void NodeView::Paste() +{ + if (!graph_) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + QVector pasted_nodes = PasteNodesFromClipboard(static_cast(graph_), command); + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + if (!pasted_nodes.isEmpty()) { + AttachNodesToCursor(pasted_nodes); + } +} + +void NodeView::Duplicate() +{ + if (!graph_) { + return; + } + + QVector selected = scene_.GetSelectedNodes(); + + if (selected.isEmpty()) { + return; + } + + QUndoCommand* command = new QUndoCommand(); + + QVector duplicated_nodes = Node::CopyDependencyGraph(selected, command); + + Core::instance()->undo_stack()->pushIfHasChildren(command); + + AttachNodesToCursor(duplicated_nodes); +} + +void NodeView::keyPressEvent(QKeyEvent *event) +{ + super::keyPressEvent(event); + + if (event->key() == Qt::Key_Escape && !attached_items_.isEmpty()) { + DetachItemsFromCursor(); + + // We undo the last action which SHOULD be adding the node + // FIXME: Possible danger of this not being the case? + Core::instance()->undo_stack()->undo(); + } +} + +void NodeView::mousePressEvent(QMouseEvent *event) +{ + if (HandPress(event)) return; + + if (event->button() == Qt::RightButton) { + // Qt doesn't do this by default for some reason + if (!(event->modifiers() & Qt::ShiftModifier)) { + scene_.clearSelection(); + } + + // If there's an item here, select it + QGraphicsItem* item = itemAt(event->pos()); + if (item) { + item->setSelected(true); + } + } + + if (event->modifiers() & Qt::ControlModifier) { + NodeViewItem* item = dynamic_cast(itemAt(event->pos())); + + if (item) { + create_edge_ = new NodeViewEdge(); + create_edge_src_ = item; + scene_.addItem(create_edge_); + return; + } + } + + super::mousePressEvent(event); +} + +void NodeView::mouseMoveEvent(QMouseEvent *event) +{ + if (HandMove(event)) return; + + if (create_edge_) { + // Determine scene coordinate + QPointF scene_pt = mapToScene(event->pos()); + + // Find if the cursor is currently inside an item + NodeViewItem* item_at_cursor = dynamic_cast(itemAt(event->pos())); + + // If the item has changed + if (item_at_cursor != create_edge_dst_) { + // 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); + } + } + + // Set destination + create_edge_dst_ = item_at_cursor; + + // 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); + } + } + } + + // 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); + } + + 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(highlight_index, create_edge_src_->pos()), + true); + } else { + create_edge_dst_input_ = nullptr; + 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_); + return; + } + + super::mouseMoveEvent(event); + + // See if there are any items attached + if (!attached_items_.isEmpty()) { + // Move those items to the cursor + MoveAttachedNodesToCursor(event->pos()); + + // See if the user clicked on an edge (only when dropping single nodes) + if (attached_items_.size() == 1) { + Node* attached_node = attached_items_.first().item->GetNode(); + + QRect edge_detect_rect(event->pos(), event->pos()); + + int edge_detect_radius = fontMetrics().height(); + edge_detect_rect.adjust(-edge_detect_radius, -edge_detect_radius, edge_detect_radius, edge_detect_radius); + + QList items = this->items(edge_detect_rect); + + NodeViewEdge* new_drop_edge = nullptr; + + // See if there is an edge here + foreach (QGraphicsItem* item, items) { + new_drop_edge = dynamic_cast(item); + + if (new_drop_edge) { + drop_input_ = nullptr; + + foreach (NodeInput* input, attached_node->parameters()) { + if (input->IsConnectable()) { + if (input->GetDataType() == new_drop_edge->input()->GetDataType()) { + drop_input_ = input; + break; + } else if (!drop_input_) { + drop_input_ = input; + } + } + } + + if (drop_input_) { + break; + } else { + new_drop_edge = nullptr; + } + } + } + + if (drop_edge_ != new_drop_edge) { + if (drop_edge_) { + drop_edge_->SetHighlighted(false); + } + + drop_edge_ = new_drop_edge; + + if (drop_edge_) { + drop_edge_->SetHighlighted(true); + } + } + } + } +} + +void NodeView::mouseReleaseEvent(QMouseEvent *event) +{ + if (HandRelease(event)) return; + + if (create_edge_) { + delete create_edge_; + create_edge_ = nullptr; + + if (create_edge_dst_) { + // Clear highlight + create_edge_dst_->SetHighlightedIndex(-1); + + // Collapse if we expanded it + if (create_edge_dst_temp_expanded_) { + create_edge_dst_->SetExpanded(false); + } + + if (create_edge_dst_input_) { + // Make connection + Core::instance()->undo_stack()->push(new NodeEdgeAddCommand(create_edge_src_->GetNode(), create_edge_dst_input_, -1)); + create_edge_dst_input_ = nullptr; + } + + create_edge_dst_ = nullptr; + } + return; + } + + if (!attached_items_.isEmpty()) { + if (attached_items_.size() == 1) { + Node* dropping_node = attached_items_.first().item->GetNode(); + + if (drop_edge_) { + // We have everything we need to place the node in between + QUndoCommand* command = new QUndoCommand(); + + // Remove old edge + new NodeEdgeRemoveCommand(drop_edge_->output(), drop_edge_->input(), drop_edge_->element(), command); + + // Place new edges + new NodeEdgeAddCommand(drop_edge_->output(), drop_input_, -1, command); + new NodeEdgeAddCommand(dropping_node, drop_edge_->input(), drop_edge_->element(), command); + + Core::instance()->undo_stack()->push(command); + } + + drop_edge_ = nullptr; + } + + DetachItemsFromCursor(); + } + + super::mouseReleaseEvent(event); +} + +void NodeView::wheelEvent(QWheelEvent *event) +{ + if (event->modifiers() & Qt::ControlModifier) { + // FIXME: Hardcoded divider (0.001) + qreal multiplier = 1.0 + (static_cast(event->angleDelta().x() + event->angleDelta().y()) * 0.001); + + double test_scale = scale_ * multiplier; + + if (test_scale > 0.1) { + scale(multiplier, multiplier); + scale_ = test_scale; + } + } else { + QWidget::wheelEvent(event); + } +} + +void NodeView::SceneSelectionChangedSlot() +{ + QVector current_selection = scene_.GetSelectedNodes(); + + 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 { + foreach (Node* n, current_selection) { + if (!selected_nodes_.contains(n)) { + selected.append(n); + } + } + } + + // Determine which nodes are newly deselected + if (current_selection.isEmpty()) { + // All nodes that were selected have been deselected + deselected = selected_nodes_; + } else { + foreach (Node* n, selected_nodes_) { + if (!current_selection.contains(n)) { + deselected.append(n); + } + } + } + + selected_nodes_ = current_selection; + + if (!selected.isEmpty()) { + emit NodesSelected(selected); + } + + if (!deselected.isEmpty()) { + emit NodesDeselected(deselected); + } +} + +void NodeView::ShowContextMenu(const QPoint &pos) +{ + if (!graph_) { + return; + } + + Menu m; + + MenuShared::instance()->AddItemsForEditMenu(&m, false); + + m.addSeparator(); + + QVector selected = scene_.GetSelectedItems(); + + if (itemAt(pos) && !selected.isEmpty()) { + + // Label node action + QAction* label_action = m.addAction(tr("Label")); + connect(label_action, &QAction::triggered, this, [this](){ + Core::instance()->LabelNodes(scene_.GetSelectedNodes()); + }); + + m.addSeparator(); + + // Auto-position action + QAction* autopos = m.addAction(tr("Auto-Position")); + connect(autopos, &QAction::triggered, this, &NodeView::AutoPositionDescendents); + + } else { + + QAction* curved_action = m.addAction(tr("Smooth Edges")); + curved_action->setCheckable(true); + curved_action->setChecked(scene_.GetEdgesAreCurved()); + connect(curved_action, &QAction::triggered, &scene_, &NodeViewScene::SetEdgesAreCurved); + + m.addSeparator(); + + Menu* filter_menu = new Menu(tr("Filter"), &m); + m.addMenu(filter_menu); + + filter_menu->AddActionWithData(tr("Show All"), + kFilterShowAll, + filter_mode_); + + filter_menu->AddActionWithData(tr("Show Selected Blocks Only"), + kFilterShowSelectedBlocks, + filter_mode_); + + connect(filter_menu, &Menu::triggered, this, &NodeView::ContextMenuFilterChanged); + + + + Menu* direction_menu = new Menu(tr("Direction"), &m); + m.addMenu(direction_menu); + + direction_menu->AddActionWithData(tr("Top to Bottom"), + NodeViewCommon::kTopToBottom, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Bottom to Top"), + NodeViewCommon::kBottomToTop, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Left to Right"), + NodeViewCommon::kLeftToRight, + scene_.GetFlowDirection()); + + direction_menu->AddActionWithData(tr("Right to Left"), + NodeViewCommon::kRightToLeft, + scene_.GetFlowDirection()); + + connect(direction_menu, &Menu::triggered, this, &NodeView::ContextMenuSetDirection); + + m.addSeparator(); + + Menu* add_menu = NodeFactory::CreateMenu(&m); + add_menu->setTitle(tr("Add")); + connect(add_menu, &Menu::triggered, this, &NodeView::CreateNodeSlot); + m.addMenu(add_menu); + + } + + m.exec(mapToGlobal(pos)); +} + +void NodeView::CreateNodeSlot(QAction *action) +{ + Node* new_node = NodeFactory::CreateFromMenuAction(action); + + if (new_node) { + Core::instance()->undo_stack()->push(new NodeAddCommand(graph_, new_node)); + + NodeViewItem* item = scene_.NodeToUIObject(new_node); + AttachItemsToCursor({item}); + } +} + +void NodeView::ContextMenuSetDirection(QAction *action) +{ + SetFlowDirection(static_cast(action->data().toInt())); +} + +void NodeView::AutoPositionDescendents() +{ + QVector selected = scene_.GetSelectedNodes(); + + foreach (Node* n, selected) { + scene_.ReorganizeFrom(n); + } +} + +void NodeView::ContextMenuFilterChanged(QAction *action) +{ + Q_UNUSED(action) +} + +void NodeView::AttachNodesToCursor(const QVector &nodes) +{ + QVector items(nodes.size()); + + for (int i=0; i& items) +{ + DetachItemsFromCursor(); + + if (!items.isEmpty()) { + foreach (NodeViewItem* i, items) { + attached_items_.append({i, i->pos() - items.first()->pos()}); + } + + setMouseTracking(true); + + MoveAttachedNodesToCursor(mapFromGlobal(QCursor::pos())); + } +} + +void NodeView::DetachItemsFromCursor() +{ + attached_items_.clear(); + setMouseTracking(false); +} + +void NodeView::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + scene_.SetFlowDirection(dir); +} + +void NodeView::MoveAttachedNodesToCursor(const QPoint& p) +{ + QPointF item_pos = mapToScene(p); + + foreach (const AttachedItem& i, attached_items_) { + i.item->setPos(item_pos + i.original_pos); + } +} + +void NodeView::ConnectSelectionChangedSignal() +{ + connect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); +} + +void NodeView::DisconnectSelectionChangedSignal() +{ + disconnect(&scene_, &QGraphicsScene::selectionChanged, this, &NodeView::SceneSelectionChangedSlot); } } - diff --git a/app/widget/nodeview/nodeview.h b/app/widget/nodeview/nodeview.h index 3b1dd85e4..eadad8de4 100644 --- a/app/widget/nodeview/nodeview.h +++ b/app/widget/nodeview/nodeview.h @@ -22,47 +22,143 @@ #define NODEVIEW_H #include +#include #include "node/graph.h" #include "node/nodecopypaste.h" +#include "nodeviewedge.h" +#include "nodeviewscene.h" #include "widget/handmovableview/handmovableview.h" namespace olive { +/** + * @brief A widget for viewing and editing node graphs + * + * This widget takes a NodeGraph object and constructs a QGraphicsScene representing its data, viewing and allowing + * the user to make modifications to it. + */ class NodeView : public HandMovableView, public NodeCopyPasteService { Q_OBJECT public: NodeView(QWidget* parent); + virtual ~NodeView() override; + /** * @brief Sets the graph to view */ - void SetGraph(NodeGraph* graph){} + void SetGraph(NodeGraph* graph); /** * @brief Delete selected nodes from graph (user-friendly/undoable) */ - void DeleteSelected(){} + void DeleteSelected(); - void SelectAll(){} - void DeselectAll(){} + void SelectAll(); + void DeselectAll(); - void Select(const QVector& nodes){} - void SelectWithDependencies(QVector nodes){} + void Select(const QVector& nodes); + void SelectWithDependencies(QVector nodes); - void CopySelected(bool cut){} - void Paste(){} + void CopySelected(bool cut); + void Paste(); - void Duplicate(){} + void Duplicate(); - void SelectBlocks(const QVector& blocks){} +signals: + void NodesSelected(const QVector& nodes); - void DeselectBlocks(const QVector& blocks){} + void NodesDeselected(const QVector& nodes); + +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 wheelEvent(QWheelEvent* event) override; private: + void AttachNodesToCursor(const QVector &nodes); + + void AttachItemsToCursor(const QVector &items); + + void DetachItemsFromCursor(); + + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + void MoveAttachedNodesToCursor(const QPoint &p); + + void ConnectSelectionChangedSignal(); + void DisconnectSelectionChangedSignal(); + NodeGraph* graph_; + struct AttachedItem { + NodeViewItem* item; + QPointF original_pos; + }; + + QList 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_; + + NodeViewScene scene_; + + QVector selected_nodes_; + + QVector selected_blocks_; + + enum FilterMode { + kFilterShowAll, + kFilterShowSelectedBlocks + }; + + FilterMode filter_mode_; + + double scale_; + +private slots: + /** + * @brief Receiver for when the scene's selected items change + */ + void SceneSelectionChangedSlot(); + + /** + * @brief Receiver for when the user right clicks (or otherwise requests a context menu) + */ + void ShowContextMenu(const QPoint &pos); + + /** + * @brief Receiver for when the user requests a new node from the add menu + */ + void CreateNodeSlot(QAction* action); + + /** + * @brief Receiver for setting the direction from the context menu + */ + void ContextMenuSetDirection(QAction* action); + + /** + * @brief Receiver for auto-position descendents menu action + */ + void AutoPositionDescendents(); + + /** + * @brief Receiver for the user changing the filter + */ + void ContextMenuFilterChanged(QAction* action); + }; } diff --git a/app/widget/nodeview/nodeviewcommon.h b/app/widget/nodeview/nodeviewcommon.h new file mode 100644 index 000000000..648229b28 --- /dev/null +++ b/app/widget/nodeview/nodeviewcommon.h @@ -0,0 +1,58 @@ +/*** + + 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 NODEVIEWCOMMON_H +#define NODEVIEWCOMMON_H + +#include + +#include "common/define.h" + +namespace olive { + +class NodeViewCommon { +public: + enum FlowDirection { + kTopToBottom, + kBottomToTop, + kLeftToRight, + kRightToLeft + }; + + static Qt::Orientation GetFlowOrientation(FlowDirection dir) { + if (dir == kTopToBottom || dir == kBottomToTop) { + return Qt::Vertical; + } else { + return Qt::Horizontal; + } + } + + static bool DirectionsAreOpposing(FlowDirection a, FlowDirection b) { + return ((a == NodeViewCommon::kLeftToRight && b == NodeViewCommon::kRightToLeft) + || (a == NodeViewCommon::kRightToLeft && b == NodeViewCommon::kLeftToRight) + || (a == NodeViewCommon::kTopToBottom && b == NodeViewCommon::kBottomToTop) + || (a == NodeViewCommon::kBottomToTop && b == NodeViewCommon::kTopToBottom)); + } + +}; + +} + +#endif // NODEVIEWCOMMON_H diff --git a/app/widget/nodeview/nodeviewedge.cpp b/app/widget/nodeview/nodeviewedge.cpp new file mode 100644 index 000000000..a52cb762b --- /dev/null +++ b/app/widget/nodeview/nodeviewedge.cpp @@ -0,0 +1,215 @@ +/*** + + 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 "nodeviewedge.h" + +#include +#include +#include +#include + +#include "common/bezier.h" +#include "common/lerp.h" +#include "nodeview.h" +#include "nodeviewitem.h" +#include "nodeviewscene.h" + +namespace olive { + +NodeViewEdge::NodeViewEdge(Node* output, NodeInput *input, int element, + NodeViewItem* from_item, NodeViewItem* to_item, + QGraphicsItem* parent) : + QGraphicsPathItem(parent), + output_(output), + input_(input), + element_(element), + from_item_(from_item), + to_item_(to_item) +{ + Init(); + SetConnected(true); +} + +NodeViewEdge::NodeViewEdge(QGraphicsItem *parent) : + QGraphicsPathItem(parent) +{ + Init(); +} + +void NodeViewEdge::Adjust() +{ + // Draw a line between the two + SetPoints(from_item()->GetOutputPoint(), + to_item()->GetInputPoint(input_, from_item()->pos()), + to_item()->IsExpanded()); +} + +void NodeViewEdge::SetConnected(bool c) +{ + connected_ = c; + + update(); +} + +void NodeViewEdge::SetHighlighted(bool e) +{ + highlighted_ = e; + + update(); +} + +void NodeViewEdge::SetPoints(const QPointF &start, const QPointF &end, bool input_is_expanded) +{ + QPainterPath path; + path.moveTo(start); + + double angle = qAtan2(end.y() - start.y(), end.x() - start.x()); + + if (curved_) { + + double half_x = lerp(start.x(), end.x(), 0.5); + double half_y = lerp(start.y(), end.y(), 0.5); + + QPointF cp1, cp2; + + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == 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) { + cp2 = QPointF(half_x, end.y()); + } else { + cp2 = QPointF(end.x(), half_y); + } + + path.cubicTo(cp1, cp2, end); + + if (!qFuzzyCompare(start.x(), end.x())) { + double continue_x = end.x() - qCos(angle)*arrow_size_; + + double x1, x2, x3, x4, y1, y2, y3, y4; + if (start.x() < end.x()) { + x1 = start.x(); + x2 = cp1.x(); + x3 = cp2.x(); + x4 = end.x(); + y1 = start.y(); + y2 = cp1.y(); + y3 = cp2.y(); + y4 = end.y(); + } else { + x1 = end.x(); + x2 = cp2.x(); + x3 = cp1.x(); + x4 = start.x(); + y1 = end.y(); + y2 = cp2.y(); + y3 = cp1.y(); + y4 = start.y(); + } + + double t = Bezier::CubicXtoT(continue_x, x1, x2, x3, x4); + double y = Bezier::CubicTtoY(y1, y2, y3, y4, t); + + angle = qAtan2(end.y() - y, end.x() - continue_x); + } + + } else { + + path.lineTo(end); + + } + + setPath(path); + + const double arrow_angle = 150.0 * 3.141592 / 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); +} + +void NodeViewEdge::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + flow_dir_ = dir; + + Adjust(); +} + +void NodeViewEdge::SetCurved(bool e) +{ + curved_ = e; + + update(); +} + +void NodeViewEdge::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) +{ + QPalette::ColorGroup group; + QPalette::ColorRole role; + + if (connected_) { + group = QPalette::Active; + } else { + group = QPalette::Disabled; + } + + if (highlighted_ != bool(option->state & QStyle::State_Selected)) { + role = QPalette::Highlight; + } else { + role = QPalette::Text; + } + + // Draw main path + QColor edge_color = qApp->palette().color(group, role); + + 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); + + // Ensures this UI object is drawn behind other objects + setZValue(-1); + + // Use font metrics to set edge width for basic high DPI support + edge_width_ = QFontMetrics(QFont()).height() / 12; + arrow_size_ = QFontMetrics(QFont()).height() / 2; +} + +} diff --git a/app/widget/nodeview/nodeviewedge.h b/app/widget/nodeview/nodeviewedge.h new file mode 100644 index 000000000..43dc1fdc6 --- /dev/null +++ b/app/widget/nodeview/nodeviewedge.h @@ -0,0 +1,143 @@ +/*** + + 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 NODEEDGEITEM_H +#define NODEEDGEITEM_H + +#include +#include + +#include "nodeviewcommon.h" +#include "node/node.h" + +namespace olive { + +class NodeViewItem; + +/** + * @brief A graphical representation of a NodeEdge to be used in NodeView + * + * A fairly simple line widget use to visualize a connection between two node parameters (a NodeEdge). + */ +class NodeViewEdge : public QGraphicsPathItem +{ +public: + NodeViewEdge(Node* output, NodeInput *input, int element, + NodeViewItem* from_item, NodeViewItem* to_item, + QGraphicsItem* parent = nullptr); + + NodeViewEdge(QGraphicsItem* parent = nullptr); + + Node* output() const + { + return output_; + } + + NodeInput* input() const + { + return input_; + } + + int element() const + { + return element_; + } + + NodeViewItem* from_item() const + { + return from_item_; + } + + NodeViewItem* to_item() const + { + return to_item_; + } + + void Adjust(); + + /** + * @brief Set the connected state of this line + * + * When the edge is not connected, it visually depicts this by coloring the line grey. When an edge is connected or + * a potential connection is valid, the line is colored white. This function sets whether the line should be grey + * (false) or white (true). + * + * Using SetEdge() automatically sets this to true. Under most circumstances this should be left alone, and only + * be set when an edge is being created/dragged. + */ + void SetConnected(bool c); + + /** + * @brief Set highlighted state + * + * Changes color of edge. + */ + void SetHighlighted(bool e); + + /** + * @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); + + /** + * @brief Set whether edges should be drawn as curved or as straight lines + */ + void SetCurved(bool e); + +protected: + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; + +private: + void Init(); + + Node* output_; + + NodeInput* input_; + + int element_; + + NodeViewItem* from_item_; + + NodeViewItem* to_item_; + + int edge_width_; + + bool connected_; + + bool highlighted_; + + NodeViewCommon::FlowDirection flow_dir_; + + bool curved_; + + QPolygonF arrow_; + + int arrow_size_; + +}; + +} + +#endif // NODEEDGEITEM_H diff --git a/app/widget/nodeview/nodeviewitem.cpp b/app/widget/nodeview/nodeviewitem.cpp new file mode 100644 index 000000000..cf6b8617e --- /dev/null +++ b/app/widget/nodeview/nodeviewitem.cpp @@ -0,0 +1,485 @@ +/*** + + 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 "nodeviewitem.h" + +#include +#include +#include +#include +#include + +#include "common/flipmodifiers.h" +#include "common/qtutils.h" +#include "config/config.h" +#include "core.h" +#include "nodeview.h" +#include "nodeviewscene.h" +#include "nodeviewundo.h" +#include "ui/colorcoding.h" +#include "ui/icons/icons.h" +#include "window/mainwindow/mainwindow.h" + +namespace olive { + +NodeViewItem::NodeViewItem(QGraphicsItem *parent) : + QGraphicsRectItem(parent), + node_(nullptr), + expanded_(false), + hide_titlebar_(false), + highlighted_index_(-1), + flow_dir_(NodeViewCommon::kLeftToRight) +{ + // Set flags for this widget + setFlag(QGraphicsItem::ItemIsMovable); + setFlag(QGraphicsItem::ItemIsSelectable); + setFlag(QGraphicsItem::ItemSendsGeometryChanges); + + // + // We use font metrics to set all the UI measurements for DPI-awareness + // + + // Set border width + node_border_width_ = DefaultItemBorder(); + + int widget_width = DefaultItemWidth(); + int widget_height = DefaultItemHeight(); + + title_bar_rect_ = QRectF(-widget_width/2, -widget_height/2, widget_width, widget_height); + setRect(title_bar_rect_); +} + +QPointF NodeViewItem::GetNodePosition() const +{ + QPointF node_pos; + + qreal adjusted_x = pos().x() / DefaultItemHorizontalPadding(); + qreal adjusted_y = pos().y() / DefaultItemVerticalPadding(); + + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + node_pos.setX(adjusted_x); + node_pos.setY(adjusted_y); + break; + case NodeViewCommon::kRightToLeft: + node_pos.setX(-adjusted_x); + node_pos.setY(adjusted_y); + break; + case NodeViewCommon::kTopToBottom: + node_pos.setX(adjusted_y); + node_pos.setY(adjusted_x); + break; + case NodeViewCommon::kBottomToTop: + node_pos.setX(-adjusted_y); + node_pos.setY(adjusted_x); + break; + } + + return node_pos; +} + +void NodeViewItem::SetNodePosition(const QPointF &pos) +{ + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + setPos(pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kRightToLeft: + setPos(-pos.x() * DefaultItemHorizontalPadding(), + pos.y() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kTopToBottom: + setPos(pos.y() * DefaultItemHorizontalPadding(), + pos.x() * DefaultItemVerticalPadding()); + break; + case NodeViewCommon::kBottomToTop: + setPos(pos.y() * DefaultItemHorizontalPadding(), + -pos.x() * DefaultItemVerticalPadding()); + break; + } +} + +int NodeViewItem::DefaultTextPadding() +{ + return QFontMetrics(QFont()).height() / 4; +} + +int NodeViewItem::DefaultItemHeight() +{ + return QFontMetrics(QFont()).height() + DefaultTextPadding() * 2; +} + +int NodeViewItem::DefaultItemWidth() +{ + return QtUtils::QFontMetricsWidth(QFontMetrics(QFont()), "HHHHHHHHHHHH");; +} + +int NodeViewItem::DefaultItemBorder() +{ + return QFontMetrics(QFont()).height() / 12; +} + +qreal NodeViewItem::DefaultItemHorizontalPadding() const +{ + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + return DefaultItemWidth() * 1.5; + } else { + return DefaultItemWidth() * 1.25; + } +} + +qreal NodeViewItem::DefaultItemVerticalPadding() const +{ + if (NodeViewCommon::GetFlowOrientation(flow_dir_) == Qt::Horizontal) { + return DefaultItemHeight() * 1.5; + } else { + return DefaultItemHeight() * 2.0; + } +} + +void NodeViewItem::AddEdge(NodeViewEdge *edge) +{ + edges_.append(edge); +} + +void NodeViewItem::RemoveEdge(NodeViewEdge *edge) +{ + edges_.removeOne(edge); +} + +int NodeViewItem::GetIndexAt(QPointF pt) const +{ + pt -= pos(); + + for (int i=0; iRetranslate(); + + foreach (NodeInput* input, node_->parameters()) { + if (input->IsConnectable()) { + node_inputs_.append(input); + } + } + + SetNodePosition(node_->GetPosition()); + } + + update(); +} + +void NodeViewItem::SetExpanded(bool e, bool hide_titlebar) +{ + if (node_inputs_.isEmpty() + || (expanded_ == e && hide_titlebar_ == hide_titlebar)) { + return; + } + + expanded_ = e; + hide_titlebar_ = hide_titlebar; + + if (expanded_ && !node_inputs_.isEmpty()) { + // Create new rect + QRectF new_rect = title_bar_rect_; + + if (hide_titlebar_) { + new_rect.setHeight(new_rect.height() * node_inputs_.size()); + } else { + new_rect.setHeight(new_rect.height() * (node_inputs_.size() + 1)); + } + + setRect(new_rect); + } else { + setRect(title_bar_rect_); + } + + update(); + + ReadjustAllEdges(); +} + +void NodeViewItem::ToggleExpanded() +{ + SetExpanded(!IsExpanded()); +} + +void NodeViewItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) +{ + // HACK for getting the main QWidget palette color (the `widget`'s palette uses the NodeView color instead which we + // don't want here) + QPalette app_pal = Core::instance()->main_window()->palette(); + + // Draw background rect if expanded + if (IsExpanded()) { + painter->setPen(Qt::NoPen); + painter->setBrush(app_pal.color(QPalette::Window)); + + painter->drawRect(rect()); + + painter->setPen(app_pal.color(QPalette::Text)); + + for (int i=0;ifillRect(input_rect, highlight_col); + } + + painter->drawText(input_rect, Qt::AlignCenter, node_inputs_.at(i)->name()); + } + } + + // 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; + + if (!node_->GetLabel().isEmpty()) { + // Use label directly if node has one + node_label = node_->GetLabel(); + } else { + Track* track = dynamic_cast(node_); + + if (track) { + // Exception for tracks + node_label = Track::GetDefaultTrackName(track->type(), track->Index()); + } else { + // Otherwise, just use the node's short name + node_label = node_->ShortName(); + } + } + + QFont f; + QFontMetrics fm(f); + + // Draw right or down arrow based on expanded state + int icon_size = fm.height() / 2; + int icon_padding = title_bar_rect_.height() / 2 - icon_size / 2; + int icon_full_size = icon_size + icon_padding * 2; + const QIcon& expand_icon = IsExpanded() ? icon::TriDown : icon::TriRight; + expand_icon.paint(painter, QRect(title_bar_rect_.x() + icon_padding, + title_bar_rect_.y() + icon_padding, + icon_size, + icon_size)); + + // Calculate how much space we have for text + int item_width = title_bar_rect_.width(); + int max_text_width = item_width - DefaultTextPadding() * 2 - icon_full_size; + int label_width = QtUtils::QFontMetricsWidth(fm, node_label); + + // Concatenate text if necessary (adds a "..." to the end and removes characters until the + // string fits in the bounds) + if (label_width > max_text_width) { + QString concatenated; + + do { + node_label.chop(1); + concatenated = QCoreApplication::translate("NodeViewItem", "%1...").arg(node_label); + } while ((label_width = QtUtils::QFontMetricsWidth(fm, concatenated)) > max_text_width); + + node_label = concatenated; + } + + // Determine the text color (automatically calculate from node background color) + painter->setPen(ColorCoding::GetUISelectorColor(node_->color())); + + // Determine X position (favors horizontal centering unless it'll overrun the arrow) + QRectF text_rect = title_bar_rect_; + Qt::Alignment text_align = Qt::AlignCenter; + int likely_x = item_width / 2 - label_width / 2; + if (likely_x < icon_full_size) { + text_rect.adjust(icon_full_size, 0, 0, 0); + text_align = Qt::AlignLeft | Qt::AlignVCenter; + } + + // Draw the text in a rect (the rect is sized around text already in the constructor) + painter->drawText(text_rect, + text_align, + node_label); + } + + // Draw final border + QPen border_pen; + border_pen.setWidth(node_border_width_); + + if (option->state & QStyle::State_Selected) { + border_pen.setColor(app_pal.color(QPalette::Highlight)); + } else { + border_pen.setColor(Qt::black); + } + + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); + + painter->drawRect(rect()); +} + +void NodeViewItem::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + + QGraphicsRectItem::mousePressEvent(event); +} + +void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) +{ + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + + QGraphicsRectItem::mouseMoveEvent(event); +} + +void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + event->setModifiers(FlipControlAndShiftModifiers(event->modifiers())); + + QGraphicsRectItem::mouseReleaseEvent(event); +} + +void NodeViewItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) +{ + QGraphicsRectItem::mouseDoubleClickEvent(event); + + if (!(event->modifiers() & Qt::ControlModifier)) { + SetExpanded(!IsExpanded()); + } +} + +QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionHasChanged && node_) { + node_->blockSignals(true); + node_->SetPosition(GetNodePosition()); + node_->blockSignals(false); + + ReadjustAllEdges(); + } + + return QGraphicsItem::itemChange(change, value); +} + +void NodeViewItem::ReadjustAllEdges() +{ + foreach (NodeViewEdge* edge, edges_) { + edge->Adjust(); + } +} + +void NodeViewItem::SetHighlightedIndex(int index) +{ + if (highlighted_index_ == index) { + return; + } + + highlighted_index_ = index; + + update(); +} + +QRectF NodeViewItem::GetInputRect(int index) const +{ + QRectF r = title_bar_rect_; + + if (!hide_titlebar_) { + index++; + } + + if (IsExpanded()) { + r.translate(0, r.height() * index); + } + + return r; +} + +QPointF NodeViewItem::GetInputPoint(NodeInput *input, const QPointF& source_pos) const +{ + return GetInputPoint(node_inputs_.indexOf(input), source_pos); +} + +QPointF NodeViewItem::GetInputPoint(int input, const QPointF &source_pos) const +{ + return pos() + GetInputPointInternal(input, source_pos); +} + +QPointF NodeViewItem::GetOutputPoint() const +{ + switch (flow_dir_) { + case NodeViewCommon::kLeftToRight: + default: + return pos() + QPointF(rect().right(), rect().center().y()); + case NodeViewCommon::kRightToLeft: + return pos() + QPointF(rect().left(), rect().center().y()); + case NodeViewCommon::kTopToBottom: + return pos() + QPointF(rect().center().x(), rect().bottom()); + case NodeViewCommon::kBottomToTop: + return pos() + QPointF(rect().center().x(), rect().top()); + } +} + +void NodeViewItem::SetFlowDirection(NodeViewCommon::FlowDirection dir) +{ + flow_dir_ = dir; +} + +QPointF NodeViewItem::GetInputPointInternal(int index, const QPointF& source_pos) const +{ + QRectF input_rect = GetInputRect(index); + + Qt::Orientation flow_orientation = NodeViewCommon::GetFlowOrientation(flow_dir_); + + if (flow_orientation == Qt::Horizontal || IsExpanded()) { + if (flow_dir_ == NodeViewCommon::kLeftToRight + || (flow_orientation == Qt::Vertical && source_pos.x() < pos().x())) { + return QPointF(input_rect.left(), input_rect.center().y()); + } else { + return QPointF(input_rect.right(), input_rect.center().y()); + } + } else { + if (flow_dir_ == NodeViewCommon::kTopToBottom) { + return QPointF(input_rect.center().x(), input_rect.top()); + } else { + return QPointF(input_rect.center().x(), input_rect.bottom()); + } + } +} + +} diff --git a/app/widget/nodeview/nodeviewitem.h b/app/widget/nodeview/nodeviewitem.h new file mode 100644 index 000000000..9985dbceb --- /dev/null +++ b/app/widget/nodeview/nodeviewitem.h @@ -0,0 +1,174 @@ +/*** + + 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 NODEVIEWITEM_H +#define NODEVIEWITEM_H + +#include +#include +#include +#include +#include + +#include "node/node.h" +#include "nodeviewcommon.h" + +namespace olive { + +class NodeViewEdge; + +/** + * @brief A visual widget representation of a Node object to be used in a NodeView + * + * This widget can be collapsed or expanded to show/hide the node's various parameters. + * + * To retrieve the NodeViewItem for a certain Node, use NodeView::NodeToUIObject(). + */ +class NodeViewItem : public QGraphicsRectItem +{ +public: + NodeViewItem(QGraphicsItem* parent = nullptr); + + QPointF GetNodePosition() const; + void SetNodePosition(const QPointF& pos); + + /** + * @brief Set the Node to correspond to this widget + */ + void SetNode(Node* n); + + /** + * @brief Get currently attached node + */ + Node* GetNode() const + { + return node_; + } + + /** + * @brief Get expanded state + */ + bool IsExpanded() const + { + return expanded_; + } + + /** + * @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(NodeInput* input, const QPointF &source_pos) const; + QPointF GetInputPoint(int input, const QPointF &source_pos) const; + + QPointF GetOutputPoint() const; + + /** + * @brief Sets the direction nodes are flowing + */ + void SetFlowDirection(NodeViewCommon::FlowDirection dir); + + static int DefaultTextPadding(); + + static int DefaultItemHeight(); + + static int DefaultItemWidth(); + + static int DefaultItemBorder(); + + qreal DefaultItemHorizontalPadding() const; + + qreal DefaultItemVerticalPadding() const; + + void AddEdge(NodeViewEdge* edge); + void RemoveEdge(NodeViewEdge* edge); + + int GetIndexAt(QPointF pt) const; + + NodeInput* GetInputAtIndex(int index) const + { + return node_inputs_.at(index); + } + + void SetHighlightedIndex(int index); + +protected: + virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; + + 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(); + + /** + * @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; + + /** + * @brief Reference to attached Node + */ + Node* node_; + + /** + * @brief Cached list of node inputs + */ + QList node_inputs_; + + /** + * @brief Rectangle of the Node's title bar (equal to rect() when collapsed) + */ + QRectF title_bar_rect_; + + /// Sizing variables to use when drawing + int node_border_width_; + + /** + * @brief Expanded state + */ + bool expanded_; + + bool hide_titlebar_; + + int highlighted_index_; + + NodeViewCommon::FlowDirection flow_dir_; + + QVector edges_; + +}; + +} + +#endif // NODEVIEWITEM_H diff --git a/app/widget/nodeview/nodeviewscene.cpp b/app/widget/nodeview/nodeviewscene.cpp new file mode 100644 index 000000000..6363cd3c1 --- /dev/null +++ b/app/widget/nodeview/nodeviewscene.cpp @@ -0,0 +1,295 @@ +/*** + + 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 "nodeviewscene.h" + +#include "common/functiontimer.h" +#include "nodeviewedge.h" +#include "nodeviewitem.h" +#include "project/item/sequence/sequence.h" + +namespace olive { + +NodeViewScene::NodeViewScene(QObject *parent) : + QGraphicsScene(parent), + graph_(nullptr), + direction_(NodeViewCommon::kLeftToRight), + curved_edges_(true) +{ +} + +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_); + + // Update position too + i.value()->SetNodePosition(i.key()->GetPosition()); + } + } + + { + // 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(); + + qDeleteAll(item_map_); + item_map_.clear(); + + qDeleteAll(edges_); + edges_.clear(); +} + +void NodeViewScene::SelectAll() +{ + QList all_items = this->items(); + + foreach (QGraphicsItem* i, all_items) { + i->setSelected(true); + } +} + +void NodeViewScene::DeselectAll() +{ + QList selected_items = this->selectedItems(); + + foreach (QGraphicsItem* i, selected_items) { + i->setSelected(false); + } +} + +NodeViewItem *NodeViewScene::NodeToUIObject(Node *n) +{ + return item_map_.value(n); +} + +NodeViewEdge *NodeViewScene::EdgeToUIObject(Node* output, NodeInput* input, int element) +{ + foreach (NodeViewEdge* edge, edges_) { + if (edge->output() == output && edge->input() == input && edge->element() == element) { + return edge; + } + } + + return nullptr; +} + +void NodeViewScene::SetGraph(NodeGraph *graph) +{ + graph_ = graph; +} + +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; + + for (iterator=item_map_.begin();iterator!=item_map_.end();iterator++) { + if (iterator.value()->isSelected()) { + selected.append(iterator.value()); + } + } + + return selected; +} + +QVector NodeViewScene::GetSelectedEdges() const +{ + QVector edges; + + foreach (NodeViewEdge* e, edges_) { + if (e->isSelected()) { + edges.append(e); + } + } + + return edges; +} + +void NodeViewScene::AddNode(Node* node) +{ + NodeViewItem* item = new NodeViewItem(); + + item->SetFlowDirection(direction_); + item->SetNode(node); + + addItem(item); + item_map_.insert(node, item); + + connect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); + connect(node, &Node::LabelChanged, this, &NodeViewScene::NodeLabelChanged); +} + +void NodeViewScene::RemoveNode(Node *node) +{ + disconnect(node, &Node::LabelChanged, this, &NodeViewScene::NodeLabelChanged); + disconnect(node, &Node::PositionChanged, this, &NodeViewScene::NodePositionChanged); + + delete item_map_.take(node); +} + +void NodeViewScene::AddEdge(Node* output, NodeInput* input, int element) +{ + AddEdgeInternal(output, input, element, NodeToUIObject(output), NodeToUIObject(input->parent())); +} + +void NodeViewScene::RemoveEdge(Node* output, NodeInput* input, int element) +{ + NodeViewEdge* edge = EdgeToUIObject(output, input, element); + 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->GetRoutesTo(n) == 1) { + weight += DetermineWeight(i); + } + } + + return qMax(1, weight); +} + +void NodeViewScene::AddEdgeInternal(Node *output, NodeInput *input, int element, NodeViewItem *from, NodeViewItem *to) +{ + NodeViewEdge* edge_ui = new NodeViewEdge(output, input, element, 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); +} + +Qt::Orientation NodeViewScene::GetFlowOrientation() const +{ + return NodeViewCommon::GetFlowOrientation(direction_); +} + +NodeViewCommon::FlowDirection NodeViewScene::GetFlowDirection() const +{ + return direction_; +} + +void NodeViewScene::ReorganizeFrom(Node* n) +{ + QVector immediates = n->GetImmediateDependencies(); + + if (immediates.isEmpty()) { + // Nothing to do + return; + } + + QPointF parent_pos = n->GetPosition(); + + int weight_count = DetermineWeight(n); + + qreal child_x = parent_pos.x() - 1.0; + qreal children_height = weight_count-1; + qreal children_y = parent_pos.y() - children_height * 0.5; + + int weight_counter = 0; + + foreach (Node* i, immediates) { + if (i->GetRoutesTo(n) == 1) { + int weight = DetermineWeight(i); + + i->SetPosition(QPointF(child_x, + children_y + weight_counter + (weight - 1) * 0.5)); + + weight_counter += weight; + + ReorganizeFrom(i); + } + } +} + +bool NodeViewScene::GetEdgesAreCurved() const +{ + return curved_edges_; +} + +void NodeViewScene::SetEdgesAreCurved(bool curved) +{ + if (curved_edges_ != curved) { + curved_edges_ = curved; + + foreach (NodeViewEdge* e, edges_) { + e->SetCurved(curved_edges_); + } + } +} + +void NodeViewScene::NodePositionChanged(const QPointF &pos) +{ + // Update node's internal position + item_map_.value(static_cast(sender()))->SetNodePosition(pos); +} + +void NodeViewScene::NodeLabelChanged() +{ + // 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 new file mode 100644 index 000000000..9c9c44b76 --- /dev/null +++ b/app/widget/nodeview/nodeviewscene.h @@ -0,0 +1,138 @@ +/*** + + 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 NODEVIEWSCENE_H +#define NODEVIEWSCENE_H + +#include +#include + +#include "node/graph.h" +#include "nodeviewedge.h" +#include "nodeviewitem.h" + +namespace olive { + +class NodeViewScene : public QGraphicsScene +{ + Q_OBJECT +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, NodeInput *input, int element); + + void SetGraph(NodeGraph* graph); + + QVector GetSelectedNodes() const; + QVector GetSelectedItems() const; + QVector GetSelectedEdges() const; + + const QHash& item_map() const + { + return item_map_; + } + + const QVector& edges() const + { + return edges_; + } + + Qt::Orientation GetFlowOrientation() const; + + NodeViewCommon::FlowDirection GetFlowDirection() const; + void SetFlowDirection(NodeViewCommon::FlowDirection direction); + + bool GetEdgesAreCurved() const; + + void ReorganizeFrom(Node* n); + +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(). + */ + void 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); + + void AddEdge(Node* output, NodeInput* input, int element); + void RemoveEdge(Node* output, NodeInput* input, int element); + + /** + * @brief Set whether edges in this scene should be curved or not + */ + void SetEdgesAreCurved(bool curved); + +private: + static int DetermineWeight(Node* n); + + void AddEdgeInternal(Node* output, NodeInput* input, int element, NodeViewItem* from, NodeViewItem* to); + + QHash item_map_; + + QVector edges_; + + NodeGraph* graph_; + + NodeViewCommon::FlowDirection direction_; + + bool curved_edges_; + +private slots: + /** + * @brief Receiver for whenever a node position changes + */ + void NodePositionChanged(const QPointF& pos); + + /** + * @brief Receiver for when a node's label has changed + */ + void NodeLabelChanged(); + +}; + +} + +#endif // NODEVIEWSCENE_H