refactor: workspace layout — crates/, app at root, legacy C++ removed

Single mechanical restructure commit:
- root Cargo.toml = oakapp bin + workspace; one cargo build produces
  oakapp, oak-cli, oak-worker, liboakengine.dylib
- app/rust/src -> src/ (app at repo root, no rust/ nesting)
- src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore;
  src/bindings/oakotio -> crates/oakotio; src/engine/rust ->
  crates/oakengine (keeps cdylib+staticlib+rlib)
- public C headers include/<mod>/ -> crates/oakengine/include/<mod>/
- OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone)
- legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/,
  app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets
  submodule, otio-install, all build-* output (~40GB)
- oakstorage kept but excluded from the workspace (skeleton w/ todos);
  gpui excluded (own workspace)
- verified: cargo build green, cargo test --workspace 1845/0
  (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
2026-08-10 20:24:25 +08:00
parent f8540e3892
commit 013a175707
4212 changed files with 8331 additions and 2274987 deletions
-39
View File
@@ -1,39 +0,0 @@
# Olive - Non-Linear Video Editor
# Copyright (C) 2022 Olive Team
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
set(OLIVE_SOURCES
${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
widget/nodeview/nodeviewscene.h
widget/nodeview/nodeviewtoolbar.cpp
widget/nodeview/nodeviewtoolbar.h
widget/nodeview/nodewidget.cpp
widget/nodeview/nodewidget.h
PARENT_SCOPE
)
File diff suppressed because it is too large Load Diff
-307
View File
@@ -1,307 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEW_H
#define OAK_NODEVIEW_H
#include <QGraphicsView>
#include <QHash>
#include <QTimer>
#include "core.h"
#include "engineeventbridge.h"
#include "nodeviewedge.h"
#include "nodeviewcontext.h"
#include "nodeviewminimap.h"
#include "nodeviewscene.h"
#include "widget/handmovableview/handmovableview.h"
#include "widget/menu/menu.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 {
Q_OBJECT
public:
NodeView(QWidget *parent = nullptr);
virtual ~NodeView() override;
void set_contexts(const QVector<oak::Node> &nodes);
const QVector<oak::Node> &get_contexts() const
{
if (overlay_view_) {
return overlay_view_->get_contexts();
} else {
return contexts_;
}
}
bool is_group_overlay() const
{
return overlay_view_;
}
void close_contexts_belonging_to_project(oak::Project project);
void clear_graph();
/**
* @brief Delete selected nodes from graph (user-friendly/undoable)
*/
void delete_selected();
void select_all();
void deselect_all();
void select(
const QVector<QPair<OakEngineNode *, OakEngineNode *>> &nodes,
bool center_view_on_item);
void copy_selected(bool cut);
void paste();
void duplicate();
void set_color_label(int index);
void zoom_in();
void zoom_out();
const QVector<oak::Node> &get_current_contexts() const
{
return contexts_;
}
public slots:
void set_mini_map_enabled(bool e)
{
minimap_->setVisible(e);
}
void show_add_menu()
{
Menu *m = create_add_menu(nullptr);
m->exec(QCursor::pos());
delete m;
}
void center_on_items_bounding_rect();
void center_on_node(OakEngineNode *n);
void label_selected_nodes();
signals:
void nodes_selected(const QVector<OakEngineNode *> &nodes);
void nodes_deselected(const QVector<OakEngineNode *> &nodes);
void node_selection_changed(const QVector<OakEngineNode *> &nodes);
void node_selection_changed_with_contexts(
const QVector<QPair<OakEngineNode *, OakEngineNode *>> &nodes);
void node_group_opened(OakEngineNode *group);
void node_group_closed();
void esc_pressed();
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 dragEnterEvent(QDragEnterEvent *event) override;
virtual void dragMoveEvent(QDragMoveEvent *event) override;
virtual void dropEvent(QDropEvent *event) override;
virtual void dragLeaveEvent(QDragLeaveEvent *event) override;
virtual void resizeEvent(QResizeEvent *event) override;
virtual void zoom_into_cursor_position(QWheelEvent *event, double multiplier,
const QPointF &cursor_pos) override;
virtual bool event(QEvent *event) override;
virtual bool eventFilter(QObject *object, QEvent *event) override;
virtual void changeEvent(QEvent *e) override;
private:
void detach_items_from_cursor(bool delete_nodes_too = true);
void set_flow_direction(NodeViewCommon::FlowDirection dir);
void move_attached_nodes_to_cursor(const QPoint &p);
void process_moving_attached_nodes(const QPoint &pos);
QVector<oak::Node> process_dropping_attached_nodes(void *command,
oak::Node select_context,
const QPoint &pos);
oak::Node get_context_at_mouse_pos(const QPoint &p);
void connect_selection_changed_signal();
void disconnect_selection_changed_signal();
void zoom_from_keyboard(double multiplier);
void clear_create_edge_input_if_necessary();
QPointF get_estimated_position_for_context(NodeViewItem *item,
oak::Node context) const;
NodeViewItem *get_assumed_item_for_selected_node(oak::Node node);
bool get_assumed_position_for_selected_node(oak::Node node, NodeViewItemPosition *pos);
Menu *create_add_menu(Menu *parent);
void position_new_edge(const QPoint &pos);
void add_context(oak::Node n);
void remove_context(oak::Node n);
bool is_item_attached_to_cursor(NodeViewItem *item) const;
void expand_item(NodeViewItem *item);
void collapse_item(NodeViewItem *item);
void end_edge_drag(bool cancel = false);
void post_paste(const QVector<oak::Node> &new_nodes,
const QHash<oak::Node, NodeViewItemPosition> &map);
void resize_overlay();
NodeViewMiniMap *minimap_;
NodeViewContext *get_context_item_from_node_item(NodeViewItem *item);
struct AttachedItem {
NodeViewItem *item;
oak::Node node;
QPointF original_pos;
};
void set_attached_items(const QVector<AttachedItem> &items);
QVector<AttachedItem> attached_items_;
NodeViewEdge *drop_edge_;
oak::Input drop_input_;
NodeViewEdge *create_edge_;
NodeViewItem *create_edge_output_item_;
NodeViewItem *create_edge_input_item_;
oak::Input create_edge_input_;
bool create_edge_already_exists_;
bool create_edge_from_output_;
QVector<NodeViewItem *> create_edge_expanded_items_;
NodeViewScene scene_;
QVector<oak::Node> selected_nodes_;
QVector<oak::Node> contexts_;
QVector<oak::Node> last_set_filter_nodes_;
QHash<oak::Node, QPointF> context_offsets_;
QMap<NodeViewItem *, QPointF> dragging_items_;
NodeView *overlay_view_;
double scale_;
bool dont_emit_selection_signals_;
EngineEventBridge *bridge_ = nullptr;
QHash<oak::Node, int64_t> removed_from_graph_subs_;
QAction *show_in_param_editor_action_;
static const double k_minimum_scale;
static const int k_maximum_contexts;
private slots:
/**
* @brief Receiver for when the scene's selected items change
*/
void update_selection_cache();
/**
* @brief Receiver for when the user right clicks (or otherwise requests a context menu)
*/
void show_context_menu(const QPoint &pos);
/**
* @brief Receiver for when the user requests a new node from the add menu
*/
void create_node_slot(QAction *action);
/**
* @brief Receiver for setting the direction from the context menu
*/
void context_menu_set_direction(QAction *action);
/**
* @brief Opens the selected node in a Viewer
*/
void open_selected_node_in_viewer();
void update_scene_bounding_rect();
void reposition_mini_map();
void update_viewport_on_mini_map();
void move_to_scene_point(const QPointF &pos);
void node_removed_from_graph(OakEngineNode *source);
void group_nodes();
void ungroup_nodes();
void show_node_properties();
void show_selected_node_in_param_editor();
void item_about_to_be_deleted(NodeViewItem *item);
void close_overlay();
};
}
#endif // OAK_NODEVIEW_H
-76
View File
@@ -1,76 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEWCOMMON_H
#define OAK_NODEVIEWCOMMON_H
#include <QtGlobal>
#include "oakutil/define.h"
namespace olive
{
class NodeViewCommon {
public:
enum FlowDirection {
k_invalid_direction = -1,
k_top_to_bottom,
k_bottom_to_top,
k_left_to_right,
k_right_to_left
};
static Qt::Orientation get_flow_orientation(FlowDirection dir)
{
if (dir == k_top_to_bottom || dir == k_bottom_to_top) {
return Qt::Vertical;
} else {
return Qt::Horizontal;
}
}
static bool is_flow_vertical(FlowDirection dir)
{
return dir == k_top_to_bottom || dir == k_bottom_to_top;
}
static bool is_flow_horizontal(FlowDirection dir)
{
return dir == k_left_to_right || dir == k_right_to_left;
}
static bool directions_are_opposing(FlowDirection a, FlowDirection b)
{
return ((a == NodeViewCommon::k_left_to_right &&
b == NodeViewCommon::k_right_to_left) ||
(a == NodeViewCommon::k_right_to_left &&
b == NodeViewCommon::k_left_to_right) ||
(a == NodeViewCommon::k_top_to_bottom &&
b == NodeViewCommon::k_bottom_to_top) ||
(a == NodeViewCommon::k_bottom_to_top &&
b == NodeViewCommon::k_top_to_bottom));
}
};
}
#endif // OAK_NODEVIEWCOMMON_H
-511
View File
@@ -1,511 +0,0 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "nodeviewcontext.h"
#include <QBrush>
#include "oakengine/node.h"
#include <QCoreApplication>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QPen>
#include <QStyleOptionGraphicsItem>
#include "core.h"
#include "common/trackreferencehandle.h"
#include "nodeviewitem.h"
#include "common/colorcodingapp.h"
#include "oakengine/timeline.h"
#include "oakutil/qtutils.h"
#include "widget/timelinewidget/cliphandle.h"
#include "widget/viewer/vieweroutpututils.h"
namespace olive
{
#define super QGraphicsRectItem
NodeViewContext::NodeViewContext(oak::Node context, QGraphicsItem *item)
: super(item)
, context_(context)
, bridge_(new EngineEventBridge(this))
{
// Block contexts (clips/transitions nested in the node graph) show their
// track placement in the title. All engine queries go through the C ABI:
// oakengine_node_is_block() replaces dynamic_cast<Block*> (Block is
// abstract and carries no own type id), and the track type ordinals are
// the TrackReference mirror values (== engine Track::Type, pinned by the
// static_asserts in trackreferencehandle.h).
OakEngineNode *track_node =
oakengine_node_is_block(context_.handle()) ?
block_track_handle(
reinterpret_cast<OakEngineBlock *>(context_.handle())) :
nullptr;
OakEngineNode *track_seq =
track_node ? oakengine_track_get_sequence(track_node) : nullptr;
if (track_seq) {
Rational timebase = viewer_output_video_params(track_seq)
.frame_rate_as_time_base();
QString type_label;
switch (oakengine_track_get_type(track_node)) {
case TrackReference::k_video:
type_label = QCoreApplication::translate("NodeViewContext", "V");
break;
case TrackReference::k_audio:
type_label = QCoreApplication::translate("NodeViewContext", "A");
break;
case TrackReference::k_subtitle:
type_label = QCoreApplication::translate("NodeViewContext", "S");
break;
default:
break;
}
int in_num = 0, in_den = 1, out_num = 0, out_den = 1;
oakengine_block_get_in_rational(context_.handle(), &in_num, &in_den);
oakengine_block_get_out_rational(context_.handle(), &out_num, &out_den);
lbl_ =
QCoreApplication::translate("NodeViewContext", "%1 [%2] :: %3 - %4")
.arg(context_.label_and_name(),
type_label,
QString::fromStdString(Timecode::time_to_timecode(
Rational(in_num, in_den), timebase,
Core::instance()->get_timecode_display())),
QString::fromStdString(Timecode::time_to_timecode(
Rational(out_num, out_den), timebase,
Core::instance()->get_timecode_display())));
} else {
lbl_ = context_.label_and_name();
}
connect(bridge_, &EngineEventBridge::node_node_added_to_context, this,
[this](OakEngineNode *source, OakEngineNode *node) {
if (source == context_.handle()) {
add_child(node);
} else {
group_added_node(node, source);
}
});
connect(bridge_, &EngineEventBridge::node_node_removed_from_context, this,
[this](OakEngineNode *source, OakEngineNode *node) {
if (source == context_.handle()) {
remove_child(node);
} else {
group_removed_node(node, source);
}
});
connect(bridge_, &EngineEventBridge::node_context_position_changed, this,
[this](OakEngineNode *, OakEngineNode *node, double x, double y) {
set_child_position(node, QPointF(x, y));
});
connect(bridge_, &EngineEventBridge::node_input_connected, this,
[this](OakEngineNode *source, OakEngineNode *output,
const QString &input, int element) {
child_input_connected(output,
oak::Input(source, input, element));
});
connect(bridge_, &EngineEventBridge::node_input_disconnected, this,
[this](OakEngineNode *source, OakEngineNode *output,
const QString &input, int element) {
child_input_disconnected(output,
oak::Input(source, input, element));
});
node_subs_[context_.handle()].append(bridge_->subscribe(
context_.handle(),
OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT));
node_subs_[context_.handle()].append(bridge_->subscribe(
context_.handle(),
OAKENGINE_EVENT_NODE_CONTEXT_POSITION_CHANGED));
node_subs_[context_.handle()].append(bridge_->subscribe(
context_.handle(),
OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT));
const int ctx_count = context_.context_node_count();
for (int i = 0; i < ctx_count; i++) {
oak::Node child = context_.context_node_at(i).node;
if (!child.is_null()) {
add_child(child.handle());
}
}
}
NodeViewContext::~NodeViewContext()
{
// Delete edges before items, because the edge constructor references the items
qDeleteAll(edges_);
edges_.clear();
}
void NodeViewContext::add_child(OakEngineNode *node)
{
if (context_.is_null()) {
return;
}
NodeViewItem *item = new NodeViewItem(oak::Node(node), context_, this);
item->set_flow_direction(flow_dir_);
add_node_internal(node, item);
oak::Node group_node(node);
if (group_node.is_group()) {
const int grp_count = group_node.context_node_count();
for (int i = 0; i < grp_count; i++) {
oak::Node grp_child = group_node.context_node_at(i).node;
if (!grp_child.is_null()) {
// Use this item as the representative for all of these nodes too
add_node_internal(grp_child.handle(), item);
}
}
node_subs_[node].append(bridge_->subscribe(
reinterpret_cast<void *>(node),
OAKENGINE_EVENT_NODE_NODE_ADDED_TO_CONTEXT));
node_subs_[node].append(bridge_->subscribe(
reinterpret_cast<void *>(node),
OAKENGINE_EVENT_NODE_NODE_REMOVED_FROM_CONTEXT));
}
update_rect();
}
void NodeViewContext::set_child_position(OakEngineNode *node, const QPointF &pos)
{
item_map_.value(node)->set_node_position(pos);
}
void NodeViewContext::remove_child(OakEngineNode *node)
{
foreach (int64_t id, node_subs_.take(node)) {
bridge_->unsubscribe(id);
}
NodeViewItem *item = item_map_.take(node);
// Remove from scene before emitting signal so that any drag functions that might be happening
// now can be handled before the item is destroyed
scene()->removeItem(item);
emit item_about_to_be_deleted(item);
// Delete edges first because the edge destructor will try to reference item (maybe that should
// be changed...)
QVector<NodeViewEdge *> edges_to_remove = item->get_all_edges_recursively();
foreach (NodeViewEdge *edge, edges_to_remove) {
if (item->get_node() == oak::Node(node) || edge->output() == oak::Node(node) ||
edge->input().node_handle() == node) {
child_input_disconnected(edge->output().handle(), 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->get_node() == oak::Node(node)) {
if (item->get_node().is_group()) {
for (auto it = item_map_.begin(); it != item_map_.end();) {
if (it.value() == item) {
it = item_map_.erase(it);
} else {
it++;
}
}
}
delete item;
}
update_rect();
}
void NodeViewContext::child_input_connected(OakEngineNode *output, const oak::Input &input)
{
// Add edge
if (!input.is_hidden()) {
if (NodeViewItem *output_item = item_map_.value(output)) {
add_edge_internal(
output, input, output_item,
item_map_.value(input.node_handle())->get_item_for_input(input));
}
}
}
bool NodeViewContext::child_input_disconnected(OakEngineNode *output,
const oak::Input &input)
{
// Remove edge
for (int i = 0; i < edges_.size(); i++) {
NodeViewEdge *e = edges_.at(i);
if (e->output() == oak::Node(output) && e->input() == input) {
if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: edge removed: %p -> %p (%s,%d) ctx=%p",
(void *)output, (void *)input.node_handle(),
qPrintable(input.input_id()), input.element(),
(void *)this);
}
delete e;
edges_.removeAt(i);
return true;
}
}
return false;
}
qreal get_text_offset(const QFontMetricsF &fm)
{
return fm.height() / 2;
}
void NodeViewContext::update_rect()
{
QFont f;
QFontMetricsF fm(f);
qreal lbl_offset = get_text_offset(fm);
QRectF cbr = childrenBoundingRect();
QRectF rect = cbr;
int pad = NodeViewItem::default_item_height();
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::set_flow_direction(NodeViewCommon::FlowDirection dir)
{
flow_dir_ = dir;
foreach (NodeViewItem *item, item_map_) {
item->set_flow_direction(dir);
}
}
void NodeViewContext::set_curved_edges(bool e)
{
curved_edges_ = e;
foreach (NodeViewEdge *edge, edges_) {
edge->set_curved(e);
}
}
void NodeViewContext::get_selected_for_deletion(QVector<OakEngineNode *> &nodes,
QVector<OakEngineNode *> &contexts,
QVector<NodeViewEdge *> &edges) const
{
// Collect any selected edges
foreach (NodeViewEdge *edge, edges_) {
if (edge->isSelected()) {
edges.append(edge);
}
}
// Collect any selected nodes
foreach (NodeViewItem *node, item_map_) {
if (node->isSelected()) {
nodes.append(node->get_node().handle());
contexts.append(context_.handle());
}
}
}
void NodeViewContext::select(const QVector<OakEngineNode *> &nodes)
{
foreach (OakEngineNode *n, nodes) {
if (NodeViewItem *item = item_map_.value(n)) {
item->setSelected(true);
}
}
}
QVector<NodeViewItem *> NodeViewContext::get_selected_items() const
{
QVector<NodeViewItem *> 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::map_scene_pos_to_node_pos_in_context(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::screen_to_node_point(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 = AppColorCoding::get_color(context_.effective_color_label());
QColor c = QtUtils::to_q_color(color);
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(AppColorCoding::get_ui_selector_color(color));
int offset = get_text_offset(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::add_node_internal(OakEngineNode *node, NodeViewItem *item)
{
if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: node added to ctx %p: %p", (void *)this,
(void *)node);
}
node_subs_[node].append(bridge_->subscribe(
reinterpret_cast<void *>(node),
OAKENGINE_EVENT_NODE_INPUT_CONNECTED));
node_subs_[node].append(bridge_->subscribe(
reinterpret_cast<void *>(node),
OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED));
item_map_.insert(node, item);
if (node == context_.handle()) {
item->set_label_as_output(true);
}
// Iterate output connections via the wrapper
oak::Node wrapped(node);
const int out_count = wrapped.output_connection_count();
for (int i = 0; i < out_count; i++) {
oak::NodeConnection conn = wrapped.output_connection_at_ex(i);
if (!conn.hidden) {
if (NodeViewItem *other_item = item_map_.value(conn.node.handle())) {
oak::Input ai(conn.node.handle(), conn.input_id, conn.element);
add_edge_internal(node, ai, item,
other_item->get_item_for_input(ai));
} else if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: out-edge skipped (no item for %p): %p -> %p (%s) ctx=%p",
(void *)conn.node.handle(), (void *)node,
(void *)conn.node.handle(),
qPrintable(conn.input_id), (void *)this);
}
}
}
// Iterate input connections via the wrapper
const int in_count = wrapped.input_connection_count_all();
for (int i = 0; i < in_count; i++) {
oak::NodeConnection conn = wrapped.input_connection_at_all(i);
if (!conn.hidden) {
if (NodeViewItem *other_item = item_map_.value(conn.source_node.handle())) {
oak::Input ai(conn.node.handle(), conn.input_id, conn.element);
add_edge_internal(conn.source_node.handle(), ai, other_item,
item->get_item_for_input(ai));
} else if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: in-edge skipped (no item for %p): %p -> %p (%s) ctx=%p",
(void *)conn.source_node.handle(),
(void *)conn.source_node.handle(), (void *)node,
qPrintable(conn.input_id), (void *)this);
}
}
}
}
void NodeViewContext::add_edge_internal(OakEngineNode *output, const oak::Input &input,
NodeViewItem *from, NodeViewItem *to)
{
if (from == to) {
return;
}
if (qEnvironmentVariableIsSet("OAK_DEBUG_EDGES")) {
qWarning("EDGE-DEBUG: edge added: %p -> %p (%s,%d) ctx=%p", (void *)output,
(void *)input.node_handle(), qPrintable(input.input_id()),
input.element(), (void *)this);
}
NodeViewEdge *edge_ui = new NodeViewEdge(oak::Node(output), input, from, to, this);
edge_ui->adjust();
edge_ui->set_curved(curved_edges_);
edges_.append(edge_ui);
}
void NodeViewContext::group_added_node(OakEngineNode *node, OakEngineNode *group)
{
add_node_internal(node, item_map_.value(group));
}
void NodeViewContext::group_removed_node(OakEngineNode *node, OakEngineNode *group)
{
if (item_map_.value(node) == item_map_.value(group)) {
item_map_.remove(node);
}
}
}
-126
View File
@@ -1,126 +0,0 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_NODEVIEWCONTEXT_H
#define OAK_NODEVIEWCONTEXT_H
#include <QGraphicsRectItem>
#include <QGraphicsTextItem>
#include <QHash>
#include "engineeventbridge.h"
#include "oakutil/oaknode.h"
#include "nodeviewcommon.h"
#include "nodeviewedge.h"
namespace olive
{
class NodeViewContext : public QObject, public QGraphicsRectItem {
Q_OBJECT
public:
NodeViewContext(oak::Node context, QGraphicsItem *item = nullptr);
virtual ~NodeViewContext() override;
oak::Node get_context() const
{
return context_;
}
void update_rect();
void set_flow_direction(NodeViewCommon::FlowDirection dir);
void set_curved_edges(bool e);
void get_selected_for_deletion(QVector<OakEngineNode *> &nodes,
QVector<OakEngineNode *> &contexts,
QVector<NodeViewEdge *> &edges) const;
void select(const QVector<OakEngineNode *> &nodes);
QVector<NodeViewItem *> get_selected_items() const;
QPointF map_scene_pos_to_node_pos_in_context(const QPointF &pos) const;
NodeViewItem *get_item_from_map(OakEngineNode *node) const
{
return item_map_.value(node);
}
virtual void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
public:
// Not slots: they are invoked from EngineEventBridge lambdas / directly,
// never as connect() targets.
void add_child(OakEngineNode *node);
void set_child_position(OakEngineNode *node, const QPointF &pos);
void remove_child(OakEngineNode *node);
void child_input_connected(OakEngineNode *output, const oak::Input &input);
bool child_input_disconnected(OakEngineNode *output, const oak::Input &input);
signals:
void item_about_to_be_deleted(NodeViewItem *item);
protected:
virtual QVariant itemChange(QGraphicsItem::GraphicsItemChange change,
const QVariant &value) override;
virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
private:
void add_node_internal(OakEngineNode *node, NodeViewItem *item);
void add_edge_internal(OakEngineNode *output, const oak::Input &input,
NodeViewItem *from, NodeViewItem *to);
oak::Node context_;
QString lbl_;
NodeViewCommon::FlowDirection flow_dir_;
bool curved_edges_;
int last_titlebar_height_;
QMap<OakEngineNode *, NodeViewItem *> item_map_;
QVector<NodeViewEdge *> edges_;
EngineEventBridge *bridge_ = nullptr;
QHash<OakEngineNode *, QVector<int64_t>> node_subs_;
private:
// Ordinary member functions (NOT slots): invoked from lambdas only.
void group_added_node(OakEngineNode *node, OakEngineNode *group);
void group_removed_node(OakEngineNode *node, OakEngineNode *group);
};
}
#endif // OAK_NODEVIEWCONTEXT_H
-260
View File
@@ -1,260 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodeviewedge.h"
#include <QApplication>
#include <QDebug>
#include <QGraphicsSceneMouseEvent>
#include <QStyleOptionGraphicsItem>
#include "oakutil/lerp.h"
#include "nodeview.h"
#include "nodeviewitem.h"
#include "nodeviewscene.h"
namespace olive
{
#define super QGraphicsPathItem
NodeViewEdge::NodeViewEdge(oak::Node output, const oak::Input &input,
NodeViewItem *from_item, NodeViewItem *to_item,
QGraphicsItem *parent)
: super(parent)
, output_(output)
, input_(input)
, from_item_(from_item)
, to_item_(to_item)
{
init();
set_connected(true);
from_item_->add_edge(this);
to_item_->add_edge(this);
}
NodeViewEdge::NodeViewEdge(QGraphicsItem *parent)
: QGraphicsPathItem(parent)
, from_item_(nullptr)
, to_item_(nullptr)
{
init();
}
NodeViewEdge::~NodeViewEdge()
{
if (from_item_) {
from_item_->remove_edge(this);
}
if (to_item_) {
to_item_->remove_edge(this);
}
}
void NodeViewEdge::set_from_item(NodeViewItem *i)
{
if (from_item_) {
from_item_->remove_edge(this);
}
from_item_ = i;
if (from_item_) {
from_item_->add_edge(this);
}
adjust();
}
void NodeViewEdge::set_to_item(NodeViewItem *i)
{
if (to_item_) {
to_item_->remove_edge(this);
}
to_item_ = i;
if (to_item_) {
to_item_->add_edge(this);
}
adjust();
}
void NodeViewEdge::adjust()
{
// Draw a line between the two
set_points(from_item()->get_output_point(), to_item()->get_input_point());
}
void NodeViewEdge::set_connected(bool c)
{
connected_ = c;
update();
}
void NodeViewEdge::set_highlighted(bool e)
{
highlighted_ = e;
update();
}
void NodeViewEdge::set_points(const QPointF &start, const QPointF &end)
{
cached_start_ = start;
cached_end_ = end;
update_curve();
}
void NodeViewEdge::set_curved(bool e)
{
curved_ = e;
update_curve();
}
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());
}
void NodeViewEdge::init()
{
connected_ = false;
highlighted_ = false;
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;
}
void NodeViewEdge::update_curve()
{
const QPointF &start = cached_start_;
const QPointF &end = cached_end_;
QPainterPath path;
path.moveTo(start);
double angle = std::atan2(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;
NodeViewCommon::FlowDirection from_flow =
from_item_ ? from_item_->get_flow_direction() :
NodeViewCommon::k_invalid_direction;
NodeViewCommon::FlowDirection to_flow =
to_item_ ? to_item_->get_flow_direction() :
NodeViewCommon::k_invalid_direction;
if (from_flow == NodeViewCommon::k_invalid_direction &&
to_flow == NodeViewCommon::k_invalid_direction) {
// This is a technically unsupported scenario, but to avoid issues, we'll use a fallback
from_flow = NodeViewCommon::k_left_to_right;
to_flow = NodeViewCommon::k_left_to_right;
} else if (from_flow == NodeViewCommon::k_invalid_direction) {
from_flow = to_flow;
} else if (to_flow == NodeViewCommon::k_invalid_direction) {
to_flow = from_flow;
}
if (NodeViewCommon::get_flow_orientation(from_flow) == Qt::Horizontal) {
cp1 = QPointF(half_x, start.y());
} else {
cp1 = QPointF(start.x(), half_y);
}
if (NodeViewCommon::get_flow_orientation(to_flow) == Qt::Horizontal) {
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() - std::cos(angle);
double x1 = start.x();
double x2 = cp1.x();
double x3 = cp2.x();
double x4 = end.x();
double y1 = start.y();
double y2 = cp1.y();
double y3 = cp2.y();
double y4 = end.y();
if (start.x() >= end.x()) {
std::swap(x1, x4);
std::swap(x2, x3);
std::swap(y1, y4);
std::swap(y2, y3);
}
double t = Bezier::cubic_xto_t(continue_x, x1, x2, x3, x4);
double y = Bezier::cubic_tto_y(y1, y2, y3, y4, t);
angle = std::atan2(end.y() - y, end.x() - continue_x);
}
} else {
path.lineTo(end);
}
setPath(mapFromScene(path));
}
}
-150
View File
@@ -1,150 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEEDGEITEM_H
#define OAK_NODEEDGEITEM_H
#include <QGraphicsPathItem>
#include <QPalette>
#include "nodeviewcommon.h"
#include "oakutil/oaknode.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(oak::Node output, const oak::Input &input,
NodeViewItem *from_item,
NodeViewItem *to_item, QGraphicsItem *parent = nullptr);
NodeViewEdge(QGraphicsItem *parent = nullptr);
virtual ~NodeViewEdge() override;
oak::Node output() const
{
return output_;
}
const oak::Input &input() const
{
return input_;
}
int element() const
{
return element_;
}
NodeViewItem *from_item() const
{
return from_item_;
}
NodeViewItem *to_item() const
{
return to_item_;
}
void set_from_item(NodeViewItem *i);
void set_to_item(NodeViewItem *i);
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 set_connected(bool c);
bool is_connected() const
{
return connected_;
}
/**
* @brief Set highlighted state
*
* Changes color of edge.
*/
void set_highlighted(bool e);
/**
* @brief Set points to create curve from
*/
void set_points(const QPointF &start, const QPointF &end);
/**
* @brief Set whether edges should be drawn as curved or as straight lines
*/
void set_curved(bool e);
protected:
virtual void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
private:
void init();
void update_curve();
oak::Node output_;
oak::Input input_;
int element_;
NodeViewItem *from_item_;
NodeViewItem *to_item_;
int edge_width_;
bool connected_;
bool highlighted_;
bool curved_;
QPointF cached_start_;
QPointF cached_end_;
};
}
#endif // OAK_NODEEDGEITEM_H
-934
View File
@@ -1,934 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodeviewitem.h"
#include <QDebug>
#include "oakengine/node.h"
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include "oakutil/qtutils.h"
#include "common/configwrapper.h"
#include "core.h"
#include "common/nodevaluehandle.h"
#include "nodeview.h"
#include "nodeviewscene.h"
#include "common/colorcodingapp.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"
namespace olive
{
static QVector<QString> node_input_list(oak::Node n)
{
const int count = n.input_count();
QVector<QString> result;
result.reserve(count);
for (int i = 0; i < count; i++) {
result.append(n.input_id(i));
}
return result;
}
NodeViewItem::NodeViewItem(oak::Node node, const QString &input,
int element, oak::Node context,
QGraphicsItem *parent)
: QGraphicsRectItem(parent)
, node_(node)
, input_(input)
, element_(element)
, context_(context)
, expanded_(false)
, highlighted_(false)
, flow_dir_(NodeViewCommon::k_invalid_direction)
, arrow_click_(false)
, label_as_output_(false)
{
//
// We use font metrics to set all the UI measurements for DPI-awareness
//
// Set border width
node_border_width_ = default_item_border();
// Set rect size to default
set_rect_size();
// Create connector
input_connector_ = new NodeViewItemConnector(false, this);
output_connector_ = new NodeViewItemConnector(true, this);
bridge_ = new EngineEventBridge(this);
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_LABEL_CHANGED);
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_COLOR_CHANGED);
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_MESSAGE_COUNT_CHANGED);
connect(bridge_, &EngineEventBridge::node_label_changed, this,
&NodeViewItem::node_appearance_changed);
connect(bridge_, &EngineEventBridge::node_color_changed, this,
&NodeViewItem::node_appearance_changed);
connect(bridge_, &EngineEventBridge::node_message_count_changed, this,
&NodeViewItem::node_appearance_changed);
if (is_output_item()) {
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_INPUT_ADDED);
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_INPUT_REMOVED);
connect(bridge_, &EngineEventBridge::node_input_added, this,
&NodeViewItem::repopulate_inputs);
connect(bridge_, &EngineEventBridge::node_input_removed, this,
&NodeViewItem::repopulate_inputs);
repopulate_inputs();
// Set flags for this widget
setFlag(QGraphicsItem::ItemSendsGeometryChanges);
setFlag(QGraphicsItem::ItemIsMovable);
setFlag(QGraphicsItem::ItemIsSelectable);
if (!context_.is_null()) {
QPointF ctx_pos;
bool ctx_expanded = false;
context_.context_position_of(node_, &ctx_pos, &ctx_expanded);
set_node_position(ctx_pos);
set_expanded(ctx_expanded);
}
} else {
output_connector_->setVisible(false);
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED);
connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this,
[this](OakEngineNode *, const QString &changed_input, int, int) {
input_array_size_changed(changed_input);
});
}
// This should be set during runtime, but just in case here's a default fallback
set_flow_direction(NodeViewCommon::k_left_to_right);
}
NodeViewItem::~NodeViewItem()
{
Q_ASSERT(edges_.isEmpty());
}
NodeViewItemPosition NodeViewItem::get_node_position_data() const
{
return NodeViewItemPosition{get_node_position(), is_expanded()};
}
QPointF NodeViewItem::get_node_position() const
{
return screen_to_node_point(pos(), flow_dir_);
}
void NodeViewItem::set_node_position(const QPointF &pos)
{
cached_node_pos_ = pos;
update_node_position();
}
void NodeViewItem::set_node_position(const NodeViewItemPosition &pos)
{
set_node_position(pos.position);
set_expanded(pos.expanded);
}
QVector<NodeViewEdge *> NodeViewItem::get_all_edges_recursively() const
{
QVector<NodeViewEdge *> list = edges_;
foreach (NodeViewItem *item, children_) {
list.append(item->get_all_edges_recursively());
}
return list;
}
int NodeViewItem::default_text_padding()
{
return QFontMetrics(QFont()).height() / 4;
}
int NodeViewItem::default_item_height()
{
return QFontMetrics(QFont()).height() + default_text_padding() * 2;
}
int NodeViewItem::default_item_width()
{
return QtUtils::q_font_metrics_width(QFontMetrics(QFont()),
"HHHHHHHHHHHHHHHH");
;
}
int NodeViewItem::default_item_border()
{
return QFontMetrics(QFont()).height() / 12;
}
QPointF NodeViewItem::node_to_screen_point(QPointF p,
NodeViewCommon::FlowDirection direction)
{
switch (direction) {
case NodeViewCommon::k_left_to_right:
// NodeGraphs are always left-to-right internally, no need to translate
break;
case NodeViewCommon::k_right_to_left:
// Invert X value
p.setX(-p.x());
break;
case NodeViewCommon::k_top_to_bottom:
// Swap X/Y
p = QPointF(p.y(), p.x());
break;
case NodeViewCommon::k_bottom_to_top:
// Swap X/Y and invert Y
p = QPointF(p.y(), -p.x());
break;
case NodeViewCommon::k_invalid_direction:
break;
}
// Multiply by item sizes for this direction
p.setX(p.x() * default_item_horizontal_padding(direction));
p.setY(p.y() * default_item_vertical_padding(direction));
return p;
}
QPointF NodeViewItem::screen_to_node_point(QPointF p,
NodeViewCommon::FlowDirection direction)
{
// Divide by item sizes for this direction
p.setX(p.x() / default_item_horizontal_padding(direction));
p.setY(p.y() / default_item_vertical_padding(direction));
switch (direction) {
case NodeViewCommon::k_left_to_right:
// NodeGraphs are always left-to-right internally, no need to translate
break;
case NodeViewCommon::k_right_to_left:
// Invert X value
p.setX(-p.x());
break;
case NodeViewCommon::k_top_to_bottom:
// Swap X/Y
p = QPointF(p.y(), p.x());
break;
case NodeViewCommon::k_bottom_to_top:
// Swap X/Y and invert Y
p = QPointF(-p.y(), p.x());
break;
case NodeViewCommon::k_invalid_direction:
break;
}
return p;
}
qreal NodeViewItem::default_item_horizontal_padding(
NodeViewCommon::FlowDirection dir)
{
if (NodeViewCommon::get_flow_orientation(dir) == Qt::Horizontal) {
return default_item_width() * 1.5;
} else {
return default_item_width() * 1.25;
}
}
qreal NodeViewItem::default_item_vertical_padding(NodeViewCommon::FlowDirection dir)
{
if (NodeViewCommon::get_flow_orientation(dir) == Qt::Horizontal) {
return default_item_height() * 1.5;
} else {
return default_item_height() * 2.0;
}
}
qreal NodeViewItem::default_item_horizontal_padding() const
{
return default_item_horizontal_padding(flow_dir_);
}
qreal NodeViewItem::default_item_vertical_padding() const
{
return default_item_vertical_padding(flow_dir_);
}
void NodeViewItem::add_edge(NodeViewEdge *edge)
{
edges_.append(edge);
}
void NodeViewItem::remove_edge(NodeViewEdge *edge)
{
edges_.removeOne(edge);
}
void NodeViewItem::set_expanded(bool e, bool hide_titlebar)
{
if (!can_be_expanded() || (expanded_ == e)) {
return;
}
expanded_ = e;
if (!context_.is_null()) {
context_.set_context_expanded_of(node_, e);
}
if (is_output_item()) {
// 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 (is_output_item()) {
// Create items for each input of the node
foreach (const QString &input, node_input_list(node_)) {
if (is_input_valid(input)) {
NodeViewItem *item =
new NodeViewItem(node_, input, -1, context_, this);
children_.append(item);
}
}
QVector<NodeViewEdge *> edges = edges_;
for (auto it = edges.cbegin(); it != edges.cend(); it++) {
if ((*it)->to_item() == this) {
(*it)->set_to_item(get_item_for_input((*it)->input()));
}
}
} else {
// Create items for each element of the input array
int arr_sz = oak::Input(node_, input_).array_size();
children_.resize(arr_sz);
for (int i = 0; i < arr_sz; i++) {
NodeViewItem *item =
new NodeViewItem(node_, input_, i, context_, this);
children_[i] = item;
}
QVector<NodeViewEdge *> edges = edges_;
for (auto it = edges.cbegin(); it != edges.cend(); it++) {
if ((*it)->to_item() == this) {
(*it)->set_to_item(get_item_for_input((*it)->input()));
}
}
}
} else {
foreach (NodeViewItem *child, children_) {
QVector<NodeViewEdge *> child_edges = child->edges();
foreach (NodeViewEdge *edge, child_edges) {
edge->set_to_item(this);
}
delete child;
}
children_.clear();
}
update_children_positions();
if (flow_dir_ == NodeViewCommon::k_top_to_bottom) {
update_output_connector_position();
}
readjust_all_edges();
update_context_rect();
update();
}
void NodeViewItem::toggle_expanded()
{
set_expanded(!is_expanded());
}
void NodeViewItem::paint(QPainter *painter,
const QStyleOptionGraphicsItem *option, QWidget *)
{
// Use main window palette since the palette passed in `widget` is the NodeView palette which
// has been slightly modified
QPalette app_pal = Core::instance()->main_window()->palette();
// We only draw a single unit's worth
QRectF single_unit_rect = rect();
single_unit_rect.setHeight(default_item_height());
if (is_output_item()) {
// 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(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 (is_output_item()) {
if (label_as_output_) {
node_name = QCoreApplication::translate("NodeViewItem", "Output");
} else {
node_label = node_.get_label();
node_name = node_.short_name();
}
} else {
if (element_ == -1) {
node_name = oak::Input(node_, input_).name();
} else {
int64_t arraystart = 0;
oak::Input(node_, input_).property_int("arraystart", &arraystart);
node_name = QString::number(element_ + static_cast<int>(arraystart));
}
}
// Draw arrow if necessary
int arrow_size = can_be_expanded() ? draw_expand_arrow(painter) : 0;
if (is_output_item()) {
// Determine the text color (automatically calculate from node background color)
painter->setPen(AppColorCoding::get_ui_selector_color(
AppColorCoding::get_color(node_.effective_color_label())));
} else {
// Just use text item
painter->setPen(app_pal.text().color());
}
if (node_label.isEmpty()) {
// Draw name only
draw_node_title(painter, node_name, single_unit_rect, Qt::AlignVCenter,
arrow_size);
} else {
int text_pad = default_text_padding() / 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);
draw_node_title(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);
draw_node_title(painter, node_name, safe_label_bounds, Qt::AlignBottom,
arrow_size);
}
if (is_output_item()) {
int message_count = node_.plugin_message_count();
if (message_count > 0) {
QString badge_text = QString::number(message_count);
QFont badge_font = painter->font();
badge_font.setPointSizeF(badge_font.pointSizeF() * 0.7);
painter->setFont(badge_font);
QFontMetrics badge_metrics(badge_font);
int text_width = badge_metrics.horizontalAdvance(badge_text);
int text_height = badge_metrics.height();
int pad = text_height / 3;
int badge_width = qMax(text_width + pad * 2, text_height + pad);
int badge_height = text_height + pad;
QRectF badge_rect(single_unit_rect.right() - badge_width - 4,
single_unit_rect.top() + 4, badge_width,
badge_height);
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(220, 50, 47));
painter->drawRoundedRect(badge_rect, badge_height / 2,
badge_height / 2);
painter->setPen(Qt::white);
painter->drawText(badge_rect, Qt::AlignCenter, badge_text);
}
}
// Draw final border (output only)
if (is_output_item()) {
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)
{
if (last_arrow_rect_.contains(event->pos().toPoint())) {
arrow_click_ = true;
toggle_expanded();
return;
}
event->setModifiers(
QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
QGraphicsRectItem::mousePressEvent(event);
}
void NodeViewItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (arrow_click_) {
return;
}
event->setModifiers(
QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
QGraphicsRectItem::mouseMoveEvent(event);
}
void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (arrow_click_) {
arrow_click_ = false;
return;
}
event->setModifiers(
QtUtils::flip_control_and_shift_modifiers(event->modifiers()));
QGraphicsRectItem::mouseReleaseEvent(event);
}
QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change,
const QVariant &value)
{
if (node_.is_null()) {
return QGraphicsItem::itemChange(change, value);
}
if (change == ItemPositionHasChanged) {
readjust_all_edges();
update_context_rect();
} else if (change == ItemSelectedHasChanged) {
if (value.toBool()) {
qDebug() << "Selected node:" << node_.handle();
}
}
return QGraphicsItem::itemChange(change, value);
}
void NodeViewItem::readjust_all_edges()
{
foreach (NodeViewEdge *edge, edges_) {
if (NodeViewItem *to_item = edge->to_item()) {
static_cast<NodeViewItem *>(to_item->parentItem())
->update_flow_direction_of_input_item(to_item);
}
edge->adjust();
}
foreach (NodeViewItem *child, children_) {
child->readjust_all_edges();
}
}
void NodeViewItem::update_context_rect()
{
QGraphicsItem *item = parentItem();
while (item) {
if (NodeViewContext *ctx = dynamic_cast<NodeViewContext *>(item)) {
ctx->update_rect();
break;
}
item = item->parentItem();
}
}
void NodeViewItem::draw_node_title(QPainter *painter, QString text,
const QRectF &rect,
Qt::Alignment vertical_align,
int icon_full_size)
{
QFontMetrics fm = painter->fontMetrics();
// Calculate how much space we have for text
int item_width = this->rect().width();
int max_text_width = item_width - default_text_padding() * 2 - icon_full_size;
int label_width = QtUtils::q_font_metrics_width(fm, text);
// 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 {
text.chop(1);
concatenated =
QCoreApplication::translate("NodeViewItem", "%1...").arg(text);
} while ((label_width = QtUtils::q_font_metrics_width(fm, concatenated)) >
max_text_width);
text = concatenated;
}
// Determine X position (favors horizontal centering unless it'll overrun the arrow)
QRectF text_rect = rect;
Qt::Alignment text_align = Qt::AlignHCenter | vertical_align;
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 | vertical_align;
}
// Draw the text in a rect (the rect is sized around text already in the constructor)
painter->drawText(text_rect, text_align, text);
}
int NodeViewItem::draw_expand_arrow(QPainter *painter)
{
// Draw right or down arrow based on expanded state
int icon_size = painter->fontMetrics().height() / 2;
int icon_padding = default_item_height() / 2 - icon_size / 2;
int icon_full_size = icon_size + icon_padding * 2;
painter->setRenderHint(QPainter::SmoothPixmapTransform);
const QIcon &expand_icon = is_expanded() ? icon::tri_down : icon::tri_right;
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::set_label_as_output(bool e)
{
label_as_output_ = e;
output_connector_->setVisible(!e);
update();
}
QPointF NodeViewItem::get_input_point() const
{
return input_connector_->scenePos();
}
QPointF NodeViewItem::get_output_point() const
{
QPointF p = output_connector_->scenePos();
QRectF r = output_connector_->polygon().boundingRect();
switch (flow_dir_) {
case NodeViewCommon::k_left_to_right:
default:
p.setX(p.x() + r.width());
break;
case NodeViewCommon::k_right_to_left:
p.setX(p.x() - r.width());
break;
case NodeViewCommon::k_top_to_bottom:
p.setY(p.y() + r.height());
break;
case NodeViewCommon::k_bottom_to_top:
p.setY(p.y() - r.height());
break;
}
return p;
}
void NodeViewItem::set_flow_direction(NodeViewCommon::FlowDirection dir)
{
if (flow_dir_ != dir) {
flow_dir_ = dir;
input_connector_->set_flow_direction(dir);
output_connector_->set_flow_direction(dir);
update_input_connector_position();
update_output_connector_position();
if (is_output_item()) {
update_node_position();
}
readjust_all_edges();
}
}
void NodeViewItem::update_node_position()
{
setPos(node_to_screen_point(cached_node_pos_, flow_dir_));
}
void NodeViewItem::update_input_connector_position()
{
QRectF output_rect = input_connector_->polygon().boundingRect();
NodeViewCommon::FlowDirection using_flow_dir = flow_dir_;
if (is_expanded() && !NodeViewCommon::is_flow_horizontal(flow_dir_)) {
if (edges_.isEmpty() || edges_.first()->from_item()->x() < this->x()) {
using_flow_dir = NodeViewCommon::k_left_to_right;
} else {
using_flow_dir = NodeViewCommon::k_right_to_left;
}
}
// Input connector flow directions change conditionally
switch (using_flow_dir) {
case NodeViewCommon::k_left_to_right:
input_connector_->setPos(rect().left() - output_rect.width(), 0);
break;
case NodeViewCommon::k_right_to_left:
input_connector_->setPos(rect().right() + output_rect.width(), 0);
break;
case NodeViewCommon::k_top_to_bottom:
input_connector_->setPos(rect().center().x(),
rect().top() - output_rect.height());
break;
case NodeViewCommon::k_bottom_to_top:
input_connector_->setPos(rect().center().x(),
rect().bottom() + output_rect.height());
break;
case NodeViewCommon::k_invalid_direction:
break;
}
}
void NodeViewItem::update_output_connector_position()
{
switch (flow_dir_) {
case NodeViewCommon::k_left_to_right:
output_connector_->setPos(rect().right(), 0);
break;
case NodeViewCommon::k_right_to_left:
output_connector_->setPos(rect().left(), 0);
break;
case NodeViewCommon::k_top_to_bottom:
output_connector_->setPos(rect().center().x(), rect().bottom());
break;
case NodeViewCommon::k_bottom_to_top:
output_connector_->setPos(rect().center().x(), rect().top());
break;
case NodeViewCommon::k_invalid_direction:
break;
}
}
bool NodeViewItem::is_input_valid(const QString &input)
{
oak::Input in(node_, input);
if (!in.is_connectable() || in.is_hidden()) {
return false;
}
// For OFX plugin nodes, only show texture inputs in the node graph
// to avoid excessively tall nodes with dozens of scalar parameters.
// Scalar parameters are still visible in the parameter panel.
if (node_.has_plugin() && in.data_type() != NodeValueType::k_texture) {
return false;
}
return true;
}
void NodeViewItem::set_rect_size(int height_units)
{
// Set rect
int widget_width = default_item_width();
int widget_height = default_item_height();
setRect(QRectF(-widget_width / 2, -widget_height / 2, widget_width,
widget_height * height_units));
}
bool NodeViewItem::can_be_expanded() const
{
if (is_output_item()) {
return has_connectable_inputs_;
} else {
return oak::Input(node_, input_).flags() & k_input_flag_array &&
element_ == -1 && !oak::Input(node_, input_).is_connected();
}
}
void NodeViewItem::update_children_positions()
{
int y = 1;
int h = default_item_height();
foreach (NodeViewItem *c, children_) {
c->setPos(QPointF(0, y * h));
y += c->get_logical_height_with_children();
}
set_rect_size(y);
if (NodeViewItem *p = dynamic_cast<NodeViewItem *>(parentItem())) {
p->update_children_positions();
}
}
int NodeViewItem::get_logical_height_with_children() const
{
int h = 1;
foreach (NodeViewItem *c, children_) {
h += c->get_logical_height_with_children();
}
return h;
}
void NodeViewItem::update_flow_direction_of_input_item(NodeViewItem *child)
{
if (!child->is_output_item()) {
if (NodeViewCommon::is_flow_vertical(flow_dir_)) {
if (!child->edges().isEmpty() &&
child->edges().first()->from_item()->scenePos().x() >
child->scenePos().x()) {
child->set_flow_direction(NodeViewCommon::k_right_to_left);
} else {
child->set_flow_direction(NodeViewCommon::k_left_to_right);
}
} else {
child->set_flow_direction(flow_dir_);
}
}
}
void NodeViewItem::repopulate_inputs()
{
if (is_output_item()) {
has_connectable_inputs_ = false;
foreach (const QString &input, node_input_list(node_)) {
if (is_input_valid(input)) {
has_connectable_inputs_ = true;
break;
}
}
input_connector_->setVisible(has_connectable_inputs_);
}
if (is_expanded() && (is_output_item() || element_ == -1)) {
// Create or remove inputs when necessary
// NOTE: This is not the most efficient thing in the world, but it does work
set_expanded(false);
set_expanded(true);
}
}
void NodeViewItem::input_array_size_changed(const QString &input)
{
if (input == input_) {
repopulate_inputs();
}
}
void NodeViewItem::node_appearance_changed()
{
update();
}
void NodeViewItem::set_highlighted(bool e)
{
highlighted_ = e;
update();
}
NodeViewItem *NodeViewItem::get_item_for_input(oak::Input input)
{
if (node_.is_group()) {
if (input.node() != node_) {
// Translate input to group input
// WRAPPER-GAP: oakengine_group_get_id_of_passthrough (group passthrough lookup has no wrapper)
char id[256];
if (oakengine_group_get_id_of_passthrough(
node_.handle(),
input.node_handle(),
input.input_id().toUtf8().constData(), input.element(),
id, sizeof(id)) > 0) {
input.set_node_handle(node_.handle());
input.set_input_id(QString::fromUtf8(id));
}
}
}
if (is_expanded()) {
if (input_.isEmpty()) {
// Look for the input in our children
foreach (NodeViewItem *i, children_) {
if (i->input_ == input.input_id()) {
return i->get_item_for_input(input);
}
}
} else {
// Look for element in our children
if (input.element() >= 0 && input.element() < children_.size()) {
return children_.at(input.element())->get_item_for_input(input);
}
}
}
// Fallback to this object
return this;
}
}
-258
View File
@@ -1,258 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEWITEM_H
#define OAK_NODEVIEWITEM_H
#include <QFontMetrics>
#include <QGraphicsRectItem>
#include <QLinearGradient>
#include <QWidget>
#include "oakutil/oaknode.h"
#include "nodeviewcommon.h"
#include "nodeviewitemconnector.h"
#include "engineeventbridge.h"
namespace olive
{
class NodeViewItem;
class NodeViewEdge;
/**
* @brief Local UI-only position aggregate (replaces engine Node::Position).
*/
struct NodeViewItemPosition {
QPointF position;
bool expanded = false;
};
/**
* @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 QObject, public QGraphicsRectItem {
Q_OBJECT
public:
NodeViewItem(oak::Node node, const QString &input, int element,
oak::Node context, QGraphicsItem *parent = nullptr);
NodeViewItem(oak::Node node, oak::Node context,
QGraphicsItem *parent = nullptr)
: NodeViewItem(node, QString(), -1, context, parent)
{
}
virtual ~NodeViewItem() override;
NodeViewItemPosition get_node_position_data() const;
QPointF get_node_position() const;
void set_node_position(const QPointF &pos);
void set_node_position(const NodeViewItemPosition &pos);
QVector<NodeViewEdge *> get_all_edges_recursively() const;
/**
* @brief Get currently attached node
*/
oak::Node get_node() const
{
return node_;
}
oak::Input get_input() const
{
return oak::Input(node_, input_, element_);
}
oak::Node get_context() const
{
return context_;
}
/**
* @brief Get expanded state
*/
bool is_expanded() const
{
return expanded_;
}
const QVector<NodeViewEdge *> &edges() const
{
return edges_;
}
/**
* @brief Set expanded state
*/
void set_expanded(bool e, bool hide_titlebar = false);
void toggle_expanded();
QPointF get_input_point() const;
QPointF get_output_point() const;
/**
* @brief Sets the direction nodes are flowing
*/
void set_flow_direction(NodeViewCommon::FlowDirection dir);
NodeViewCommon::FlowDirection get_flow_direction() const
{
return flow_dir_;
}
static int default_text_padding();
static int default_item_height();
static int default_item_width();
static int default_item_border();
static QPointF node_to_screen_point(QPointF p,
NodeViewCommon::FlowDirection direction);
static QPointF screen_to_node_point(QPointF p,
NodeViewCommon::FlowDirection direction);
static qreal
default_item_horizontal_padding(NodeViewCommon::FlowDirection dir);
static qreal default_item_vertical_padding(NodeViewCommon::FlowDirection dir);
qreal default_item_horizontal_padding() const;
qreal default_item_vertical_padding() const;
void add_edge(NodeViewEdge *edge);
void remove_edge(NodeViewEdge *edge);
bool is_labelled_as_output_of_context() const
{
return label_as_output_;
}
void set_label_as_output(bool e);
void set_highlighted(bool e);
NodeViewItem *get_item_for_input(oak::Input input);
bool is_output_item() const
{
return input_.isEmpty();
}
void readjust_all_edges();
void update_flow_direction_of_input_item(NodeViewItem *child);
bool can_be_expanded() const;
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 QVariant itemChange(QGraphicsItem::GraphicsItemChange change,
const QVariant &value) override;
private:
void update_context_rect();
void draw_node_title(QPainter *painter, QString text, const QRectF &rect,
Qt::Alignment vertical_align, int icon_full_size);
int draw_expand_arrow(QPainter *painter);
/**
* @brief Internal update function when logical position changes
*/
void update_node_position();
void update_input_connector_position();
void update_output_connector_position();
bool is_input_valid(const QString &input);
void set_rect_size(int height_units = 1);
void update_children_positions();
int get_logical_height_with_children() const;
/**
* @brief Reference to attached Node
*/
oak::Node node_;
QString input_;
int element_;
oak::Node context_;
/**
* @brief Cached list of node inputs
*/
QVector<NodeViewItem *> children_;
/// Sizing variables to use when drawing
int node_border_width_;
/**
* @brief Expanded state
*/
bool expanded_;
bool highlighted_;
NodeViewCommon::FlowDirection flow_dir_;
QVector<NodeViewEdge *> edges_;
QPointF cached_node_pos_;
QRect last_arrow_rect_;
bool arrow_click_;
NodeViewItemConnector *input_connector_;
NodeViewItemConnector *output_connector_;
bool has_connectable_inputs_;
bool label_as_output_;
EngineEventBridge *bridge_ = nullptr;
private slots:
void node_appearance_changed();
void repopulate_inputs();
void input_array_size_changed(const QString &input);
};
}
#endif // OAK_NODEVIEWITEM_H
@@ -1,102 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodeviewitemconnector.h"
#include <QApplication>
#include <QFontMetrics>
#include <QPalette>
#include <QPen>
#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::default_item_border()));
setBrush(c);
}
void NodeViewItemConnector::set_flow_direction(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::k_left_to_right:
// 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::k_top_to_bottom:
// 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::k_bottom_to_top:
// 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::k_right_to_left:
// 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::k_invalid_direction:
break;
}
setPolygon(p);
}
QPainterPath NodeViewItemConnector::shape() const
{
// Yes, we skip QGraphicsPolygonItem because it adds the polygon. QGraphicsItem adds the
// boundingRect which we modify below
return QGraphicsItem::shape(); // clazy:exclude=skipped-base-method
}
QRectF NodeViewItemConnector::boundingRect() const
{
QRectF b = this->polygon().boundingRect();
const int radius = QFontMetrics(QFont()).height() / 2;
b.adjust(-radius, -radius, radius, radius);
return b;
}
}
@@ -1,52 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEWITEMCONNECTOR_H
#define OAK_NODEVIEWITEMCONNECTOR_H
#include <QGraphicsPolygonItem>
#include "nodeviewcommon.h"
namespace olive
{
class NodeViewItemConnector : public QGraphicsPolygonItem {
public:
NodeViewItemConnector(bool is_output, QGraphicsItem *parent = nullptr);
void set_flow_direction(NodeViewCommon::FlowDirection dir);
bool is_output() const
{
return output_;
}
virtual QPainterPath shape() const override;
virtual QRectF boundingRect() const override;
private:
bool output_;
};
}
#endif // OAK_NODEVIEWITEMCONNECTOR_H
-161
View File
@@ -1,161 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodeviewminimap.h"
#include <QMouseEvent>
namespace olive
{
#define super QGraphicsView
NodeViewMiniMap::NodeViewMiniMap(NodeViewScene *scene, QWidget *parent)
: super(parent)
, resizing_(false)
{
connect(scene, &QGraphicsScene::sceneRectChanged, this,
&NodeViewMiniMap::scene_changed);
setScene(scene);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setViewportUpdateMode(FullViewportUpdate);
setFrameShape(QFrame::Panel);
setFrameShadow(QFrame::Plain);
setMouseTracking(true);
QMetaObject::invokeMethod(this, &NodeViewMiniMap::set_default_size,
Qt::QueuedConnection);
resize_triangle_sz_ = fontMetrics().height() / 2;
}
void NodeViewMiniMap::set_viewport_rect(const QPolygonF &rect)
{
viewport_rect_ = rect;
viewport()->update();
}
void NodeViewMiniMap::drawForeground(QPainter *painter, const QRectF &rect)
{
super::drawForeground(painter, rect);
QColor viewport_color = palette().text().color();
// Draw resize triangle
painter->save();
painter->resetTransform();
QPointF triangle[3] = { QPointF(0, 0), QPointF(resize_triangle_sz_, 0),
QPointF(0, resize_triangle_sz_) };
painter->setBrush(viewport_color);
painter->setPen(viewport_color);
painter->drawPolygon(triangle, 3);
painter->restore();
// Draw viewport rectangle
viewport_color.setAlphaF(0.25);
painter->setBrush(viewport_color);
painter->drawPolygon(viewport_rect_);
}
void NodeViewMiniMap::resizeEvent(QResizeEvent *event)
{
super::resizeEvent(event);
emit resized();
scene_changed(sceneRect());
}
void NodeViewMiniMap::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
if (mouse_inside_resize_triangle(event)) {
// Resizing!
resizing_ = true;
resize_anchor_ = QCursor::pos();
} else {
emit_move_signal(event);
}
}
}
void NodeViewMiniMap::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
if (resizing_) {
QPointF movement = QCursor::pos() - resize_anchor_;
resize(QSize(width() - movement.x(), height() - movement.y()));
resize_anchor_ = QCursor::pos();
} else {
emit_move_signal(event);
}
} else {
if (mouse_inside_resize_triangle(event)) {
setCursor(Qt::SizeFDiagCursor);
} else {
unsetCursor();
}
}
}
void NodeViewMiniMap::mouseReleaseEvent(QMouseEvent *event)
{
resizing_ = false;
}
void NodeViewMiniMap::scene_changed(const QRectF &bounding)
{
double x_scale = double(this->width()) / bounding.width();
double y_scale = double(this->height()) / bounding.height();
double min_scale = qMin(x_scale, y_scale);
QTransform transform;
transform.scale(min_scale, min_scale);
setTransform(transform);
}
void NodeViewMiniMap::set_default_size()
{
if (parentWidget()) {
resize(parentWidget()->width() / 4, parentWidget()->height() / 4);
}
}
bool NodeViewMiniMap::mouse_inside_resize_triangle(QMouseEvent *event)
{
return event->pos().x() <= resize_triangle_sz_ &&
event->pos().y() <= resize_triangle_sz_;
}
void NodeViewMiniMap::emit_move_signal(QMouseEvent *event)
{
emit move_to_scene_point(mapToScene(event->pos()));
}
}
-78
View File
@@ -1,78 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEWMINIMAP_H
#define OAK_NODEVIEWMINIMAP_H
#include <QGraphicsView>
#include "nodeviewscene.h"
namespace olive
{
class NodeViewMiniMap : public QGraphicsView {
Q_OBJECT
public:
NodeViewMiniMap(NodeViewScene *scene, QWidget *parent = nullptr);
public slots:
void set_viewport_rect(const QPolygonF &rect);
signals:
void resized();
void move_to_scene_point(const QPointF &pos);
protected:
virtual void drawForeground(QPainter *painter, const QRectF &rect) override;
virtual void resizeEvent(QResizeEvent *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
{
}
private slots:
void scene_changed(const QRectF &bounding);
void set_default_size();
private:
bool mouse_inside_resize_triangle(QMouseEvent *event);
void emit_move_signal(QMouseEvent *event);
int resize_triangle_sz_;
QPolygonF viewport_rect_;
bool resizing_;
QPoint resize_anchor_;
};
}
#endif // OAK_NODEVIEWMINIMAP_H
-119
View File
@@ -1,119 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodeviewscene.h"
#include "core.h"
#include "nodeviewedge.h"
#include "nodeviewitem.h"
namespace olive
{
NodeViewScene::NodeViewScene(QObject *parent)
: QGraphicsScene(parent)
, direction_(NodeViewCommon::k_left_to_right)
, curved_edges_(true)
{
}
void NodeViewScene::set_flow_direction(NodeViewCommon::FlowDirection direction)
{
direction_ = direction;
foreach (NodeViewContext *ctx, context_map_) {
ctx->set_flow_direction(direction_);
}
}
void NodeViewScene::select_all()
{
foreach (QGraphicsItem *i, items()) {
i->setSelected(true);
}
}
void NodeViewScene::deselect_all()
{
foreach (QGraphicsItem *i, items()) {
i->setSelected(false);
}
}
QVector<NodeViewItem *> NodeViewScene::get_selected_items() const
{
QVector<NodeViewItem *> items;
foreach (NodeViewContext *ctx, context_map_) {
items.append(ctx->get_selected_items());
}
return items;
}
NodeViewContext *NodeViewScene::add_context(oak::Node node)
{
NodeViewContext *context_item = context_map_.value(node);
if (!context_item) {
context_item = new NodeViewContext(node);
context_item->set_flow_direction(get_flow_direction());
context_item->set_curved_edges(get_edges_are_curved());
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 context_item;
}
void NodeViewScene::remove_context(oak::Node node)
{
delete context_map_.take(node);
}
Qt::Orientation NodeViewScene::get_flow_orientation() const
{
return NodeViewCommon::get_flow_orientation(direction_);
}
void NodeViewScene::set_edges_are_curved(bool curved)
{
if (curved_edges_ != curved) {
curved_edges_ = curved;
foreach (NodeViewContext *ctx, context_map_) {
ctx->set_curved_edges(curved_edges_);
}
}
}
}
-85
View File
@@ -1,85 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEVIEWSCENE_H
#define OAK_NODEVIEWSCENE_H
#include <QGraphicsScene>
#include <QTimer>
#include "nodeviewcontext.h"
#include "nodeviewedge.h"
#include "nodeviewitem.h"
namespace olive
{
class NodeViewScene : public QGraphicsScene {
Q_OBJECT
public:
NodeViewScene(QObject *parent = nullptr);
void select_all();
void deselect_all();
QVector<NodeViewItem *> get_selected_items() const;
const QHash<oak::Node, NodeViewContext *> &context_map() const
{
return context_map_;
}
Qt::Orientation get_flow_orientation() const;
NodeViewCommon::FlowDirection get_flow_direction() const
{
return direction_;
}
void set_flow_direction(NodeViewCommon::FlowDirection direction);
bool get_edges_are_curved() const
{
return curved_edges_;
}
public:
// Not slots: they are called directly, never used as connect() targets.
NodeViewContext *add_context(oak::Node node);
void remove_context(oak::Node node);
public slots:
/**
* @brief Set whether edges in this scene should be curved or not
*/
void set_edges_are_curved(bool curved);
private:
QHash<oak::Node, NodeViewContext *> context_map_;
NodeViewCommon::FlowDirection direction_;
bool curved_edges_;
};
}
#endif // OAK_NODEVIEWSCENE_H
-98
View File
@@ -1,98 +0,0 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "nodeviewtoolbar.h"
#include <QEvent>
#include <QHBoxLayout>
#include "ui/icons/icons.h"
namespace olive
{
#define super QWidget
NodeViewToolBar::NodeViewToolBar(QWidget *parent)
: QWidget(parent)
{
QHBoxLayout *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
add_node_btn_ = new QPushButton();
connect(add_node_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::add_node_clicked);
layout->addWidget(add_node_btn_);
minimap_btn_ = new QPushButton();
minimap_btn_->setCheckable(true);
connect(minimap_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::mini_map_enabled_toggled);
layout->addWidget(minimap_btn_);
// Zoom controls per the UI design reference: [+] [-] [Fit]
zoom_out_btn_ = new QPushButton();
connect(zoom_out_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::zoom_out_clicked);
layout->addWidget(zoom_out_btn_);
zoom_in_btn_ = new QPushButton();
connect(zoom_in_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::zoom_in_clicked);
layout->addWidget(zoom_in_btn_);
fit_btn_ = new QPushButton();
connect(fit_btn_, &QPushButton::clicked, this,
&NodeViewToolBar::fit_clicked);
layout->addWidget(fit_btn_);
layout->addStretch();
retranslate();
update_icons();
}
void NodeViewToolBar::changeEvent(QEvent *e)
{
if (e->type() == QEvent::LanguageChange) {
retranslate();
} else if (e->type() == QEvent::StyleChange) {
update_icons();
}
super::changeEvent(e);
}
void NodeViewToolBar::retranslate()
{
add_node_btn_->setToolTip(tr("Add Node"));
minimap_btn_->setToolTip(tr("Toggle Mini-Map"));
zoom_in_btn_->setToolTip(tr("Zoom In"));
zoom_out_btn_->setToolTip(tr("Zoom Out"));
fit_btn_->setToolTip(tr("Fit to Content"));
fit_btn_->setText(tr("Fit"));
}
void NodeViewToolBar::update_icons()
{
add_node_btn_->setIcon(icon::add);
minimap_btn_->setIcon(icon::mini_map);
zoom_in_btn_->setIcon(icon::zoom_in);
zoom_out_btn_->setIcon(icon::zoom_out);
}
}
-71
View File
@@ -1,71 +0,0 @@
/*
* Oak Video Editor - Non-Linear Video Editor
* Copyright (C) 2025 Olive CE Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef OAK_NODEVIEWTOOLBAR_H
#define OAK_NODEVIEWTOOLBAR_H
#include <QPushButton>
#include <QWidget>
namespace olive
{
class NodeViewToolBar : public QWidget {
Q_OBJECT
public:
NodeViewToolBar(QWidget *parent = nullptr);
public slots:
void set_mini_map_enabled(bool e)
{
minimap_btn_->setChecked(e);
}
signals:
void add_node_clicked();
void mini_map_enabled_toggled(bool e);
void zoom_in_clicked();
void zoom_out_clicked();
void fit_clicked();
protected:
virtual void changeEvent(QEvent *e) override;
private:
void retranslate();
void update_icons();
QPushButton *add_node_btn_;
QPushButton *minimap_btn_;
QPushButton *zoom_in_btn_;
QPushButton *zoom_out_btn_;
QPushButton *fit_btn_;
};
}
#endif // OAK_NODEVIEWTOOLBAR_H
-61
View File
@@ -1,61 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "nodewidget.h"
#include <QVBoxLayout>
namespace olive
{
NodeWidget::NodeWidget(QWidget *parent)
: QWidget(parent)
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
outer_layout->setContentsMargins(0, 0, 0, 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::mini_map_enabled_toggled, node_view_,
&NodeView::set_mini_map_enabled);
connect(toolbar_, &NodeViewToolBar::add_node_clicked, node_view_,
&NodeView::show_add_menu);
connect(toolbar_, &NodeViewToolBar::zoom_in_clicked, node_view_,
&NodeView::zoom_in);
connect(toolbar_, &NodeViewToolBar::zoom_out_clicked, node_view_,
&NodeView::zoom_out);
connect(toolbar_, &NodeViewToolBar::fit_clicked, node_view_,
&NodeView::center_on_items_bounding_rect);
// Set defaults
toolbar_->set_mini_map_enabled(true);
node_view_->set_mini_map_enabled(true);
setSizePolicy(node_view_->sizePolicy());
}
}
-57
View File
@@ -1,57 +0,0 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_NODEWIDGET_H
#define OAK_NODEWIDGET_H
#include <QWidget>
#include "nodeview.h"
#include "nodeviewtoolbar.h"
namespace olive
{
class NodeWidget : public QWidget {
Q_OBJECT
public:
NodeWidget(QWidget *parent = nullptr);
NodeView *view() const
{
return node_view_;
}
void set_contexts(const QVector<oak::Node> &nodes)
{
node_view_->set_contexts(nodes);
toolbar_->setEnabled(!nodes.isEmpty());
}
private:
NodeView *node_view_;
NodeViewToolBar *toolbar_;
};
}
#endif // OAK_NODEWIDGET_H