Merge branch 'main' into lulumumu9999/main

This commit is contained in:
2026-08-04 23:10:33 +08:00
133 changed files with 902 additions and 8529 deletions
File diff suppressed because it is too large Load Diff
-301
View File
@@ -1,301 +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 <QTimer>
#include "core.h"
#include "node/group/group.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<Node *> &nodes);
const QVector<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(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<Node::ContextPair> &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<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<Node *> &nodes);
void nodes_deselected(const QVector<Node *> &nodes);
void node_selection_changed(const QVector<Node *> &nodes);
void
node_selection_changed_with_contexts(const QVector<Node::ContextPair> &nodes);
void node_group_opened(NodeGroup *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<Node *> process_dropping_attached_nodes(MultiUndoCommand *command,
Node *select_context,
const QPoint &pos);
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,
Node *context) const;
NodeViewItem *get_assumed_item_for_selected_node(Node *node);
bool get_assumed_position_for_selected_node(Node *node, Node::Position *pos);
Menu *create_add_menu(Menu *parent);
void position_new_edge(const QPoint &pos);
void add_context(Node *n);
void remove_context(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<Node *> &new_nodes,
const Node::PositionMap &map);
void resize_overlay();
NodeViewMiniMap *minimap_;
NodeViewContext *get_context_item_from_node_item(NodeViewItem *item);
struct AttachedItem {
NodeViewItem *item;
Node *node;
QPointF original_pos;
};
void set_attached_items(const QVector<AttachedItem> &items);
QVector<AttachedItem> attached_items_;
NodeViewEdge *drop_edge_;
NodeInput drop_input_;
NodeViewEdge *create_edge_;
NodeViewItem *create_edge_output_item_;
NodeViewItem *create_edge_input_item_;
NodeInput create_edge_input_;
bool create_edge_already_exists_;
bool create_edge_from_output_;
QVector<NodeViewItem *> create_edge_expanded_items_;
NodeViewScene scene_;
QVector<Node *> selected_nodes_;
QVector<Node *> contexts_;
QVector<Node *> last_set_filter_nodes_;
QMap<Node *, QPointF> context_offsets_;
QMap<NodeViewItem *, QPointF> dragging_items_;
NodeView *overlay_view_;
double scale_;
bool dont_emit_selection_signals_;
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();
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 "common/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
-412
View File
@@ -1,412 +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 <QCoreApplication>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPen>
#include <QStyleOptionGraphicsItem>
#include "core.h"
#include "node/block/block.h"
#include "node/group/group.h"
#include "node/output/track/track.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "nodeviewitem.h"
#include "ui/colorcoding.h"
namespace olive
{
#define super QGraphicsRectItem
NodeViewContext::NodeViewContext(Node *context, QGraphicsItem *item)
: super(item)
, context_(context)
{
Block *block = dynamic_cast<Block *>(context_);
if (block && block->track() && block->track()->sequence()) {
Rational timebase = block->track()
->sequence()
->get_video_params()
.frame_rate_as_time_base();
lbl_ =
QCoreApplication::translate("NodeViewContext", "%1 [%2] :: %3 - %4")
.arg(block->get_label_and_name(),
Track::Reference::type_to_translated_string(
block->track()->type()),
QString::fromStdString(Timecode::time_to_timecode(
block->in(), timebase,
Core::instance()->get_timecode_display())),
QString::fromStdString(Timecode::time_to_timecode(
block->out(), timebase,
Core::instance()->get_timecode_display())));
} else {
lbl_ = context_->get_label_and_name();
}
const Node::PositionMap &map = context_->get_context_positions();
for (auto it = map.cbegin(); it != map.cend(); it++) {
add_child(it.key());
}
connect(context_, &Node::node_added_to_context, this,
&NodeViewContext::add_child, Qt::DirectConnection);
connect(context_, &Node::node_position_in_context_changed, this,
&NodeViewContext::set_child_position, Qt::DirectConnection);
connect(context_, &Node::node_removed_from_context, this,
&NodeViewContext::remove_child, Qt::DirectConnection);
}
NodeViewContext::~NodeViewContext()
{
// Delete edges before items, because the edge constructor references the items
qDeleteAll(edges_);
edges_.clear();
}
void NodeViewContext::add_child(Node *node)
{
if (!context_) {
return;
}
NodeViewItem *item = new NodeViewItem(node, context_, this);
item->set_flow_direction(flow_dir_);
add_node_internal(node, item);
if (NodeGroup *group = dynamic_cast<NodeGroup *>(node)) {
for (auto it = group->get_context_positions().cbegin();
it != group->get_context_positions().cend(); it++) {
// Use this item as the representative for all of these nodes too
add_node_internal(it.key(), item);
}
connect(group, &NodeGroup::node_added_to_context, this,
&NodeViewContext::group_added_node);
connect(group, &NodeGroup::node_removed_from_context, this,
&NodeViewContext::group_removed_node);
}
update_rect();
}
void NodeViewContext::set_child_position(Node *node, const QPointF &pos)
{
item_map_.value(node)->set_node_position(pos);
}
void NodeViewContext::remove_child(Node *node)
{
disconnect(node, &Node::input_connected, this,
&NodeViewContext::child_input_connected);
disconnect(node, &Node::input_disconnected, this,
&NodeViewContext::child_input_disconnected);
if (NodeGroup *group = dynamic_cast<NodeGroup *>(node)) {
disconnect(group, &NodeGroup::node_added_to_context, this,
&NodeViewContext::group_added_node);
disconnect(group, &NodeGroup::node_removed_from_context, this,
&NodeViewContext::group_removed_node);
}
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 (node == item->get_node() || edge->output() == node ||
edge->input().node() == node) {
child_input_disconnected(edge->output(), edge->input());
}
}
// Check if this item is specifically for this node and the node is a group. If so, remove it for
// all other entries in the map.
if (item->get_node() == node) {
if (dynamic_cast<NodeGroup *>(item->get_node())) {
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(Node *output, const NodeInput &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())->get_item_for_input(input));
}
}
}
bool NodeViewContext::child_input_disconnected(Node *output,
const NodeInput &input)
{
// Remove edge
for (int i = 0; i < edges_.size(); i++) {
NodeViewEdge *e = edges_.at(i);
if (e->output() == output && e->input() == input) {
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);
}
}
int NodeViewContext::delete_selected(NodeViewDeleteCommand *command)
{
int count = 0;
// Delete any selected edges
foreach (NodeViewEdge *edge, edges_) {
if (edge->isSelected()) {
command->add_edge(edge->output(), edge->input());
}
}
// Delete any selected nodes
foreach (NodeViewItem *node, item_map_) {
if (node->isSelected()) {
command->add_node(node->get_node(), context_);
count++;
}
}
return count;
}
void NodeViewContext::select(const QVector<Node *> &nodes)
{
foreach (Node *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 = context_->color();
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(ColorCoding::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(Node *node, NodeViewItem *item)
{
connect(node, &Node::input_connected, this,
&NodeViewContext::child_input_connected);
connect(node, &Node::input_disconnected, this,
&NodeViewContext::child_input_disconnected);
item_map_.insert(node, item);
if (node == context_) {
item->set_label_as_output(true);
}
for (auto it = node->output_connections().cbegin();
it != node->output_connections().cend(); it++) {
if (!it->second.is_hidden()) {
if (NodeViewItem *other_item = item_map_.value(it->second.node())) {
add_edge_internal(node, it->second, item,
other_item->get_item_for_input(it->second));
}
}
}
for (auto it = node->input_connections().cbegin();
it != node->input_connections().cend(); it++) {
if (!it->first.is_hidden()) {
if (NodeViewItem *other_item = item_map_.value(it->second)) {
add_edge_internal(it->second, it->first, other_item,
item->get_item_for_input(it->first));
}
}
}
}
void NodeViewContext::add_edge_internal(Node *output, const NodeInput &input,
NodeViewItem *from, NodeViewItem *to)
{
if (from == to) {
return;
}
NodeViewEdge *edge_ui = new NodeViewEdge(output, input, from, to, this);
edge_ui->adjust();
edge_ui->set_curved(curved_edges_);
edges_.append(edge_ui);
}
void NodeViewContext::group_added_node(Node *node)
{
NodeGroup *group = static_cast<NodeGroup *>(sender());
add_node_internal(node, item_map_.value(group));
}
void NodeViewContext::group_removed_node(Node *node)
{
NodeGroup *group = static_cast<NodeGroup *>(sender());
if (item_map_.value(node) == item_map_.value(group)) {
item_map_.remove(node);
}
}
}
-116
View File
@@ -1,116 +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 "node/node.h"
#include "node/nodeundo.h"
#include "nodeviewcommon.h"
#include "nodeviewedge.h"
namespace olive
{
class NodeViewContext : public QObject, public QGraphicsRectItem {
Q_OBJECT
public:
NodeViewContext(Node *context, QGraphicsItem *item = nullptr);
virtual ~NodeViewContext() override;
Node *get_context() const
{
return context_;
}
void update_rect();
void set_flow_direction(NodeViewCommon::FlowDirection dir);
void set_curved_edges(bool e);
int delete_selected(NodeViewDeleteCommand *command);
void select(const QVector<Node *> &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(Node *node) const
{
return item_map_.value(node);
}
virtual void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget = nullptr) override;
public slots:
void add_child(Node *node);
void set_child_position(Node *node, const QPointF &pos);
void remove_child(Node *node);
void child_input_connected(Node *output, const NodeInput &input);
bool child_input_disconnected(Node *output, const NodeInput &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(Node *node, NodeViewItem *item);
void add_edge_internal(Node *output, const NodeInput &input,
NodeViewItem *from, NodeViewItem *to);
Node *context_;
QString lbl_;
NodeViewCommon::FlowDirection flow_dir_;
bool curved_edges_;
int last_titlebar_height_;
QMap<Node *, NodeViewItem *> item_map_;
QVector<NodeViewEdge *> edges_;
private slots:
void group_added_node(Node *node);
void group_removed_node(Node *node);
};
}
#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 "common/lerp.h"
#include "nodeview.h"
#include "nodeviewitem.h"
#include "nodeviewscene.h"
namespace olive
{
#define super QGraphicsPathItem
NodeViewEdge::NodeViewEdge(Node *output, const NodeInput &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));
}
}
-149
View File
@@ -1,149 +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 "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, const NodeInput &input, NodeViewItem *from_item,
NodeViewItem *to_item, QGraphicsItem *parent = nullptr);
NodeViewEdge(QGraphicsItem *parent = nullptr);
virtual ~NodeViewEdge() override;
Node *output() const
{
return output_;
}
const 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 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();
Node *output_;
NodeInput 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
-907
View File
@@ -1,907 +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 <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include "common/qtutils.h"
#include "config/config.h"
#include "core.h"
#include "node/nodeundo.h"
#include "node/value.h"
#include "pluginSupport/oliveplugininstance.h"
#include "nodeview.h"
#include "nodeviewscene.h"
#include "ui/colorcoding.h"
#include "ui/icons/icons.h"
#include "window/mainwindow/mainwindow.h"
namespace olive
{
NodeViewItem::NodeViewItem(Node *node, const QString &input, int element,
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);
connect(node_, &Node::label_changed, this,
&NodeViewItem::node_appearance_changed);
connect(node_, &Node::color_changed, this,
&NodeViewItem::node_appearance_changed);
connect(node_, &Node::message_count_changed, this,
&NodeViewItem::node_appearance_changed);
if (is_output_item()) {
connect(node_, &Node::input_added, this,
&NodeViewItem::repopulate_inputs);
connect(node_, &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_) {
set_node_position(context_->get_node_position_data_in_context(node_));
}
} else {
output_connector_->setVisible(false);
connect(node_, &Node::input_array_size_changed, this,
&NodeViewItem::input_array_size_changed);
connect(node_, &Node::input_array_size_changed, this,
&NodeViewItem::input_array_size_changed);
}
// 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());
}
Node::Position NodeViewItem::get_node_position_data() const
{
return Node::Position(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 Node::Position &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_) {
context_->set_node_expanded_in_context(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_->inputs()) {
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 = node_->input_array_size(input_);
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 = node_->get_input_name(input_);
} else {
node_name = QString::number(
element_ +
node_->get_input_property(input_, QStringLiteral("arraystart"))
.toInt());
}
}
// 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(ColorCoding::get_ui_selector_color(node_->color()));
} 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()) {
auto *instance = node_->getPluginInstance();
auto *olive_instance =
dynamic_cast<plugin::OlivePluginInstance *>(instance);
int message_count =
olive_instance ? olive_instance->persistent_message_count() : 0;
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_) {
if (change == ItemPositionHasChanged) {
readjust_all_edges();
update_context_rect();
} else if (change == ItemSelectedHasChanged) {
if (value.toBool()) {
qDebug() << "Selected node:" << node_;
}
}
}
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)
{
if (!node_->is_input_connectable(input) || node_->is_input_hidden(input)) {
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_->getPluginInstance() != nullptr &&
node_->get_input_data_type(input) != NodeValue::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 node_->get_input_flags(input_) & k_input_flag_array &&
element_ == -1 && !node_->is_input_connected(input_);
}
}
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_->inputs()) {
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(NodeInput input)
{
if (NodeGroup *group = dynamic_cast<NodeGroup *>(node_)) {
if (input.node() != group) {
// Translate input to group input
QString id = group->get_id_of_passthrough(input);
input.set_node(group);
input.set_input(id);
}
}
if (is_expanded()) {
if (input_.isEmpty()) {
// Look for the input in our children
foreach (NodeViewItem *i, children_) {
if (i->input_ == input.input()) {
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;
}
}
-246
View File
@@ -1,246 +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 "node/node.h"
#include "nodeviewcommon.h"
#include "nodeviewitemconnector.h"
namespace olive
{
class NodeViewItem;
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 QObject, public QGraphicsRectItem {
Q_OBJECT
public:
NodeViewItem(Node *node, const QString &input, int element, Node *context,
QGraphicsItem *parent = nullptr);
NodeViewItem(Node *node, Node *context, QGraphicsItem *parent = nullptr)
: NodeViewItem(node, QString(), -1, context, parent)
{
}
virtual ~NodeViewItem() override;
Node::Position get_node_position_data() const;
QPointF get_node_position() const;
void set_node_position(const QPointF &pos);
void set_node_position(const Node::Position &pos);
QVector<NodeViewEdge *> get_all_edges_recursively() const;
/**
* @brief Get currently attached node
*/
Node *get_node() const
{
return node_;
}
NodeInput get_input() const
{
return NodeInput(node_, input_, element_);
}
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(NodeInput 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
*/
Node *node_;
QString input_;
int element_;
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_;
private slots:
void node_appearance_changed();
void repopulate_inputs();
void input_array_size_changed(const QString &input);
};
}
#endif // OAK_NODEVIEWITEM_H
-102
View File
@@ -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;
}
}
-52
View File
@@ -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
-120
View File
@@ -1,120 +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 "node/project/sequence/sequence.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(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(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_);
}
}
}
}
-87
View File
@@ -1,87 +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 "node/project.h"
#include "nodeviewcontext.h"
#include "nodeviewedge.h"
#include "nodeviewitem.h"
#include "undo/undostack.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<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 slots:
NodeViewContext *add_context(Node *node);
void remove_context(Node *node);
/**
* @brief Set whether edges in this scene should be curved or not
*/
void set_edges_are_curved(bool curved);
private:
QHash<Node *, NodeViewContext *> context_map_;
Project *graph_;
NodeViewCommon::FlowDirection direction_;
bool curved_edges_;
};
}
#endif // OAK_NODEVIEWSCENE_H
-76
View File
@@ -1,76 +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_);
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"));
}
void NodeViewToolBar::update_icons()
{
add_node_btn_->setIcon(icon::add);
minimap_btn_->setIcon(icon::mini_map);
}
}
-59
View File
@@ -1,59 +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);
protected:
virtual void changeEvent(QEvent *e) override;
private:
void retranslate();
void update_icons();
QPushButton *add_node_btn_;
QPushButton *minimap_btn_;
};
}
#endif // OAK_NODEVIEWTOOLBAR_H
-55
View File
@@ -1,55 +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);
// 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<Node *> &nodes)
{
node_view_->set_contexts(nodes);
toolbar_->setEnabled(!nodes.isEmpty());
}
private:
NodeView *node_view_;
NodeViewToolBar *toolbar_;
};
}
#endif // OAK_NODEWIDGET_H
-947
View File
@@ -1,947 +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 "projectexplorer.h"
#include <QDebug>
#include <QDesktopServices>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QProcess>
#include <QUrl>
#include <QVBoxLayout>
#include "common/define.h"
#include "core.h"
#include "dialog/footageproperties/footageproperties.h"
#include "dialog/proxy/proxydialog.h"
#include "dialog/sequence/sequence.h"
#include "projectexplorerundo.h"
#include "oakengine/footage.h"
#include "oakengine/node.h"
#include "oakengine/task.h"
#include "widget/menu/menu.h"
#include "widget/menu/menushared.h"
#include "node/nodeundo.h"
#include "window/mainwindow/mainwindow.h"
#include "window/mainwindow/mainwindowundo.h"
#include "widget/timelinewidget/timelinewidget.h"
namespace olive
{
namespace
{
QVector<Footage *> get_selected_proxy_footage(const QVector<Node *> &items)
{
QVector<Footage *> footage;
for (Node *node : items) {
Footage *candidate = dynamic_cast<Footage *>(node);
if (!candidate || !candidate->get_first_enabled_video_stream().is_valid() ||
footage.contains(candidate)) {
continue;
}
footage.append(candidate);
}
return footage;
}
/**
* @brief Proxy generation driven by the liboakengine C ABI facade
*
* Replaces the direct ProxyManager::get_or_start_proxy() drive: the actual
* transcode and its synchronous wait live behind
* oakengine_footage_proxy_generate() (which also records the proxy state
* on the footage and invalidates it), while the task stays on the
* TaskManager queue like before.
*/
class FacadeProxyTask : public Task {
public:
FacadeProxyTask(Footage *footage)
: footage_(footage)
{
set_title(tr("Generating proxy for \"%1\"")
.arg(footage->get_label_or_name()));
}
protected:
virtual bool run() override
{
OakEngineFootage *handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(footage_));
const int rc = oakengine_footage_proxy_generate(handle);
oakengine_footage_free(handle);
if (rc != OAKENGINE_OK) {
char err[512];
err[0] = '\0';
oakengine_footage_last_error(err, sizeof(err));
set_error(err[0] ? QString::fromUtf8(err) :
tr("Proxy generation failed"));
return false;
}
return true;
}
private:
Footage *footage_;
};
}
ProjectExplorer::ProjectExplorer(QWidget *parent)
: QWidget(parent)
, model_(this)
{
// Create layout
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0);
// Set up navigation bar
nav_bar_ = new ProjectExplorerNavigation(this);
connect(nav_bar_, &ProjectExplorerNavigation::size_changed, this,
&ProjectExplorer::size_changed_slot);
connect(nav_bar_, &ProjectExplorerNavigation::directory_up_clicked, this,
&ProjectExplorer::dir_up_slot);
layout->addWidget(nav_bar_);
// Set up stacked widget
stacked_widget_ = new QStackedWidget(this);
layout->addWidget(stacked_widget_);
// Set up sort filter proxy model
sort_model_.setSourceModel(&model_);
sort_model_.setFilterCaseSensitivity(Qt::CaseInsensitive);
sort_model_.setSortRole(ProjectViewModel::k_inner_text_role);
// Add tree view to stacked widget
tree_view_ = new ProjectExplorerTreeView(stacked_widget_);
tree_view_->setSortingEnabled(true);
tree_view_->sortByColumn(0, Qt::AscendingOrder);
tree_view_->setContextMenuPolicy(Qt::CustomContextMenu);
add_view(tree_view_);
// Add list view to stacked widget
list_view_ = new ProjectExplorerListView(stacked_widget_);
list_view_->setContextMenuPolicy(Qt::CustomContextMenu);
add_view(list_view_);
// Add icon view to stacked widget
icon_view_ = new ProjectExplorerIconView(stacked_widget_);
icon_view_->setContextMenuPolicy(Qt::CustomContextMenu);
add_view(icon_view_);
// Set default view to tree view
set_view_type(ProjectToolbar::tree_view);
// Set default icon size
size_changed_slot(k_project_icon_size_default);
connect(tree_view_, &ProjectExplorerTreeView::customContextMenuRequested,
this, &ProjectExplorer::show_context_menu);
connect(list_view_, &ProjectExplorerListView::customContextMenuRequested,
this, &ProjectExplorer::show_context_menu);
connect(icon_view_, &ProjectExplorerIconView::customContextMenuRequested,
this, &ProjectExplorer::show_context_menu);
update_nav_bar_text();
}
const ProjectToolbar::ViewType &ProjectExplorer::view_type() const
{
return view_type_;
}
void ProjectExplorer::set_view_type(ProjectToolbar::ViewType type)
{
view_type_ = type;
// Set widget based on view type
switch (view_type_) {
case ProjectToolbar::tree_view:
stacked_widget_->setCurrentWidget(tree_view_);
nav_bar_->setVisible(false);
break;
case ProjectToolbar::list_view:
stacked_widget_->setCurrentWidget(list_view_);
nav_bar_->setVisible(true);
break;
case ProjectToolbar::icon_view:
stacked_widget_->setCurrentWidget(icon_view_);
nav_bar_->setVisible(true);
break;
}
}
void ProjectExplorer::edit(Node *item)
{
current_view()->edit(
sort_model_.mapFromSource(model_.create_index_from_item(item)));
}
void ProjectExplorer::add_view(QAbstractItemView *view)
{
view->setModel(&sort_model_);
view->setEditTriggers(QAbstractItemView::SelectedClicked);
connect(view, &QAbstractItemView::doubleClicked, this,
&ProjectExplorer::item_double_clicked_slot);
connect(view->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &ProjectExplorer::view_selection_changed);
connect(view, SIGNAL(double_clicked_empty_area()), this,
SLOT(view_empty_area_double_clicked_slot()));
stacked_widget_->addWidget(view);
}
void ProjectExplorer::browse_to_folder(const QModelIndex &index)
{
// Set appropriate views to this index
icon_view_->setRootIndex(index);
list_view_->setRootIndex(index);
// Set navbar text to folder's name
update_nav_bar_text();
// Set directory up enabled button based on whether we're in root or not
nav_bar_->set_dir_up_enabled(index.isValid());
}
int ProjectExplorer::confirm_item_deletion(Node *item)
{
QMessageBox msgbox(this);
msgbox.setWindowTitle(tr("Confirm Item Deletion"));
msgbox.setIcon(QMessageBox::Warning);
QStringList connected_nodes_names;
foreach (const Node::OutputConnection &connected,
item->output_connections()) {
if (!dynamic_cast<Folder *>(connected.second.node())) {
connected_nodes_names.append(
get_human_readable_node_name(connected.second.node()));
}
}
msgbox.setText(
tr("The item \"%1\" is currently connected to the following nodes:\n\n"
"%2\n\n"
"Are you sure you wish to delete this footage?")
.arg(get_human_readable_node_name(item),
connected_nodes_names.join('\n')));
// Set up buttons
msgbox.addButton(QMessageBox::Yes);
msgbox.addButton(QMessageBox::YesToAll);
msgbox.addButton(QMessageBox::No);
msgbox.addButton(QMessageBox::Cancel);
// Run messagebox
return msgbox.exec();
}
bool ProjectExplorer::delete_items_internal(const QVector<Node *> &selected,
bool &check_if_item_is_in_use,
MultiUndoCommand *command)
{
for (int i = 0; i < selected.size(); i++) {
// Delete sequences first
Node *node = selected.at(i);
bool can_delete_item = true;
if (check_if_item_is_in_use) {
foreach (const Node::OutputConnection &oc,
node->output_connections()) {
Folder *folder_test = dynamic_cast<Folder *>(oc.second.node());
if (!folder_test) {
// This sequence outputs to SOMETHING, confirm the user if they want to delete this
int r = confirm_item_deletion(node);
switch (r) {
case QMessageBox::No:
can_delete_item = false;
break;
case QMessageBox::Cancel:
return false;
case QMessageBox::YesToAll:
check_if_item_is_in_use = false;
break;
}
}
}
}
if (can_delete_item) {
Sequence *sequence = dynamic_cast<Sequence *>(node);
if (sequence &&
Core::instance()->main_window()->is_sequence_open(sequence)) {
command->add_child(new CloseSequenceCommand(sequence));
}
if (node->folder()) {
command->add_child(
new Folder::RemoveElementCommand(node->folder(), node));
}
command->add_child(
new NodeRemoveWithExclusiveDependenciesAndDisconnect(node));
}
}
return true;
}
QString ProjectExplorer::get_human_readable_node_name(Node *node)
{
if (node->get_label().isEmpty()) {
return node->name();
} else {
return tr("%1 (%2)").arg(node->get_label(), node->name());
}
}
void ProjectExplorer::update_nav_bar_text()
{
QString absolute;
Folder *f = static_cast<Folder *>(
sort_model_.mapToSource(list_view_->rootIndex()).internalPointer());
while (f && f != project()->root()) {
absolute.prepend(QStringLiteral("%1 / ").arg(f->get_label()));
f = f->folder();
}
absolute.prepend(QStringLiteral("/ "));
nav_bar_->set_text(absolute);
}
QAbstractItemView *ProjectExplorer::current_view() const
{
return static_cast<QAbstractItemView *>(stacked_widget_->currentWidget());
}
void ProjectExplorer::view_empty_area_double_clicked_slot()
{
emit double_clicked_item(nullptr);
}
void ProjectExplorer::item_double_clicked_slot(const QModelIndex &index)
{
// Retrieve source item from index
Node *i =
static_cast<Node *>(sort_model_.mapToSource(index).internalPointer());
// If the item is a folder, browse to it
if (dynamic_cast<Folder *>(i) &&
(view_type() == ProjectToolbar::list_view ||
view_type() == ProjectToolbar::icon_view)) {
browse_to_folder(index);
}
// Emit a signal
emit double_clicked_item(i);
}
void ProjectExplorer::size_changed_slot(int s)
{
icon_view_->setGridSize(QSize(s, s));
list_view_->setIconSize(QSize(s, s));
}
void ProjectExplorer::dir_up_slot()
{
QModelIndex current_root = icon_view_->rootIndex();
if (current_root.isValid()) {
QModelIndex parent = current_root.parent();
browse_to_folder(parent);
}
}
void ProjectExplorer::rename_selected_item()
{
auto indexes = current_view()->selectionModel()->selectedRows();
if (!indexes.empty()) {
current_view()->edit(indexes.first());
}
}
void ProjectExplorer::set_search_filter(const QString &s)
{
sort_model_.setFilterFixedString(s);
}
void ProjectExplorer::show_context_menu()
{
Menu menu;
Menu new_menu;
context_menu_items_ = selected_items();
if (context_menu_items_.isEmpty()) {
// Items to show if no items are selected
// "New" menu
new_menu.setTitle(tr("&New"));
MenuShared::instance()->add_items_for_new_menu(&new_menu);
menu.addMenu(&new_menu);
// "Import" action
QAction *import_action = menu.addAction(tr("&Import..."));
connect(import_action, &QAction::triggered, Core::instance(),
&Core::dialog_import_show);
} else {
// Actions to add when only one item is selected
if (context_menu_items_.size() == 1) {
Node *context_menu_item = context_menu_items_.first();
if (dynamic_cast<Folder *>(context_menu_item)) {
QAction *open_in_new_tab =
menu.addAction(tr("Open in New Tab"));
connect(open_in_new_tab, &QAction::triggered, this,
&ProjectExplorer::open_context_menu_item_in_new_tab);
QAction *open_in_new_window =
menu.addAction(tr("Open in New Window"));
connect(open_in_new_window, &QAction::triggered, this,
&ProjectExplorer::open_context_menu_item_in_new_window);
} else if (dynamic_cast<Footage *>(context_menu_item)) {
QString reveal_text;
#if defined(Q_OS_WINDOWS)
reveal_text = tr("Reveal in Explorer");
#elif defined(Q_OS_MAC)
reveal_text = tr("Reveal in Finder");
#else
reveal_text = tr("Reveal in File Manager");
#endif
QAction *reveal_action = menu.addAction(reveal_text);
connect(reveal_action, &QAction::triggered, this,
&ProjectExplorer::reveal_selected_footage);
QAction *replace_action = menu.addAction(tr("Replace Footage"));
connect(replace_action, &QAction::triggered, this,
&ProjectExplorer::replace_selected_footage);
}
menu.addSeparator();
}
bool all_items_are_footage = true;
bool all_items_have_video_streams = true;
bool all_items_are_footage_or_sequence = true;
foreach (Node *i, context_menu_items_) {
Footage *footage_cast_test = dynamic_cast<Footage *>(i);
Sequence *sequence_cast_test = dynamic_cast<Sequence *>(i);
if (footage_cast_test &&
!footage_cast_test->has_enabled_video_streams()) {
all_items_have_video_streams = false;
}
if (!footage_cast_test) {
all_items_are_footage = false;
}
if (!footage_cast_test && !sequence_cast_test) {
all_items_are_footage_or_sequence = false;
}
}
if (all_items_are_footage && all_items_have_video_streams) {
const QVector<Footage *> proxy_footage =
get_selected_proxy_footage(context_menu_items_);
Menu *proxy_menu = new Menu(tr("Proxy"), &menu);
menu.addMenu(proxy_menu);
QAction *generate_proxy =
proxy_menu->addAction(tr("Generate Proxy"));
generate_proxy->setEnabled(!proxy_footage.isEmpty());
connect(generate_proxy, &QAction::triggered, this,
&ProjectExplorer::generate_proxies_for_selected_footage);
QAction *use_proxy = proxy_menu->addAction(tr("Use Proxy"));
use_proxy->setCheckable(true);
use_proxy->setEnabled(!proxy_footage.isEmpty());
use_proxy->setChecked(
!proxy_footage.isEmpty() &&
std::all_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return footage->proxy_enabled();
}));
connect(use_proxy, &QAction::triggered, this,
&ProjectExplorer::set_selected_footage_proxy_enabled);
QAction *reveal_proxy = proxy_menu->addAction(tr("Reveal Proxy"));
reveal_proxy->setEnabled(
std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(reveal_proxy, &QAction::triggered, this,
&ProjectExplorer::reveal_proxy_for_selected_footage);
QAction *delete_proxy = proxy_menu->addAction(tr("Delete Proxy"));
delete_proxy->setEnabled(
std::any_of(proxy_footage.cbegin(), proxy_footage.cend(),
[](const Footage *footage) {
return !footage->proxy_path().isEmpty();
}));
connect(delete_proxy, &QAction::triggered, this,
&ProjectExplorer::delete_proxies_for_selected_footage);
QAction *proxy_settings =
proxy_menu->addAction(tr("Proxy Settings..."));
connect(proxy_settings, &QAction::triggered, this,
&ProjectExplorer::show_proxy_dialog_for_selected_footage);
}
Q_UNUSED(all_items_are_footage_or_sequence)
if (context_menu_items_.size() == 1) {
menu.addSeparator();
auto rename_action = menu.addAction(tr("Rename"));
connect(rename_action, &QAction::triggered, this,
&ProjectExplorer::rename_selected_item);
}
auto delete_action = menu.addAction(tr("Delete"));
connect(delete_action, &QAction::triggered, this,
&ProjectExplorer::delete_selected);
if (context_menu_items_.size() == 1) {
menu.addSeparator();
QAction *properties_action = menu.addAction(tr("P&roperties"));
connect(properties_action, &QAction::triggered, this,
&ProjectExplorer::show_item_properties_dialog);
}
}
menu.exec(QCursor::pos());
}
void ProjectExplorer::show_item_properties_dialog()
{
Node *sel = context_menu_items_.first();
// FIXME: Support for multiple items
if (dynamic_cast<Footage *>(sel)) {
FootagePropertiesDialog fpd(this, static_cast<Footage *>(sel));
fpd.exec();
} else if (dynamic_cast<Folder *>(sel)) {
Core::instance()->label_nodes(context_menu_items_);
} else if (dynamic_cast<Sequence *>(sel)) {
SequenceDialog sd(static_cast<Sequence *>(sel),
SequenceDialog::k_existing, this);
sd.exec();
}
}
void ProjectExplorer::reveal_selected_footage()
{
Footage *footage = static_cast<Footage *>(context_menu_items_.first());
#if defined(Q_OS_WINDOWS)
// Explorer
QStringList args;
args << "/select," << QDir::toNativeSeparators(footage->filename());
QProcess::startDetached("explorer", args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + footage->filename() + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached("osascript", args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(footage->filename()).dir().absolutePath()));
#endif
}
void ProjectExplorer::replace_selected_footage()
{
Footage *footage = static_cast<Footage *>(context_menu_items_.first());
QString file =
QFileDialog::getOpenFileName(this, tr("Replace Footage"), QString(),
Core::footage_file_dialog_filter());
if (!file.isEmpty()) {
if (!Core::is_footage_extension_allowed(file)) {
QMessageBox::warning(
this, tr("Unsupported media"),
tr("This file type is not allowed by the current media type "
"filter."));
return;
}
// Change the filename through the facade relink (reprobes the new
// file and resets proxy/stream state); the label policy stays here.
OakEngineFootage *facade_handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(footage));
const int relink_rc = oakengine_footage_relink(
facade_handle, file.toUtf8().constData());
oakengine_footage_free(facade_handle);
if (relink_rc != OAKENGINE_OK) {
char err[512];
err[0] = '\0';
oakengine_footage_last_error(err, sizeof(err));
QMessageBox::warning(
this, tr("Cannot replace footage"),
err[0] ? QString::fromUtf8(err) :
tr("The file could not be used as media."));
return;
}
if (QFileInfo(footage->filename()).fileName() ==
footage->get_label()) {
// Footage label == filename, change label too
oakengine_node_set_label(
reinterpret_cast<OakEngineNode *>(footage),
QFileInfo(file).fileName().toUtf8().constData());
}
}
}
void ProjectExplorer::open_context_menu_item_in_new_tab()
{
Core::instance()->main_window()->open_folder(
static_cast<Folder *>(context_menu_items_.first()), false);
}
void ProjectExplorer::open_context_menu_item_in_new_window()
{
Core::instance()->main_window()->open_folder(
static_cast<Folder *>(context_menu_items_.first()), true);
}
void ProjectExplorer::generate_proxies_for_selected_footage()
{
if (!project()) {
qWarning() << "GenerateProxiesForSelectedFootage: no project";
return;
}
const QVector<Footage *> footage =
get_selected_proxy_footage(context_menu_items_);
qDebug()
<< "GenerateProxiesForSelectedFootage: starting proxy generation for"
<< footage.size() << "footage item(s)";
for (Footage *item : footage) {
const VideoParams video = item->get_first_enabled_video_stream();
if (!video.is_valid()) {
qWarning()
<< "GenerateProxiesForSelectedFootage: skipping item with no valid video stream"
<< item->filename();
continue;
}
// Queue one facade-backed task per footage item (same queueing
// semantics as the old per-footage proxy tasks).
oakengine_task_manager_add(
reinterpret_cast<OakEngineTask *>(new FacadeProxyTask(item)));
}
}
void ProjectExplorer::set_selected_footage_proxy_enabled(bool enabled)
{
const QVector<Footage *> footage =
get_selected_proxy_footage(context_menu_items_);
qDebug() << "ProjectExplorer::SetSelectedFootageProxyEnabled:" << enabled
<< "footage count=" << footage.size();
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
qDebug()
<< " skipping item with empty proxy path" << item->filename();
continue;
}
OakEngineFootage *handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(item));
oakengine_footage_proxy_set_enabled(handle, enabled ? 1 : 0);
oakengine_footage_free(handle);
// The facade call toggles the flag; cache invalidation for the UI
// stays here.
item->invalidate_all(Footage::k_filename_input);
}
}
void ProjectExplorer::reveal_proxy_for_selected_footage()
{
const QVector<Footage *> footage =
get_selected_proxy_footage(context_menu_items_);
for (Footage *item : footage) {
char proxy_path[4096];
proxy_path[0] = '\0';
OakEngineFootage *handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(item));
oakengine_footage_proxy_get_path(handle, proxy_path,
sizeof(proxy_path));
oakengine_footage_free(handle);
if (proxy_path[0] == '\0') {
continue;
}
const QString path = QString::fromUtf8(proxy_path);
#if defined(Q_OS_WINDOWS)
QStringList args;
args << "/select," << QDir::toNativeSeparators(path);
QProcess::startDetached(QStringLiteral("explorer"), args);
#elif defined(Q_OS_MAC)
QStringList args;
args << "-e";
args << "tell application \"Finder\"";
args << "-e";
args << "activate";
args << "-e";
args << "select POSIX file \"" + path + "\"";
args << "-e";
args << "end tell";
QProcess::startDetached(QStringLiteral("osascript"), args);
#else
QDesktopServices::openUrl(QUrl::fromLocalFile(
QFileInfo(path).dir().absolutePath()));
#endif
}
}
void ProjectExplorer::delete_proxies_for_selected_footage()
{
const QVector<Footage *> footage =
get_selected_proxy_footage(context_menu_items_);
for (Footage *item : footage) {
if (item->proxy_path().isEmpty()) {
continue;
}
// Facade delete: removes the file, clears the proxy state and
// invalidates the footage.
OakEngineFootage *handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(item));
oakengine_footage_proxy_delete(handle);
oakengine_footage_free(handle);
}
}
void ProjectExplorer::show_proxy_dialog_for_selected_footage()
{
ProxyDialog d(this, get_selected_proxy_footage(context_menu_items_));
d.exec();
}
void ProjectExplorer::view_selection_changed()
{
QItemSelectionModel *model = static_cast<QItemSelectionModel *>(sender());
QModelIndexList selection = model->selectedIndexes();
QVector<Node *> nodes;
foreach (const QModelIndex &index, selection) {
Node *sel = static_cast<Node *>(
sort_model_.mapToSource(index).internalPointer());
if (!nodes.contains(sel)) {
nodes.append(sel);
}
}
if (nodes.isEmpty()) {
nodes.append(get_root());
}
emit selection_changed(nodes);
}
Project *ProjectExplorer::project() const
{
return model_.project();
}
void ProjectExplorer::set_project(Project *p)
{
model_.set_project(p);
}
Folder *ProjectExplorer::get_root() const
{
QModelIndex root_index = sort_model_.mapToSource(tree_view_->rootIndex());
if (!root_index.isValid()) {
return project()->root();
}
return static_cast<Folder *>(root_index.internalPointer());
}
void ProjectExplorer::set_root(Folder *item)
{
QModelIndex index =
sort_model_.mapFromSource(model_.create_index_from_item(item));
browse_to_folder(index);
tree_view_->setRootIndex(index);
}
QVector<Node *> ProjectExplorer::selected_items() const
{
// Determine which view is active and get its selected indexes
QModelIndexList index_list =
current_view()->selectionModel()->selectedRows();
// Convert indexes to item objects
QVector<Node *> selected_items;
for (int i = 0; i < index_list.size(); i++) {
QModelIndex index = sort_model_.mapToSource(index_list.at(i));
Node *item = static_cast<Node *>(index.internalPointer());
selected_items.append(item);
}
return selected_items;
}
Folder *ProjectExplorer::get_selected_folder() const
{
if (project() == nullptr) {
return nullptr;
}
Folder *folder = nullptr;
// Get the selected items from the panel
QVector<Node *> selected_nodes = selected_items();
// Heuristic for finding the selected folder:
//
// - If `folder` is nullptr, we set the first folder we find. Either the item itself if it's a folder, or the
// item's parent.
// - Otherwise, if all folders found are the same, we'll use that to import into.
// - If more than one folder is found, we play it safe and import into the root folder
for (int i = 0; i < selected_nodes.size(); i++) {
Node *sel_item = selected_nodes.at(i);
// If this item is not a folder, presumably it's parent is
if (!dynamic_cast<Folder *>(sel_item)) {
sel_item = sel_item->folder();
}
if (folder == nullptr) {
// If the folder is nullptr, cache it as this folder
folder = static_cast<Folder *>(sel_item);
} else if (folder != sel_item) {
// If not, we've already cached a folder so we check if it's the same
// If it isn't, we "play it safe" and use the root folder
folder = nullptr;
break;
}
}
// If we didn't pick up a folder from the heuristic above for whatever reason, use root
if (folder == nullptr) {
folder = project()->root();
}
return folder;
}
ProjectViewModel *ProjectExplorer::model()
{
return &model_;
}
void ProjectExplorer::select_all()
{
current_view()->selectAll();
}
void ProjectExplorer::deselect_all()
{
current_view()->selectionModel()->clearSelection();
}
void ProjectExplorer::delete_selected()
{
QVector<Node *> selected = selected_items();
if (selected.isEmpty()) {
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
bool check_if_item_is_in_use = true;
if (delete_items_internal(selected, check_if_item_is_in_use, command)) {
Core::instance()->undo_stack()->push(
command, tr("Deleted %1 Item(s)").arg(selected.size()));
} else {
delete command;
}
}
bool ProjectExplorer::select_item(Node *n, bool deselect_all_first)
{
if (deselect_all_first) {
deselect_all();
}
QModelIndex index = model_.create_index_from_item(n);
if (index.isValid()) {
index = sort_model_.mapFromSource(index);
QModelIndex parent = index.parent();
if (view_type() == ProjectToolbar::tree_view) {
// Expand all folders until this index is visible
while (parent.isValid()) {
tree_view_->expand(parent);
parent = parent.parent();
}
} else {
browse_to_folder(parent);
}
current_view()->selectionModel()->select(
index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
return true;
}
return false;
}
}
-208
View File
@@ -1,208 +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_PROJECTEXPLORER_H
#define OAK_PROJECTEXPLORER_H
#include <QSortFilterProxyModel>
#include <QStackedWidget>
#include <QTimer>
#include <QTreeView>
#include "node/project.h"
#include "projectviewmodel.h"
#include "widget/projectexplorer/projectexplorericonview.h"
#include "widget/projectexplorer/projectexplorerlistview.h"
#include "widget/projectexplorer/projectexplorertreeview.h"
#include "widget/projectexplorer/projectexplorernavigation.h"
#include "widget/projecttoolbar/projecttoolbar.h"
namespace olive
{
/**
* @brief A widget for browsing through a Project structure.
*
* ProjectExplorer automatically handles the view<->model system using a ProjectViewModel. Therefore, all that needs to
* be provided is the Project structure itself.
*
* This widget contains three views, tree view, list view, and icon view. These can be switched at any time.
*/
class ProjectExplorer : public QWidget {
Q_OBJECT
public:
ProjectExplorer(QWidget *parent);
const ProjectToolbar::ViewType &view_type() const;
Project *project() const;
void set_project(Project *p);
Folder *get_root() const;
void set_root(Folder *item);
QVector<Node *> selected_items() const;
/**
* @brief Use a heuristic to determine which (if any) folder is selected
*
* Generally for some import/adding processes, we assume that if a folder is selected, the user probably wants to
* create the new object in it rather than in the root. If, however, more than one folder is selected, we can't
* truly determine any folder from this and just return the root instead.
*
* @return
*
* A folder that's heuristically been determined as "selected", or the root directory if none, or nullptr if no
* project is open.
*/
Folder *get_selected_folder() const;
/**
* @brief Access the ViewModel model of the project
*/
ProjectViewModel *model();
void select_all();
void deselect_all();
void delete_selected();
bool select_item(Node *n, bool deselect_all_first = true);
public slots:
void set_view_type(ProjectToolbar::ViewType type);
void edit(Node *item);
void rename_selected_item();
void set_search_filter(const QString &s);
signals:
/**
* @brief Emitted when an Item is double clicked
*
* @param item
*
* The Item that was double clicked, or nullptr if empty area was double clicked
*/
void double_clicked_item(Node *item);
void selection_changed(const QVector<Node *> &selected);
private:
/**
* @brief Get all the blocks that solely rely on an input node
*
* Ignores blocks that depend on multiple inputs
*/
QList<Block *> get_footage_blocks(QList<Node *> nodes);
/**
* @brief Simple convenience function for adding a view to this stacked widget
*
* Mainly for use in the constructor. Adds the view, connects its signals/slots, and sets the model.
*
* @param view
*
* View to add to the stack
*/
void add_view(QAbstractItemView *view);
/**
* @brief Browse to a specific folder index in the model
*
* Only affects list_view_ and icon_view_.
*
* @param index
*
* Either an invalid index to return to the project root, or an index to a valid Folder object.
*/
void browse_to_folder(const QModelIndex &index);
int confirm_item_deletion(Node *item);
bool delete_items_internal(const QVector<Node *> &selected,
bool &check_if_item_is_in_use,
MultiUndoCommand *command);
static QString get_human_readable_node_name(Node *node);
void update_nav_bar_text();
/**
* @brief Get the currently active QAbstractItemView
*/
QAbstractItemView *current_view() const;
QStackedWidget *stacked_widget_;
ProjectExplorerNavigation *nav_bar_;
ProjectExplorerIconView *icon_view_;
ProjectExplorerListView *list_view_;
ProjectExplorerTreeView *tree_view_;
ProjectToolbar::ViewType view_type_;
QSortFilterProxyModel sort_model_;
ProjectViewModel model_;
QVector<Node *> context_menu_items_;
private slots:
void view_empty_area_double_clicked_slot();
void item_double_clicked_slot(const QModelIndex &index);
void size_changed_slot(int s);
void dir_up_slot();
void show_context_menu();
void show_item_properties_dialog();
void reveal_selected_footage();
void replace_selected_footage();
void open_context_menu_item_in_new_tab();
void open_context_menu_item_in_new_window();
void generate_proxies_for_selected_footage();
void set_selected_footage_proxy_enabled(bool enabled);
void reveal_proxy_for_selected_footage();
void delete_proxies_for_selected_footage();
void show_proxy_dialog_for_selected_footage();
void view_selection_changed();
};
}
#endif // OAK_PROJECTEXPLORER_H
@@ -1,35 +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 "projectexplorericonview.h"
namespace olive
{
ProjectExplorerIconView::ProjectExplorerIconView(QWidget *parent)
: ProjectExplorerListViewBase(parent)
{
setViewMode(QListView::IconMode);
setItemDelegate(&delegate_);
}
}
@@ -1,45 +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_PROJECTEXPLORERICONVIEW_H
#define OAK_PROJECTEXPLORERICONVIEW_H
#include "projectexplorerlistviewbase.h"
#include "projectexplorericonviewitemdelegate.h"
namespace olive
{
/**
* @brief The view widget used when ProjectExplorer is in Icon View
*/
class ProjectExplorerIconView : public ProjectExplorerListViewBase {
Q_OBJECT
public:
ProjectExplorerIconView(QWidget *parent);
private:
ProjectExplorerIconViewItemDelegate delegate_;
};
}
#endif // OAK_PROJECTEXPLORERICONVIEW_H
@@ -1,110 +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 "projectexplorericonviewitemdelegate.h"
#include <QPainter>
#include "common/qtutils.h"
namespace olive
{
ProjectExplorerIconViewItemDelegate::ProjectExplorerIconViewItemDelegate(
QObject *parent)
: QStyledItemDelegate(parent)
{
}
QSize ProjectExplorerIconViewItemDelegate::sizeHint(
const QStyleOptionViewItem &option, const QModelIndex &) const
{
Q_UNUSED(option)
return QSize(256, 256);
}
void ProjectExplorerIconViewItemDelegate::paint(
QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QFontMetrics fm = painter->fontMetrics();
QRect img_rect = option.rect;
// Draw Text
if (fm.height() < option.rect.height() / 2) {
img_rect.setHeight(img_rect.height() - fm.height());
QRect text_rect = option.rect;
text_rect.setTop(text_rect.top() + option.rect.height() - fm.height());
QColor text_bgcolor;
QColor text_fgcolor;
if (option.state & QStyle::State_Selected) {
text_bgcolor = option.palette.highlight().color();
text_fgcolor = option.palette.highlightedText().color();
} else {
text_bgcolor = Qt::white;
text_fgcolor = Qt::black;
}
painter->fillRect(text_rect, text_bgcolor);
painter->setPen(text_fgcolor);
QString duration_str = index.data(Qt::UserRole).toString();
int timecode_width = QtUtils::q_font_metrics_width(fm, duration_str);
int max_name_width = option.rect.width();
if (timecode_width < option.rect.width() / 2) {
painter->drawText(
text_rect, static_cast<int>(Qt::AlignBottom | Qt::AlignRight),
index.data(Qt::UserRole).toString());
max_name_width -= timecode_width;
}
painter->drawText(text_rect,
static_cast<int>(Qt::AlignBottom | Qt::AlignLeft),
fm.elidedText(index.data(Qt::DisplayRole).toString(),
Qt::ElideRight, max_name_width));
}
// Draw image
QIcon ico = index.data(Qt::DecorationRole).value<QIcon>();
QSize icon_size = ico.actualSize(img_rect.size());
img_rect =
QRect(img_rect.x() + (img_rect.width() / 2 - icon_size.width() / 2),
img_rect.y() + (img_rect.height() / 2 - icon_size.height() / 2),
icon_size.width(), icon_size.height());
painter->drawPixmap(img_rect, ico.pixmap(icon_size));
if (option.state & QStyle::State_Selected) {
QColor highlight_color = option.palette.highlight().color();
highlight_color.setAlphaF(0.5);
painter->setCompositionMode(QPainter::CompositionMode_SourceAtop);
painter->fillRect(img_rect, highlight_color);
}
}
}
@@ -1,47 +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_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
#define OAK_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
#include <QStyledItemDelegate>
#include "common/define.h"
namespace olive
{
/**
* @brief The delegate that's used to draw items when ProjectExplorer is in Icon view
*/
class ProjectExplorerIconViewItemDelegate : public QStyledItemDelegate {
public:
ProjectExplorerIconViewItemDelegate(QObject *parent = nullptr);
virtual QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
}
#endif // OAK_PROJECTEXPLORERICONVIEWITEMDELEGATE_H
@@ -1,35 +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 "projectexplorerlistview.h"
namespace olive
{
ProjectExplorerListView::ProjectExplorerListView(QWidget *parent)
: ProjectExplorerListViewBase(parent)
{
setViewMode(QListView::ListMode);
setItemDelegate(&delegate_);
}
}
@@ -1,45 +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_PROJECTEXPLORERLISTVIEW_H
#define OAK_PROJECTEXPLORERLISTVIEW_H
#include "projectexplorerlistviewbase.h"
#include "projectexplorerlistviewitemdelegate.h"
namespace olive
{
/**
* @brief The view widget used when ProjectExplorer is in List View
*/
class ProjectExplorerListView : public ProjectExplorerListViewBase {
Q_OBJECT
public:
ProjectExplorerListView(QWidget *parent);
private:
ProjectExplorerListViewItemDelegate delegate_;
};
}
#endif // OAK_PROJECTEXPLORERLISTVIEW_H
@@ -1,59 +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 "projectexplorerlistviewbase.h"
#include <QMouseEvent>
namespace olive
{
ProjectExplorerListViewBase::ProjectExplorerListViewBase(QWidget *parent)
: QListView(parent)
{
// FIXME Is this necessary?
setMovement(QListView::Free);
// Set selection mode (allows multiple item selection)
setSelectionMode(QAbstractItemView::ExtendedSelection);
// Set resize mode
setResizeMode(QListView::Adjust);
// Set widget to emit a signal on right click
setContextMenuPolicy(Qt::CustomContextMenu);
}
void ProjectExplorerListViewBase::mouseDoubleClickEvent(QMouseEvent *event)
{
// Cache here so if the index becomes invalid after the base call, we still know the truth
bool item_at_location = indexAt(event->pos()).isValid();
// Perform default double click functions
QListView::mouseDoubleClickEvent(event);
// QAbstractItemView already has a doubleClicked() signal, but we emit another here for double clicking empty space
if (!item_at_location) {
emit double_clicked_empty_area();
}
}
}
@@ -1,63 +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_PROJECTEXPLORERLISTVIEWBASE_H
#define OAK_PROJECTEXPLORERLISTVIEWBASE_H
#include <QListView>
#include "common/define.h"
namespace olive
{
/**
* @brief A QListView derivative that contains functionality used by both List view and Icon view (which are both based
* on QListView)
*/
class ProjectExplorerListViewBase : public QListView {
Q_OBJECT
public:
ProjectExplorerListViewBase(QWidget *parent);
protected:
/**
* @brief Double click event override
*
* Function that signals DoubleClickedView().
*
* FIXME: This code is the same as the code in ProjectExplorerTreeView. Is there a way to merge these two through
* subclassing?
*/
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
signals:
/**
* @brief Unconditional double click signal
*
* Emits a signal when the view is double clicked but not on any particular item
*/
void double_clicked_empty_area();
};
}
#endif // OAK_PROJECTEXPLORERLISTVIEWBASE_H
@@ -1,91 +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 "projectexplorerlistviewitemdelegate.h"
#include <QPainter>
namespace olive
{
ProjectExplorerListViewItemDelegate::ProjectExplorerListViewItemDelegate(
QObject *parent)
: QStyledItemDelegate(parent)
{
}
QSize ProjectExplorerListViewItemDelegate::sizeHint(
const QStyleOptionViewItem &option, const QModelIndex &) const
{
return QSize(option.decorationSize.height(),
option.decorationSize.height());
}
void ProjectExplorerListViewItemDelegate::paint(
QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QFontMetrics fm = painter->fontMetrics();
QRect img_rect = option.rect;
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
}
img_rect.setWidth(qMin(img_rect.width(), img_rect.height()));
QIcon ico = index.data(Qt::DecorationRole).value<QIcon>();
QSize icon_size = ico.actualSize(img_rect.size());
img_rect =
QRect(img_rect.x() + (img_rect.width() / 2 - icon_size.width() / 2),
img_rect.y() + (img_rect.height() / 2 - icon_size.height() / 2),
icon_size.width(), icon_size.height());
painter->drawPixmap(img_rect, ico.pixmap(icon_size));
QRect text_rect = option.rect;
text_rect.setLeft(text_rect.left() + option.rect.height());
int maximum_line_count = qMax(1, option.rect.height() / fm.height() - 1);
QString text;
if (maximum_line_count == 1) {
text = index.data(Qt::DisplayRole).toString();
} else {
text = index.data(Qt::ToolTipRole).toString();
if (text.isEmpty()) {
text = index.data(Qt::DisplayRole).toString();
} else {
QStringList strings = text.split("\n");
while (strings.size() > maximum_line_count) {
strings.removeLast();
}
text = strings.join("\n");
}
}
painter->setPen(option.state & QStyle::State_Selected ?
option.palette.highlightedText().color() :
option.palette.text().color());
painter->drawText(text_rect,
static_cast<int>(Qt::AlignLeft | Qt::AlignVCenter), text);
}
}
@@ -1,47 +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_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
#define OAK_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
#include <QStyledItemDelegate>
#include "common/define.h"
namespace olive
{
/**
* @brief The delegate that's used to draw items when ProjectExplorer is in List view
*/
class ProjectExplorerListViewItemDelegate : public QStyledItemDelegate {
public:
ProjectExplorerListViewItemDelegate(QObject *parent = nullptr);
virtual QSize sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
};
}
#endif // OAK_PROJECTEXPLORERLISTVIEWITEMDELEGATE_H
@@ -1,103 +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 "projectexplorernavigation.h"
#include <QEvent>
#include <QHBoxLayout>
#include "common/define.h"
#include "ui/icons/icons.h"
namespace olive
{
ProjectExplorerNavigation::ProjectExplorerNavigation(QWidget *parent)
: QWidget(parent)
{
// Create widget layout
QHBoxLayout *layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
// Create "directory up" button
dir_up_btn_ = new QPushButton(this);
dir_up_btn_->setEnabled(false);
dir_up_btn_->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
layout->addWidget(dir_up_btn_);
connect(dir_up_btn_, SIGNAL(clicked(bool)), this,
SIGNAL(directory_up_clicked()));
// Create directory tree label
dir_lbl_ = new QLabel(this);
dir_lbl_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
layout->addWidget(dir_lbl_);
// Create size slider
size_slider_ = new QSlider(this);
size_slider_->setOrientation(Qt::Horizontal);
size_slider_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
layout->addWidget(size_slider_);
connect(size_slider_, SIGNAL(valueChanged(int)), this,
SIGNAL(size_changed(int)));
retranslate();
update_icons();
}
void ProjectExplorerNavigation::set_text(const QString &s)
{
dir_lbl_->setText(s);
}
void ProjectExplorerNavigation::set_dir_up_enabled(bool e)
{
dir_up_btn_->setEnabled(e);
}
void ProjectExplorerNavigation::set_size_value(int s)
{
size_slider_->setValue(s);
}
void ProjectExplorerNavigation::changeEvent(QEvent *e)
{
if (e->type() == QEvent::LanguageChange) {
retranslate();
} else if (e->type() == QEvent::StyleChange) {
update_icons();
}
QWidget::changeEvent(e);
}
void ProjectExplorerNavigation::retranslate()
{
dir_up_btn_->setToolTip(tr("Go to parent folder"));
}
void ProjectExplorerNavigation::update_icons()
{
dir_up_btn_->setIcon(icon::dir_up);
size_slider_->setMinimum(k_project_icon_size_minimum);
size_slider_->setMaximum(k_project_icon_size_maximum);
size_slider_->setValue(k_project_icon_size_default);
}
}
@@ -1,118 +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_PROJECTEXPLORERLISTVIEWTOOLBAR_H
#define OAK_PROJECTEXPLORERLISTVIEWTOOLBAR_H
#include <QLabel>
#include <QPushButton>
#include <QSlider>
#include <QWidget>
#include "common/define.h"
namespace olive
{
/**
* @brief A navigation bar widget for ProjectExplorer's Icon and List views
*
* Unlike the Tree view, Icon and List don't follow a hierarchical view of information. This means there is no direct
* way of navigating in and out of folders in those view types. We solve this in two ways:
*
* * Double clicking a Folder in those views will enter that folder
* * This navigation bar offers a "directory up" button for leaving a folder
*
* This navbar also provides an icon size slider for those views (between kProjectIconSizeMinimum and
* kProjectIconSizeMaximum) as well as text that's intended to be set to the current Folder's name (or
* empty for the root folder).
*
* This widget does not actually communicate to Project or ProjectExplorer classes. It is simply UI widgets that are
* intended to be connected in ways that do. This is the primarily responsibility of ProjectExplorer.
*
* By default, the directory up button is disabled (assuming root folder), the text is empty, and the icon size slider
* is set to kProjectIconSizeDefault.
*/
class ProjectExplorerNavigation : public QWidget {
Q_OBJECT
public:
ProjectExplorerNavigation(QWidget *parent);
/**
* @brief Sets the text string
*
* This text is intended to be set to the current Folder's name
*
* @param s
*/
void set_text(const QString &s);
/**
* @brief Set whether the "directory up" button is enabled or not
*
* @param e
*/
void set_dir_up_enabled(bool e);
/**
* @brief Set the current value of the size slider
*
* NOTE: Does NOT emit SizeChanged().
*
* @param s
*
* New size value to set to
*/
void set_size_value(int s);
signals:
/**
* @brief Signal emitted when the directory up button is clicked
*/
void directory_up_clicked();
/**
* @brief Signal emitted when the icon size slider changes value
*
* @param size
*
* New size set in the slider
*/
void size_changed(int size);
protected:
virtual void changeEvent(QEvent *) override;
private:
void retranslate();
void update_icons();
QPushButton *dir_up_btn_;
QLabel *dir_lbl_;
QSlider *size_slider_;
};
}
#endif // OAK_PROJECTEXPLORERLISTVIEWTOOLBAR_H
@@ -1,59 +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 "projectexplorertreeview.h"
#include <QMouseEvent>
namespace olive
{
ProjectExplorerTreeView::ProjectExplorerTreeView(QWidget *parent)
: QTreeView(parent)
{
// Set selection mode (allows multiple item selection)
setSelectionMode(QAbstractItemView::ExtendedSelection);
// Allow dragging and dropping
setDragDropMode(QAbstractItemView::DragDrop);
// Enable dragging
setDragEnabled(true);
// Allow dropping from external sources
setAcceptDrops(true);
// Set context menu to emit a signal
setContextMenuPolicy(Qt::CustomContextMenu);
}
void ProjectExplorerTreeView::mouseDoubleClickEvent(QMouseEvent *event)
{
// Perform default double click functions
QTreeView::mouseDoubleClickEvent(event);
// QAbstractItemView already has a doubleClicked() signal, but we emit another here for double clicking empty space
if (!indexAt(event->pos()).isValid()) {
emit double_clicked_empty_area();
}
}
}
@@ -1,65 +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_PROJECTEXPLORERTREEVIEW_H
#define OAK_PROJECTEXPLORERTREEVIEW_H
#include <QTreeView>
#include "common/define.h"
namespace olive
{
/**
* @brief The view widget used when ProjectExplorer is in Tree View
*
* A fairly simple subclass of QTreeView that provides a double clicked signal whether the index is valid or not
* (QAbstractItemView has a doubleClicked() signal but it's only emitted with a valid index).
*/
class ProjectExplorerTreeView : public QTreeView {
Q_OBJECT
public:
ProjectExplorerTreeView(QWidget *parent);
protected:
/**
* @brief Double click event override
*
* Function that signals DoubleClickedView().
*
* FIXME: This code is the same as the code in ProjectExplorerListViewBase. Is there a way to merge these two through
*
*/
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
signals:
/**
* @brief Unconditional double click signal
*
* Emits a signal when the view is double clicked but not on any particular item
*/
void double_clicked_empty_area();
};
}
#endif // OAK_PROJECTEXPLORERTREEVIEW_H
@@ -1,32 +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_PROJECTEXPLORERUNDO_H
#define OAK_PROJECTEXPLORERUNDO_H
#include "undo/undocommand.h"
namespace olive
{
}
#endif // OAK_PROJECTEXPLORERUNDO_H
-573
View File
@@ -1,573 +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 "projectviewmodel.h"
#include "ui/icons/icons.h"
#include <QDebug>
#include <QMimeData>
#include <QUrl>
#include "common/qtutils.h"
#include "core.h"
#include "node/nodeundo.h"
namespace olive
{
ProjectViewModel::ProjectViewModel(QObject *parent)
: QAbstractItemModel(parent)
, project_(nullptr)
{
}
Project *ProjectViewModel::project() const
{
return project_;
}
void ProjectViewModel::set_project(Project *p)
{
beginResetModel();
if (project_) {
disconnect_item(project_->root());
}
project_ = p;
if (project_) {
connect_item(project_->root());
}
endResetModel();
}
QModelIndex ProjectViewModel::index(int row, int column,
const QModelIndex &parent) const
{
// I'm actually not 100% sure what this does, but it seems logical and was in the earlier code
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
// Get the parent object, we assume it's a folder since only folders can have children
Folder *item_parent = static_cast<Folder *>(get_item_object_from_index(parent));
// Return an index to this object
return createIndex(row, column, item_parent->item_child(row));
}
QModelIndex ProjectViewModel::parent(const QModelIndex &child) const
{
// Get the Item object from the index
Node *item = get_item_object_from_index(child);
// Get Item's parent object
Folder *par = item->folder();
// If the parent is the root, return an empty index
if (par == project_->root()) {
return QModelIndex();
}
// Otherwise return a true index to its parent
int parent_index = index_of_child(par);
// Make sure the index is valid (there's no reason it shouldn't be)
Q_ASSERT(parent_index > -1);
// Return an index to the parent
return createIndex(parent_index, 0, par);
}
int ProjectViewModel::rowCount(const QModelIndex &parent) const
{
// If there's no project, there are obviously no items to show
if (project_ == nullptr) {
return 0;
}
// If the index is the root, return the root child count
if (parent == QModelIndex()) {
return project_->root()->item_child_count();
}
// Otherwise, the index must contain a valid pointer, so we just return its child count
return static_cast<Folder *>(get_item_object_from_index(parent))
->item_child_count();
}
int ProjectViewModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
// Not strictly necessary, but a decent visual cue that there's no project currently active
if (project_ == nullptr) {
return 0;
}
return k_column_count;
}
QVariant ProjectViewModel::data(const QModelIndex &index, int role) const
{
Node *internal_item = get_item_object_from_index(index);
ColumnType column_type = static_cast<ColumnType>(index.column());
switch (role) {
case Qt::DisplayRole:
case k_inner_text_role: {
// Standard text role
switch (column_type) {
case k_name:
return internal_item->get_label();
case k_duration:
return internal_item->data(Node::duration);
case k_rate:
return internal_item->data(Node::frequency_rate);
case k_last_modified:
case k_created_time: {
qint64 using_time =
(column_type == k_last_modified) ?
internal_item->data(Node::modified_time).toLongLong() :
internal_item->data(Node::created_time).toLongLong();
if (using_time == 0) {
// 0 is the null value, return nothing
break;
}
QVariant ret;
if (role == k_inner_text_role) {
// Use time value directly for correct sorting
ret = using_time;
} else {
// Display role, format to a human readable string
ret = QtUtils::get_formatted_date_time(
QDateTime::fromSecsSinceEpoch(using_time));
}
return ret;
}
case k_column_count:
break;
}
} break;
case Qt::EditRole:
if (column_type == k_name) {
return internal_item->get_label();
}
break;
case Qt::DecorationRole:
// If this is the first column, return the Item's icon
if (column_type == k_name) {
return icon::from_name(internal_item->data(Node::icon).toString());
}
break;
case Qt::ToolTipRole:
return internal_item->data(Node::tooltip);
}
return QVariant();
}
QVariant ProjectViewModel::headerData(int section, Qt::Orientation orientation,
int role) const
{
// Check if we need text data (DisplayRole) and orientation is horizontal
// FIXME I'm not 100% sure what happens if the orientation is vertical/if that check is necessary
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
ColumnType column_type = static_cast<ColumnType>(section);
// Return the name based on the column's current type
switch (column_type) {
case k_name:
return tr("Name");
case k_duration:
return tr("Duration");
case k_rate:
return tr("Rate");
case k_last_modified:
return tr("Modified");
case k_created_time:
return tr("Created");
case k_column_count:
break;
}
}
return QAbstractItemModel::headerData(section, orientation, role);
}
bool ProjectViewModel::hasChildren(const QModelIndex &parent) const
{
// If it's a folder, we always return TRUE in order to always show the "expand triangle" icon,
// even when there are no "physical" children
Node *item = get_item_object_from_index(parent);
return dynamic_cast<Folder *>(item);
}
bool ProjectViewModel::setData(const QModelIndex &index, const QVariant &value,
int role)
{
// The name is editable
if (index.isValid() && index.column() == k_name && role == Qt::EditRole) {
Node *item = get_item_object_from_index(index);
QString new_name = value.toString();
if (!new_name.isEmpty()) {
NodeRenameCommand *nrc = new NodeRenameCommand();
nrc->add_node(item, value.toString());
Core::instance()->undo_stack()->push(
nrc, tr("Renamed Item \"%1\" to \"%2\"")
.arg(item->get_label(), new_name));
return true;
}
}
return false;
}
bool ProjectViewModel::canFetchMore(const QModelIndex &parent) const
{
// Use the same hack that always returns true with folders so the expand triangle is always visible
return hasChildren(parent);
}
Qt::ItemFlags ProjectViewModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
// Allow dropping files from external sources
return Qt::ItemIsDropEnabled;
}
Qt::ItemFlags f = Qt::ItemIsDragEnabled | QAbstractItemModel::flags(index);
if (dynamic_cast<Folder *>(get_item_object_from_index(index))) {
f |= Qt::ItemIsDropEnabled;
}
// If the column is the kName column, that means it's editable
if (index.column() == k_name) {
f |= Qt::ItemIsEditable;
}
return f;
}
QStringList ProjectViewModel::mimeTypes() const
{
// Allow data from this model and a file list from external sources
return { Project::k_item_mime_type, QStringLiteral("text/uri-list") };
}
QMimeData *ProjectViewModel::mimeData(const QModelIndexList &indexes) const
{
// Compliance with Qt standard
if (indexes.isEmpty()) {
return nullptr;
}
// Encode mime data for the rows/items that were dragged
QMimeData *data = new QMimeData();
// Use QDataStream to stream the item data into a byte array
QByteArray encoded_data;
QDataStream stream(&encoded_data, QIODevice::WriteOnly);
// The indexes list includes indexes for each column which we don't use. To make sure each row only gets sent *once*,
// we keep a list of dragged items
QVector<void *> dragged_items;
foreach (QModelIndex index, indexes) {
if (index.isValid()) {
// Check if we've dragged this item before
if (!dragged_items.contains(index.internalPointer())) {
// If not, add it to the stream (and also keep track of it in the vector)
Node *item = static_cast<Node *>(index.internalPointer());
QVector<Track::Reference> streams;
if (ViewerOutput *footage =
dynamic_cast<ViewerOutput *>(item)) {
streams = footage->get_enabled_streams_as_references();
}
stream << streams << reinterpret_cast<quintptr>(item);
dragged_items.append(item);
}
}
}
// Set byte array as the mime data and return the mime data
data->setData(Project::k_item_mime_type, encoded_data);
return data;
}
bool ProjectViewModel::dropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int column,
const QModelIndex &drop)
{
// Default recommended checks from https://doc.qt.io/qt-5/model-view-programming.html#using-drag-and-drop-with-item-views
if (!canDropMimeData(data, action, row, column, drop)) {
return false;
}
if (action == Qt::IgnoreAction) {
return true;
}
// Probe mime data for its format
QStringList mime_formats = data->formats();
if (mime_formats.contains(Project::k_item_mime_type)) {
// Data is drag/drop data from this model
QByteArray model_data = data->data(Project::k_item_mime_type);
// Use QDataStream to deserialize the data
QDataStream stream(&model_data, QIODevice::ReadOnly);
// Get the Item object that the items were dropped on
Folder *drop_location =
dynamic_cast<Folder *>(get_item_object_from_index(drop));
// If this is not a folder, we cannot drop these items here
if (!drop_location) {
return false;
}
// Variables to deserialize into
quintptr item_ptr;
QList<Track::Reference> streams;
// Loop through all data
MultiUndoCommand *move_command = new MultiUndoCommand();
int count = 0;
while (!stream.atEnd()) {
stream >> streams >> item_ptr;
Node *item = reinterpret_cast<Node *>(item_ptr);
// Check if Item is already the drop location or if its parent is the drop location, in which case this is a
// no-op
if (item != drop_location && item->folder() != drop_location &&
(!dynamic_cast<Folder *>(item) ||
!item_is_parent_of_child(static_cast<Folder *>(item),
drop_location))) {
move_command->add_child(new NodeEdgeRemoveCommand(
item,
NodeInput(item->folder(), Folder::k_child_input,
item->folder()->index_of_child_in_array(item))));
move_command->add_child(
new FolderAddChild(drop_location, item));
count++;
}
}
Core::instance()->undo_stack()->push(move_command,
tr("Move %1 Item(s)").arg(count));
return true;
} else if (mime_formats.contains(QStringLiteral("text/uri-list"))) {
// We received a list of files
QByteArray file_data = data->data(QStringLiteral("text/uri-list"));
// Use text stream to parse (just an easy way of sifting through line breaks
QTextStream stream(&file_data);
// Convert QByteArray to QStringList (which Core takes for importing)
QStringList urls;
while (!stream.atEnd()) {
QUrl url = stream.readLine();
if (!url.isEmpty()) {
urls.append(url.toLocalFile());
}
}
// Get folder dropped onto
Node *drop_item = get_item_object_from_index(drop);
// If we didn't drop onto an item, find the nearest parent folder (should eventually terminate at root either way)
if (!dynamic_cast<Folder *>(drop_item)) {
drop_item = drop_item->folder();
if (!drop_item) {
// Failed to find folder to place this in
return false;
}
}
// Trigger an import
Core::instance()->import_files(urls, static_cast<Folder *>(drop_item));
return true;
}
return false;
}
int ProjectViewModel::index_of_child(Node *item) const
{
// Find parent's index within its own parent
Folder *parent = item->folder();
if (parent) {
return parent->index_of_child(item);
}
return -1;
}
Node *ProjectViewModel::get_item_object_from_index(const QModelIndex &index) const
{
if (index.isValid()) {
return static_cast<Node *>(index.internalPointer());
}
return project_ ? project_->root() : nullptr;
}
bool ProjectViewModel::item_is_parent_of_child(Folder *parent, Node *child) const
{
// Loop through parent hierarchy checking if `parent` is one of its parents
do {
child = child->folder();
if (parent == child) {
return true;
}
} while (child != nullptr);
return false;
}
void ProjectViewModel::connect_item(Node *n)
{
connect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed);
Folder *f = dynamic_cast<Folder *>(n);
if (f) {
connect(f, &Folder::begin_insert_item, this,
&ProjectViewModel::folder_begin_insert_item);
connect(f, &Folder::end_insert_item, this,
&ProjectViewModel::folder_end_insert_item);
connect(f, &Folder::begin_remove_item, this,
&ProjectViewModel::folder_begin_remove_item);
connect(f, &Folder::end_remove_item, this,
&ProjectViewModel::folder_end_remove_item);
foreach (Node *c, f->children()) {
connect_item(c);
}
}
}
void ProjectViewModel::disconnect_item(Node *n)
{
disconnect(n, &Node::label_changed, this, &ProjectViewModel::item_renamed);
Folder *f = dynamic_cast<Folder *>(n);
if (f) {
disconnect(f, &Folder::begin_insert_item, this,
&ProjectViewModel::folder_begin_insert_item);
disconnect(f, &Folder::end_insert_item, this,
&ProjectViewModel::folder_end_insert_item);
disconnect(f, &Folder::begin_remove_item, this,
&ProjectViewModel::folder_begin_remove_item);
disconnect(f, &Folder::end_remove_item, this,
&ProjectViewModel::folder_end_remove_item);
foreach (Node *c, f->children()) {
disconnect_item(c);
}
}
}
void ProjectViewModel::folder_begin_insert_item(Node *n, int insert_index)
{
Folder *folder = static_cast<Folder *>(sender());
connect_item(n);
QModelIndex index;
if (folder != project_->root()) {
index = create_index_from_item(folder);
}
beginInsertRows(index, insert_index, insert_index);
}
void ProjectViewModel::folder_end_insert_item()
{
endInsertRows();
}
void ProjectViewModel::folder_begin_remove_item(Node *n, int child_index)
{
Folder *folder = static_cast<Folder *>(sender());
disconnect_item(n);
QModelIndex index;
if (folder != project_->root()) {
index = create_index_from_item(folder);
}
beginRemoveRows(index, child_index, child_index);
}
void ProjectViewModel::folder_end_remove_item()
{
endRemoveRows();
}
void ProjectViewModel::item_renamed()
{
Node *item = static_cast<Node *>(sender());
QModelIndex index = create_index_from_item(item);
emit dataChanged(index, index, { Qt::DisplayRole, Qt::EditRole });
}
QModelIndex ProjectViewModel::create_index_from_item(Node *item, int column)
{
return createIndex(index_of_child(item), column, item);
}
}
-176
View File
@@ -1,176 +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_VIEWMODEL_H
#define OAK_VIEWMODEL_H
#include <QAbstractItemModel>
#include "node/block/block.h"
#include "node/project.h"
#include "undo/undocommand.h"
namespace olive
{
/**
* @brief An adapter that interprets the data in a Project into a Qt item model for usage in ViewModel Views.
*
* Assuming a Project is currently "open" (i.e. the Project is connected to a ProjectExplorer/ProjectPanel through
* a ProjectViewModel), it may be better to make modifications (e.g. additions/removals/renames) through the
* ProjectViewModel so that the views can be efficiently and correctly updated. ProjectViewModel contains several
* "wrapper" functions for Project and Item functions that also signal any connected views to update accordingly.
*/
class ProjectViewModel : public QAbstractItemModel {
Q_OBJECT
public:
enum ColumnType {
/// Media name
k_name,
/// Media duration
k_duration,
/// Media rate (frame rate for video, sample rate for audio)
k_rate,
/// Last modified time (for footage/files)
k_last_modified,
/// Creation time (for footage/files)
k_created_time,
/// Count
k_column_count
};
static const int k_inner_text_role = Qt::UserRole + 1;
/**
* @brief ProjectViewModel Constructor
*
* @param parent
* Parent object for memory handling
*/
ProjectViewModel(QObject *parent);
/**
* @brief Get currently active project
*
* @return
*
* Currently active project or nullptr if there is none
*/
Project *project() const;
/**
* @brief Set the project to adapt
*
* Any views attached to this model will get updated by this function.
*
* @param p
*
* Project to adapt, can be set to nullptr to "close" the project (will show an empty model that cannot be modified)
*/
void set_project(Project *p);
/** Compulsory Qt QAbstractItemModel overrides */
virtual QModelIndex
index(int row, int column,
const QModelIndex &parent = QModelIndex()) const override;
virtual QModelIndex parent(const QModelIndex &child) const override;
virtual int
rowCount(const QModelIndex &parent = QModelIndex()) const override;
virtual int
columnCount(const QModelIndex &parent = QModelIndex()) const override;
virtual QVariant data(const QModelIndex &index,
int role = Qt::DisplayRole) const override;
/** Optional Qt QAbstractItemModel overrides */
virtual QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
virtual bool
hasChildren(const QModelIndex &parent = QModelIndex()) const override;
virtual bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
virtual bool canFetchMore(const QModelIndex &parent) const override;
/** Drag and drop support */
virtual Qt::ItemFlags flags(const QModelIndex &index) const override;
virtual QStringList mimeTypes() const override;
virtual QMimeData *mimeData(const QModelIndexList &indexes) const override;
virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column,
const QModelIndex &parent) override;
/**
* @brief Convenience function for creating QModelIndexes from an Item object
*/
QModelIndex create_index_from_item(Node *item, int column = 0);
private:
/**
* @brief Retrieve the index of `item` in its parent
*
* This function will return the index of a specified item in its parent according to whichever sorting algorithm
* is currently active.
*
* @return
*
* Index of the specified item, or -1 if the item is root (in which case it has no parent).
*/
int index_of_child(Node *item) const;
/**
* @brief Retrieves the Item object from a given index
*
* A convenience function for retrieving Item objects. If the index is not valid, this returns the root Item.
*/
Node *get_item_object_from_index(const QModelIndex &index) const;
/**
* @brief Check if an Item is a parent of a Child
*
* Checks entire "parent hierarchy" of `child` to see if `parent` is one of its parents.
*/
bool item_is_parent_of_child(Folder *parent, Node *child) const;
void connect_item(Node *n);
void disconnect_item(Node *n);
Project *project_;
private slots:
void folder_begin_insert_item(Node *n, int insert_index);
void folder_end_insert_item();
void folder_begin_remove_item(Node *n, int child_index);
void folder_end_remove_item();
void item_renamed();
};
}
#endif // OAK_VIEWMODEL_H
+70 -10
View File
@@ -13,7 +13,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-24.04, macos-15, windows-latest] os: [warp-ubuntu-latest-x64-16x, warp-macos-15-arm64-12x, warp-windows-latest-x64-32x]
env: env:
CMAKE_BUILD_TYPE: Release CMAKE_BUILD_TYPE: Release
CCACHE_DIR: ${{ github.workspace }}/.ccache CCACHE_DIR: ${{ github.workspace }}/.ccache
@@ -39,8 +39,10 @@ jobs:
ninja-build pkg-config nasm \ ninja-build pkg-config nasm \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
ffmpeg \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \ libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev ccache portaudio19-dev libgl1-mesa-dev libgl1-mesa-dri libxkbcommon-dev ccache \
xvfb libvulkan-dev mesa-vulkan-drivers vulkan-tools libshaderc-dev
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# macOS dependencies # macOS dependencies
@@ -133,8 +135,9 @@ jobs:
shell: msys2 {0} shell: msys2 {0}
run: | run: |
echo "OTIO_LOCATION=${PWD}/otio-install" >> "$GITHUB_ENV" echo "OTIO_LOCATION=${PWD}/otio-install" >> "$GITHUB_ENV"
# No rpath on Windows: test executables need the OTIO DLLs on PATH # No rpath on Windows: test executables need the OTIO DLLs on PATH.
echo "$(cygpath -w "${PWD}/otio-install/bin")" >> "$GITHUB_PATH" # OTIO's MinGW install puts the DLLs in lib/, not bin/.
echo "$(cygpath -w "${PWD}/otio-install/lib")" >> "$GITHUB_PATH"
- name: Build OpenTimelineIO (Windows) - name: Build OpenTimelineIO (Windows)
if: runner.os == 'Windows' && steps.otio-cache-win.outputs.cache-hit != 'true' if: runner.os == 'Windows' && steps.otio-cache-win.outputs.cache-hit != 'true'
@@ -172,7 +175,6 @@ jobs:
cmake -S . -B build -G Ninja \ cmake -S . -B build -G Ninja \
-DBUILD_TESTS=ON \ -DBUILD_TESTS=ON \
-DBUILD_QT6=ON \ -DBUILD_QT6=ON \
-DCMAKE_DISABLE_FIND_PACKAGE_Vulkan=ON \
-DOCIO_LOCATION=/usr \ -DOCIO_LOCATION=/usr \
-DOTIO_LOCATION=${OTIO_LOCATION} \ -DOTIO_LOCATION=${OTIO_LOCATION} \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \
@@ -241,6 +243,31 @@ jobs:
echo "OAK_OFX_PLUGIN_PATH=$PLUGIN_DIR" >> "$GITHUB_ENV" echo "OAK_OFX_PLUGIN_PATH=$PLUGIN_DIR" >> "$GITHUB_ENV"
echo "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" >> "$GITHUB_ENV" echo "OAK_OFX_PLUGIN_ID=net.sf.openfx.ChromaKeyerPlugin" >> "$GITHUB_ENV"
# The Windows runners have no GPU: WGL falls back to the GDI software
# OpenGL 1.1 implementation and every render worker dies calling 3.2
# core entry points. Drop Mesa's llvmpipe opengl32.dll next to every
# binary that creates a GL context (the application directory wins the
# DLL search order over System32).
- name: Install Mesa software OpenGL (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
curl -sSL -o /tmp/mesa.7z \
https://github.com/pal1000/mesa-dist-win/releases/download/26.1.5/mesa3d-26.1.5-release-mingw.7z
"/c/Program Files/7-Zip/7z.exe" x /tmp/mesa.7z -o/tmp/mesa > /dev/null
ls -la /tmp/mesa /tmp/mesa/x64
for d in build/engine build/worker build/cli build/tests/gtest build/app; do
if [ -d "$d" ]; then
cp /tmp/mesa/x64/*.dll "$d/"
# Qt ignores an app-local opengl32.dll for WGL (it always binds
# the System32 one); its documented software-GL override is
# opengl32sw.dll, loaded when QT_OPENGL=software.
cp "$d/opengl32.dll" "$d/opengl32sw.dll"
ls "$d"/opengl32sw.dll "$d"/libgallium_wgl.dll
fi
done
echo "QT_OPENGL=software" >> "$GITHUB_ENV"
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Run all tests (including previously environment-gated OFX tests) # Run all tests (including previously environment-gated OFX tests)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -248,7 +275,25 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }} run: xvfb-run -a ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
# Diagnostic: if the Linux test run failed, re-run the suspect gtest
# filter under gdb to capture a native backtrace of the segfault.
# Also break on dlclose to see who unloads the render backend library
# before RenderManager's destructor calls into it.
- name: gtest segfault backtrace (Linux)
if: runner.os == 'Linux' && failure()
env:
QT_QPA_PLATFORM: offscreen
run: |
sudo apt-get update && sudo apt-get install -y gdb
xvfb-run -a gdb -batch \
-ex run \
-ex 'bt full' \
-ex 'info proc mappings' \
-ex 'info symbol $pc' \
--args ./build/tests/gtest/olive-gtest \
--gtest_filter='MainWindow.ConstructsOffscreenWithPanelsAndMenus' || true
- name: Test (macOS) - name: Test (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
@@ -259,6 +304,21 @@ jobs:
shell: msys2 {0} shell: msys2 {0}
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }} run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
# Diagnostic: if the Windows test run failed, show which DLLs the
# engine test executables cannot resolve (0xc0000135).
- name: Missing DLL diagnosis (Windows)
if: runner.os == 'Windows' && failure()
shell: msys2 {0}
run: |
echo "PATH=$PATH"
ldd build/engine/oakengine_ipc_test.exe | grep -i "not found" || true
objdump -p build/engine/oakengine_ipc_test.exe | grep "DLL Name" | sort -u
ls otio-install/lib otio-install/bin 2>/dev/null || true
echo "=== render worker stderr logs ==="
for f in "$TEMP"/oak-render-worker-*.stderr.log "$TMP"/oak-render-worker-*.stderr.log /tmp/oak-render-worker-*.stderr.log; do
[ -f "$f" ] && { echo "--- $f"; tail -50 "$f"; }
done
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Filtered gtest runs for clearer CI output # Filtered gtest runs for clearer CI output
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -266,7 +326,7 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
run: ./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*" run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="PluginSmoke*"
- name: Plugin Smoke Tests (macOS) - name: Plugin Smoke Tests (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
@@ -281,7 +341,7 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
run: ./build/tests/gtest/olive-gtest --gtest_filter="PluginIntegration.*:PluginMisc.*" run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="PluginIntegration.*:PluginMisc.*"
- name: OFX Integration Tests (macOS) - name: OFX Integration Tests (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
@@ -291,7 +351,7 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
run: ./build/tests/gtest/olive-gtest --gtest_filter="AudioSmoke*" run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="AudioSmoke*"
- name: Audio Smoke Tests (macOS) - name: Audio Smoke Tests (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
@@ -306,7 +366,7 @@ jobs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
env: env:
QT_QPA_PLATFORM: offscreen QT_QPA_PLATFORM: offscreen
run: ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*" run: xvfb-run -a ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*"
- name: Viewer Smoke Tests (macOS) - name: Viewer Smoke Tests (macOS)
if: runner.os == 'macOS' if: runner.os == 'macOS'
+16 -6
View File
@@ -255,11 +255,19 @@ endif()
# OTIO's macOS dylibs use @loader_path install names: every binary that # OTIO's macOS dylibs use @loader_path install names: every binary that
# (transitively) links them needs a copy of the dylibs next to itself. # (transitively) links them needs a copy of the dylibs next to itself.
# Windows has no rpath either, so its DLLs must also sit next to each
# executable (relying on PATH is fragile in CI shells).
# Call oak_copy_otio_runtime(<target>) for each executable/shared library. # Call oak_copy_otio_runtime(<target>) for each executable/shared library.
if (OAK_BUNDLE_OTIO AND APPLE AND OTIO_LIBRARY_DIR) if (OAK_BUNDLE_OTIO AND (APPLE OR WIN32) AND OTIO_LIBRARY_DIR)
file(GLOB OAK_OTIO_DYLIBS if (APPLE)
"${OTIO_LIBRARY_DIR}/libopentimelineio*.dylib" file(GLOB OAK_OTIO_DYLIBS
"${OTIO_LIBRARY_DIR}/libopentime*.dylib") "${OTIO_LIBRARY_DIR}/libopentimelineio*.dylib"
"${OTIO_LIBRARY_DIR}/libopentime*.dylib")
else()
file(GLOB OAK_OTIO_DYLIBS
"${OTIO_LIBRARY_DIR}/libopentimelineio*.dll"
"${OTIO_LIBRARY_DIR}/libopentime*.dll")
endif()
function(oak_copy_otio_runtime target) function(oak_copy_otio_runtime target)
add_custom_command(TARGET ${target} POST_BUILD add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
@@ -353,8 +361,10 @@ add_subdirectory(third_party/openfx/HostSupport)
add_subdirectory(engine) add_subdirectory(engine)
add_subdirectory(cli) add_subdirectory(cli)
# Everything above the engine library links against it # Internal consumers (app, tests) link oakengine-obj directly instead of the
list(APPEND OLIVE_LIBRARIES oakengine) # oakengine shared library (see app/CMakeLists.txt). Linking both breaks the
# Windows build: the DLL import library re-defines every symbol already
# provided by the object files (multiple definition errors at link time).
add_subdirectory(app) add_subdirectory(app)
add_subdirectory(worker) add_subdirectory(worker)
+1 -1
View File
@@ -38,7 +38,7 @@ namespace olive {
* Replaces engine's olive::debug_handler so oak-editor doesn't import * Replaces engine's olive::debug_handler so oak-editor doesn't import
* that symbol. Only used in main.cpp's qInstallMessageHandler. * that symbol. Only used in main.cpp's qInstallMessageHandler.
*/ */
static void debug_handler(QtMsgType type, const QMessageLogContext &context, const QString &msg) [[maybe_unused]] [[maybe_unused]] static void debug_handler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{ {
// Suppress noisy warnings from Qt's QXcbIntegration // Suppress noisy warnings from Qt's QXcbIntegration
if (type == QtWarningMsg && msg.contains("QXcbIntegration")) { if (type == QtWarningMsg && msg.contains("QXcbIntegration")) {
+1 -1
View File
@@ -17,7 +17,7 @@
*/ */
#ifndef OAK_HTMLAPP_H #ifndef OAK_HTMLAPP_H
#define OAK_HTML_H #define OAK_HTMLAPP_H
#include <QTextDocument> #include <QTextDocument>
#include <QTextFragment> #include <QTextFragment>
+2 -2
View File
@@ -98,7 +98,7 @@ Core::Core(const OakEngineAppParams *params)
if (params) { if (params) {
oakengine_app_create(params); oakengine_app_create(params);
} else { } else {
static const OakEngineAppParams default_params = {0}; static const OakEngineAppParams default_params = {};
oakengine_app_create(&default_params); oakengine_app_create(&default_params);
} }
@@ -773,7 +773,7 @@ void Core::start_gui(bool full_screen)
main_window_ = new MainWindow(); main_window_ = new MainWindow();
// Route engine notifications to the UI // Route engine notifications to the UI
connect(this, &Core::tool_changed, this, [this](const Tool::Item &) {}); connect(this, &Core::tool_changed, this, [](const Tool::Item &) {});
// Status-bar and lifecycle notifications are handled through the facade // Status-bar and lifecycle notifications are handled through the facade
// (oakengine_app_show_status_message, oakengine_app_clear_status_message) // (oakengine_app_show_status_message, oakengine_app_clear_status_message)
// which the engine forwards through registered callbacks. The main window // which the engine forwards through registered callbacks. The main window
+2 -3
View File
@@ -61,7 +61,7 @@ namespace
// pix_fmt string (e.g. "yuv420p") to its index in the codec's supported // pix_fmt string (e.g. "yuv420p") to its index in the codec's supported
// list; 0 (the codec's preferred format) when absent. // list; 0 (the codec's preferred format) when absent.
int pix_fmt_index(int codec, const QString &pix_fmt) [[maybe_unused]] [[maybe_unused]] int pix_fmt_index(int codec, const QString &pix_fmt)
{ {
if (pix_fmt.isEmpty()) { if (pix_fmt.isEmpty()) {
return 0; return 0;
@@ -87,7 +87,7 @@ Rational export_viewer_length(const OakEngineNode *viewer)
// OakEngineEncodingParams (assembled by the dialog) -> facade POD. One-to-one with // OakEngineEncodingParams (assembled by the dialog) -> facade POD. One-to-one with
// oak_export_options_ex; see oakengine/exporter.h for the field docs. // oak_export_options_ex; see oakengine/exporter.h for the field docs.
oak_export_options_ex params_to_ex(const OakEngineEncodingParams *p) [[maybe_unused]] [[maybe_unused]] oak_export_options_ex params_to_ex(const OakEngineEncodingParams *p)
{ {
oak_export_options_ex o = {}; oak_export_options_ex o = {};
@@ -939,7 +939,6 @@ OakEngineEncodingParams *ExportDialog::generate_params() const
const int vh = static_cast<int>(video_tab_->height_slider()->get_value()); const int vh = static_cast<int>(video_tab_->height_slider()->get_value());
const Rational tb = get_selected_timebase(); const Rational tb = get_selected_timebase();
const int pix_fmt = video_tab_->pixel_format_field()->get_pixel_format(); const int pix_fmt = video_tab_->pixel_format_field()->get_pixel_format();
const int ch_count = oakengine_video_params_internal_channel_count();
const Rational par = video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio(); const Rational par = video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio();
const int interlace = video_tab_->interlaced_combobox()->get_interlace_mode(); const int interlace = video_tab_->interlaced_combobox()->get_interlace_mode();
@@ -145,7 +145,7 @@ void MarkerPropertiesDialog::accept()
// Batch-set properties via facade (one undoable command) // Batch-set properties via facade (one undoable command)
{ {
QVector<OakEngineMarker *> oak_markers; QVector<OakEngineMarker *> oak_markers;
foreach (OakEngineMarker *m, markers_) { for (OakEngineMarker *m : markers_) {
oak_markers.append(m); oak_markers.append(m);
} }
int color = color_menu_->get_selected_color(); int color = color_menu_->get_selected_color();
+9 -5
View File
@@ -228,7 +228,7 @@ int main(int argc, char *argv[])
"main", "Decompress project file (No GUI)")); "main", "Decompress project file (No GUI)"));
#ifdef _WIN32 #ifdef _WIN32
auto console_option = parser.AddOption( auto console_option = parser.add_option(
{ QStringLiteral("c"), QStringLiteral("-console") }, { QStringLiteral("c"), QStringLiteral("-console") },
QCoreApplication::translate("main", "Launch with debug console")); QCoreApplication::translate("main", "Launch with debug console"));
#endif // _WIN32 #endif // _WIN32
@@ -333,15 +333,19 @@ int main(int argc, char *argv[])
// so far. // so far.
// //
// https://bugreports.qt.io/browse/QTBUG-46140 // https://bugreports.qt.io/browse/QTBUG-46140
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); // An explicit QT_OPENGL (e.g. "software" on GPU-less CI runners with
// Mesa deployed as opengl32sw.dll) takes precedence over this default.
if (!qEnvironmentVariableIsSet("QT_OPENGL")) {
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
}
format.setVersion(3, 2); format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile); format.setProfile(QSurfaceFormat::CoreProfile);
format.setDepthBufferSize(24); format.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(format); QSurfaceFormat::setDefaultFormat(format);
// Enable application automatically using higher resolution images from icons // High-DPI pixmaps are always enabled in Qt 6 (AA_UseHighDpiPixmaps is
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); // deprecated and has no effect).
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
@@ -352,7 +356,7 @@ int main(int argc, char *argv[])
#ifdef _WIN32 #ifdef _WIN32
// Since Oak Video Editor is linked with the console subsystem (for better POSIX compatibility), a console // Since Oak Video Editor is linked with the console subsystem (for better POSIX compatibility), a console
// is created by default. If the user didn't request one, we free it here. // is created by default. If the user didn't request one, we free it here.
if (!console_option->IsSet()) { if (!console_option->is_set()) {
FreeConsole(); FreeConsole();
} }
#endif // _WIN32 #endif // _WIN32
+1 -2
View File
@@ -49,8 +49,7 @@ PanelWidget::PanelWidget(const QString &object_name)
View<QWidget>::setFocusPolicy(Qt::ClickFocus); View<QWidget>::setFocusPolicy(Qt::ClickFocus);
connect(this, &PanelWidget::shown, this, connect(this, &PanelWidget::shown, this, [this]() { QWidget::setFocus(); });
reinterpret_cast<void (PanelWidget::*)()>(&PanelWidget::setFocus));
PanelManager::instance()->register_panel(this); PanelManager::instance()->register_panel(this);
} }
+2 -2
View File
@@ -334,7 +334,7 @@ void CurveView::ContextMenuEvent(Menu &m)
void CurveView::SceneRectUpdateEvent(QRectF &r) void CurveView::SceneRectUpdateEvent(QRectF &r)
{ {
double min_val, max_val; double min_val = 0, max_val = 0;
bool got_val = false; bool got_val = false;
foreach (KeyframeViewInputConnection *con, track_connections_) { foreach (KeyframeViewInputConnection *con, track_connections_) {
@@ -694,7 +694,7 @@ void CurveView::zoom_to_fit_internal(bool selected_only)
bool got_val = false; bool got_val = false;
Rational min_time, max_time; Rational min_time, max_time;
double min_val, max_val; double min_val = 0, max_val = 0;
foreach (KeyframeViewInputConnection *con, track_connections_) { foreach (KeyframeViewInputConnection *con, track_connections_) {
foreach (const oak::Keyframe &key, con->get_keyframes()) { foreach (const oak::Keyframe &key, con->get_keyframes()) {
+1 -1
View File
@@ -355,7 +355,7 @@ void CurveWidget::keyframe_type_button_triggered(bool checked)
QVector<int> tracks; QVector<int> tracks;
}; };
QVector<TypeGroup> groups; QVector<TypeGroup> groups;
foreach (OakEngineKeyframe *item, selected) { for (OakEngineKeyframe *item : selected) {
const oak::Keyframe key(item); const oak::Keyframe key(item);
OakEngineNode *node = key.node().handle(); OakEngineNode *node = key.node().handle();
int g = 0; int g = 0;
+17 -13
View File
@@ -64,8 +64,10 @@ bool HandMovableView::hand_press(QMouseEvent *event)
setInteractive(false); setInteractive(false);
// Transform mouse event to act like the left button is pressed // Transform mouse event to act like the left button is pressed
QMouseEvent transformed(event->type(), event->pos(), Qt::LeftButton, QMouseEvent transformed(event->type(), event->position(),
Qt::LeftButton, event->modifiers()); event->globalPosition(), Qt::LeftButton,
Qt::LeftButton, event->modifiers(),
event->pointingDevice());
transformed_pos_ = QPoint(0, 0); transformed_pos_ = QPoint(0, 0);
@@ -83,22 +85,24 @@ bool HandMovableView::hand_move(QMouseEvent *event)
// Transform mouse event to act like the left button is pressed // Transform mouse event to act like the left button is pressed
QPoint adjustment(0, 0); QPoint adjustment(0, 0);
QMouseEvent transformed(event->type(), event->pos() - transformed_pos_, QMouseEvent transformed(event->type(),
Qt::LeftButton, Qt::LeftButton, event->position() - transformed_pos_,
event->modifiers()); event->globalPosition(), Qt::LeftButton,
Qt::LeftButton, event->modifiers(),
event->pointingDevice());
if (event->pos().x() < 0) { if (event->position().toPoint().x() < 0) {
transformed_pos_.setX(transformed_pos_.x() + width()); transformed_pos_.setX(transformed_pos_.x() + width());
adjustment.setX(width()); adjustment.setX(width());
} else if (event->pos().x() >= width()) { } else if (event->position().toPoint().x() >= width()) {
transformed_pos_.setX(transformed_pos_.x() - width()); transformed_pos_.setX(transformed_pos_.x() - width());
adjustment.setX(-width()); adjustment.setX(-width());
} }
if (event->pos().y() < 0) { if (event->position().toPoint().y() < 0) {
transformed_pos_.setY(transformed_pos_.y() + height()); transformed_pos_.setY(transformed_pos_.y() + height());
adjustment.setY(height()); adjustment.setY(height());
} else if (event->pos().y() >= height()) { } else if (event->position().toPoint().y() >= height()) {
transformed_pos_.setY(transformed_pos_.y() - height()); transformed_pos_.setY(transformed_pos_.y() - height());
adjustment.setY(-height()); adjustment.setY(-height());
} }
@@ -116,8 +120,8 @@ bool HandMovableView::hand_release(QMouseEvent *event)
{ {
if (dragging_hand_) { if (dragging_hand_) {
// Transform mouse event to act like the left button is pressed // Transform mouse event to act like the left button is pressed
QMouseEvent transformed(event->type(), event->localPos(), QMouseEvent transformed(event->type(), event->position(),
event->windowPos(), event->screenPos(), event->scenePosition(), event->globalPosition(),
Qt::LeftButton, Qt::LeftButton, Qt::LeftButton, Qt::LeftButton,
event->modifiers(), event->source()); event->modifiers(), event->source());
@@ -194,7 +198,7 @@ void HandMovableView::wheelEvent(QWheelEvent *event)
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
event->position(), event->globalPosition(), event->position(), event->globalPosition(),
#else #else
event->pos(), event->globalPos(), event->position().toPoint(), event->globalPos(),
#endif #endif
event->pixelDelta(), angle_delta, event->buttons(), event->pixelDelta(), angle_delta, event->buttons(),
event->modifiers(), event->phase(), event->inverted(), event->modifiers(), event->phase(), event->inverted(),
@@ -209,7 +213,7 @@ void HandMovableView::wheelEvent(QWheelEvent *event)
Qt::Horizontal; Qt::Horizontal;
} }
QWheelEvent e(event->pos(), event->globalPos(), event->pixelDelta(), QWheelEvent e(event->position().toPoint(), event->globalPos(), event->pixelDelta(),
event->angleDelta(), event->delta(), orientation, event->angleDelta(), event->delta(), orientation,
event->buttons(), event->modifiers()); event->buttons(), event->modifiers());
#endif #endif
+13 -13
View File
@@ -83,7 +83,7 @@ void KeyframeView::delete_selected()
get_selected_keyframes(); get_selected_keyframes();
QVector<OakEngineKeyframe *> keys; QVector<OakEngineKeyframe *> keys;
keys.reserve(int(selected.size())); keys.reserve(int(selected.size()));
foreach (OakEngineKeyframe *key, selected) { for (OakEngineKeyframe *key : selected) {
keys.append(key); keys.append(key);
} }
oakengine_keyframes_remove_many( oakengine_keyframes_remove_many(
@@ -289,9 +289,9 @@ bool KeyframeView::paste(
oakengine_clipboard_foreach_keyframe( oakengine_clipboard_foreach_keyframe(
cb, cb,
[](const char *, OakEngineKeyframe *kf, void *userdata) -> int { [](const char *, OakEngineKeyframe *kf, void *userdata) -> int {
auto *ctx = static_cast<PasteCtx *>(userdata); auto *paste_ctx = static_cast<PasteCtx *>(userdata);
ctx->min = std::min(ctx->min, key_time(kf)); paste_ctx->min = std::min(paste_ctx->min, key_time(kf));
ctx->total++; paste_ctx->total++;
return 0; return 0;
}, },
&ctx); &ctx);
@@ -303,17 +303,17 @@ bool KeyframeView::paste(
cb, cb,
[](const char *node_id, OakEngineKeyframe *kf, [](const char *node_id, OakEngineKeyframe *kf,
void *userdata) -> int { void *userdata) -> int {
auto *ctx = static_cast<PasteCtx *>(userdata); auto *paste_ctx = static_cast<PasteCtx *>(userdata);
auto &find_fn = auto &find_fn =
*static_cast<std::function<oak::Node(const QString &)> *>( *static_cast<std::function<oak::Node(const QString &)> *>(
ctx->find_fn); paste_ctx->find_fn);
oak::Node node_with_id = oak::Node node_with_id =
find_fn(QString::fromUtf8(node_id)); find_fn(QString::fromUtf8(node_id));
if (!node_with_id.is_null()) { if (!node_with_id.is_null()) {
Rational t = key_time(kf) - ctx->min; Rational t = key_time(kf) - paste_ctx->min;
t = ctx->self->get_adjusted_time( t = paste_ctx->self->get_adjusted_time(
ctx->self->get_time_target(), paste_ctx->self->get_time_target(),
node_with_id.handle(), t, node_with_id.handle(), t,
k_transform_towards_input); k_transform_towards_input);
key_set_time_live(kf, t); key_set_time_live(kf, t);
@@ -333,7 +333,7 @@ bool KeyframeView::paste(
void *rm = oakengine_node_remove_keyframe_command( void *rm = oakengine_node_remove_keyframe_command(
existing); existing);
oakengine_undo_command_multi_add_child( oakengine_undo_command_multi_add_child(
ctx->command, rm); paste_ctx->command, rm);
} }
oak_node_value v; oak_node_value v;
@@ -357,7 +357,7 @@ bool KeyframeView::paste(
static_cast<float>(cp_in.y()), static_cast<float>(cp_in.y()),
static_cast<float>(cp_out.x()), static_cast<float>(cp_out.x()),
static_cast<float>(cp_out.y())); static_cast<float>(cp_out.y()));
oakengine_undo_command_multi_add_child(ctx->command, cmd); oakengine_undo_command_multi_add_child(paste_ctx->command, cmd);
} else { } else {
oakengine_keyframe_dispose(kf); oakengine_keyframe_dispose(kf);
} }
@@ -760,7 +760,7 @@ void KeyframeView::show_context_menu()
QVector<int> tracks; QVector<int> tracks;
}; };
QVector<TypeGroup> groups; QVector<TypeGroup> groups;
foreach (OakEngineKeyframe *item, get_selected_keyframes()) { for (OakEngineKeyframe *item : get_selected_keyframes()) {
const oak::Keyframe key(item); const oak::Keyframe key(item);
OakEngineNode *node = key.node().handle(); OakEngineNode *node = key.node().handle();
int g = 0; int g = 0;
@@ -797,7 +797,7 @@ void KeyframeView::show_keyframe_properties_dialog()
if (!get_selected_keyframes().empty()) { if (!get_selected_keyframes().empty()) {
QVector<oak::Keyframe> keys; QVector<oak::Keyframe> keys;
keys.reserve(int(get_selected_keyframes().size())); keys.reserve(int(get_selected_keyframes().size()));
foreach (OakEngineKeyframe *key, get_selected_keyframes()) { for (OakEngineKeyframe *key : get_selected_keyframes()) {
keys.append(oak::Keyframe(key)); keys.append(oak::Keyframe(key));
} }
KeyframePropertiesDialog kd(keys, timebase(), this); KeyframePropertiesDialog kd(keys, timebase(), this);
+2 -2
View File
@@ -50,8 +50,8 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
{ {
const QString backend = oak_query_string([](char *buf, int sz) { const QString backend = oak_query_string([](char *buf, int sz) {
int backend = oakengine_render_manager_requested_backend(); int backend_id = oakengine_render_manager_requested_backend();
return oakengine_render_manager_backend_to_string(backend, buf, return oakengine_render_manager_backend_to_string(backend_id, buf,
sz); sz);
}); });
attached_renderer_ = attached_renderer_ =
+3 -2
View File
@@ -19,6 +19,7 @@
***/ ***/
#include <utility>
#include "menushared.h" #include "menushared.h"
#include <QActionGroup> #include <QActionGroup>
@@ -202,7 +203,7 @@ void MenuShared::add_items_for_edit_menu(Menu *m, bool for_clips)
void MenuShared::add_items_for_addable_objects_menu(Menu *m) void MenuShared::add_items_for_addable_objects_menu(Menu *m)
{ {
for (QAction *a : qAsConst(addable_items_)) { for (QAction *a : std::as_const(addable_items_)) {
a->setChecked((a->data().toInt() == a->setChecked((a->data().toInt() ==
Core::instance()->get_selected_addable_object())); Core::instance()->get_selected_addable_object()));
m->addAction(a); m->addAction(a);
@@ -425,7 +426,7 @@ void MenuShared::retranslate()
edit_split_item_->setText(tr("Split")); edit_split_item_->setText(tr("Split"));
edit_speedduration_item_->setText(tr("Speed/Duration")); edit_speedduration_item_->setText(tr("Speed/Duration"));
for (QAction *a : qAsConst(addable_items_)) { for (QAction *a : std::as_const(addable_items_)) {
a->setText(Tool::get_addable_object_name( a->setText(Tool::get_addable_object_name(
static_cast<Tool::AddableObject>(a->data().toInt()))); static_cast<Tool::AddableObject>(a->data().toInt())));
} }
@@ -43,9 +43,9 @@ NodeParamViewArrayWidget::NodeParamViewArrayWidget(oak::Node node,
bridge_->subscribe(reinterpret_cast<void *>(node_.handle()), bridge_->subscribe(reinterpret_cast<void *>(node_.handle()),
OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED);
connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this, connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this,
[this](OakEngineNode *, const QString &input, int old_size, [this](OakEngineNode *, const QString &input_id, int old_size,
int new_size) { int new_size) {
update_counter(input, old_size, new_size); update_counter(input_id, old_size, new_size);
}); });
update_counter(input_, 0, oak::Input(node_.handle(), input_).array_size()); update_counter(input_, 0, oak::Input(node_.handle(), input_).array_size());
@@ -58,11 +58,11 @@ void NodeParamViewArrayWidget::mouseDoubleClickEvent(QMouseEvent *event)
emit double_clicked(); emit double_clicked();
} }
void NodeParamViewArrayWidget::update_counter(const QString &input, int old_size, void NodeParamViewArrayWidget::update_counter(const QString &changed_input, int old_size,
int new_size) int new_size)
{ {
Q_UNUSED(old_size) Q_UNUSED(old_size)
if (input == input_) { if (changed_input == input_) {
count_lbl_->setText(tr("%n element(s)", nullptr, new_size)); count_lbl_->setText(tr("%n element(s)", nullptr, new_size));
} }
} }
@@ -93,15 +93,15 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(const oak::Input &input
OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED); OAKENGINE_EVENT_NODE_INPUT_DISCONNECTED);
connect(bridge_, &EngineEventBridge::node_input_connected, this, connect(bridge_, &EngineEventBridge::node_input_connected, this,
[this](OakEngineNode *source, OakEngineNode *output, [this](OakEngineNode *source, OakEngineNode *output,
const QString &input, int element) { const QString &input_id, int element) {
input_connected(output, input_connected(output,
oak::Input(source, input, element)); oak::Input(source, input_id, element));
}); });
connect(bridge_, &EngineEventBridge::node_input_disconnected, this, connect(bridge_, &EngineEventBridge::node_input_disconnected, this,
[this](OakEngineNode *source, OakEngineNode *output, [this](OakEngineNode *source, OakEngineNode *output,
const QString &input, int element) { const QString &input_id, int element) {
input_disconnected(output, input_disconnected(output,
oak::Input(source, input, element)); oak::Input(source, input_id, element));
}); });
// Creating the tree is expensive, hold off until the user specifically requests it // Creating the tree is expensive, hold off until the user specifically requests it
@@ -161,8 +161,8 @@ NodeParamViewWidgetBridge::NodeParamViewWidgetBridge(const oak::Input &input,
connect(bridge_, &EngineEventBridge::node_input_value_changed, this, connect(bridge_, &EngineEventBridge::node_input_value_changed, this,
&NodeParamViewWidgetBridge::input_value_changed); &NodeParamViewWidgetBridge::input_value_changed);
connect(bridge_, &EngineEventBridge::node_input_property_changed, this, connect(bridge_, &EngineEventBridge::node_input_property_changed, this,
[this](OakEngineNode *, const QString &input) { [this](OakEngineNode *, const QString &input_id) {
property_changed(input); property_changed(input_id);
}); });
connect(bridge_, &EngineEventBridge::node_input_data_type_changed, this, connect(bridge_, &EngineEventBridge::node_input_data_type_changed, this,
&NodeParamViewWidgetBridge::input_data_type_changed); &NodeParamViewWidgetBridge::input_data_type_changed);
@@ -728,6 +728,7 @@ void NodeParamViewWidgetBridge::widget_callback()
case NodeValueType::k_samples: case NodeValueType::k_samples:
case NodeValueType::k_video_params: case NodeValueType::k_video_params:
case NodeValueType::k_audio_params: case NodeValueType::k_audio_params:
case NodeValueType::k_push_button:
case NodeValueType::k_subtitle_params: case NodeValueType::k_subtitle_params:
case NodeValueType::k_data_type_count: case NodeValueType::k_data_type_count:
break; break;
@@ -946,6 +947,7 @@ void NodeParamViewWidgetBridge::update_widget_values()
case NodeValueType::k_samples: case NodeValueType::k_samples:
case NodeValueType::k_video_params: case NodeValueType::k_video_params:
case NodeValueType::k_audio_params: case NodeValueType::k_audio_params:
case NodeValueType::k_push_button:
case NodeValueType::k_subtitle_params: case NodeValueType::k_subtitle_params:
case NodeValueType::k_data_type_count: case NodeValueType::k_data_type_count:
break; break;
+1 -1
View File
@@ -158,7 +158,7 @@ void NodeTableView::set_time(const Rational &time)
// Set data type name // Set data type name
char name_buf[64]; char name_buf[64];
int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf)); int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf));
if (len > 0 && len < sizeof(name_buf)) { if (len > 0 && len < static_cast<int>(sizeof(name_buf))) {
name_buf[len] = '\0'; name_buf[len] = '\0';
} else { } else {
snprintf(name_buf, sizeof(name_buf), "Type %d", type); snprintf(name_buf, sizeof(name_buf), "Type %d", type);
+1 -1
View File
@@ -106,7 +106,7 @@ void NodeValueTree::set_node(const oak::Input &input, const Rational &time)
setItemWidget(item, 0, radio); setItemWidget(item, 0, radio);
char name_buf[64]; char name_buf[64];
int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf)); int len = oakengine_node_value_pretty_type_name(type, name_buf, sizeof(name_buf));
if (len > 0 && len < sizeof(name_buf)) { if (len > 0 && len < static_cast<int>(sizeof(name_buf))) {
name_buf[len] = '\0'; name_buf[len] = '\0';
} else { } else {
snprintf(name_buf, sizeof(name_buf), "Type %d", type); snprintf(name_buf, sizeof(name_buf), "Type %d", type);
+18 -17
View File
@@ -19,6 +19,7 @@
***/ ***/
#include <utility>
#include "nodeview.h" #include "nodeview.h"
#include <QInputDialog> #include <QInputDialog>
@@ -75,8 +76,8 @@ NodeView::NodeView(QWidget *parent)
, overlay_view_(nullptr) , overlay_view_(nullptr)
, scale_(1.0) , scale_(1.0)
, dont_emit_selection_signals_(false) , dont_emit_selection_signals_(false)
, show_in_param_editor_action_(nullptr)
, bridge_(new EngineEventBridge(this)) , bridge_(new EngineEventBridge(this))
, show_in_param_editor_action_(nullptr)
{ {
setScene(&scene_); setScene(&scene_);
set_default_drag_mode(RubberBandDrag); set_default_drag_mode(RubberBandDrag);
@@ -499,7 +500,7 @@ void NodeView::set_color_label(int index)
// WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper) // WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper)
void *command = oakengine_undo_command_create_multi(); void *command = oakengine_undo_command_create_multi();
for (const oak::Node &node : qAsConst(selected_nodes_)) { for (const oak::Node &node : std::as_const(selected_nodes_)) {
oakengine_undo_command_multi_add_child( oakengine_undo_command_multi_add_child(
command, command,
oakengine_node_set_color_label_command(node.handle(), index)); oakengine_node_set_color_label_command(node.handle(), index));
@@ -528,8 +529,8 @@ void NodeView::keyPressEvent(QKeyEvent *event)
case Qt::Key_Down: { case Qt::Key_Down: {
// WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper) // WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper)
void *pos_command = oakengine_undo_command_create_multi(); void *pos_command = oakengine_undo_command_create_multi();
for (const oak::Node &n : qAsConst(selected_nodes_)) { for (const oak::Node &n : std::as_const(selected_nodes_)) {
for (const oak::Node &context : qAsConst(contexts_)) { for (const oak::Node &context : std::as_const(contexts_)) {
QPointF old_pos; QPointF old_pos;
bool old_expanded = false; bool old_expanded = false;
if (context.context_position_of(n, &old_pos, &old_expanded)) { if (context.context_position_of(n, &old_pos, &old_expanded)) {
@@ -595,7 +596,7 @@ void NodeView::mousePressEvent(QMouseEvent *event)
return; return;
// Get the item that the user clicked on, if any // Get the item that the user clicked on, if any
QGraphicsItem *item = itemAt(event->pos()); QGraphicsItem *item = itemAt(event->position().toPoint());
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
// Sane defaults // Sane defaults
@@ -657,7 +658,7 @@ void NodeView::mousePressEvent(QMouseEvent *event)
scene_.addItem(create_edge_); scene_.addItem(create_edge_);
// Position edge to mouse cursor // Position edge to mouse cursor
position_new_edge(event->pos()); position_new_edge(event->position().toPoint());
return; return;
} }
} }
@@ -698,7 +699,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event)
return; return;
if (create_edge_) { if (create_edge_) {
position_new_edge(event->pos()); position_new_edge(event->position().toPoint());
return; return;
} }
@@ -708,7 +709,7 @@ void NodeView::mouseMoveEvent(QMouseEvent *event)
// See if there are any items attached // See if there are any items attached
if (!attached_items_.isEmpty()) { if (!attached_items_.isEmpty()) {
process_moving_attached_nodes(event->pos()); process_moving_attached_nodes(event->position().toPoint());
} }
} }
void NodeView::mouseReleaseEvent(QMouseEvent *event) void NodeView::mouseReleaseEvent(QMouseEvent *event)
@@ -729,11 +730,11 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event)
bool had_attached_items = !attached_items_.isEmpty(); bool had_attached_items = !attached_items_.isEmpty();
if (!attached_items_.isEmpty()) { if (!attached_items_.isEmpty()) {
select_context = get_context_at_mouse_pos(event->pos()); select_context = get_context_at_mouse_pos(event->position().toPoint());
if (!select_context.is_null()) { if (!select_context.is_null()) {
select_nodes = process_dropping_attached_nodes(command, select_context, select_nodes = process_dropping_attached_nodes(command, select_context,
event->pos()); event->position().toPoint());
} else { } else {
QToolTip::showText(QCursor::pos(), QToolTip::showText(QCursor::pos(),
tr("Nodes must be placed inside a context.")); tr("Nodes must be placed inside a context."));
@@ -780,7 +781,7 @@ void NodeView::mouseDoubleClickEvent(QMouseEvent *event)
if (!(event->modifiers() & Qt::ControlModifier)) { if (!(event->modifiers() & Qt::ControlModifier)) {
NodeViewItem *item_at_cursor = NodeViewItem *item_at_cursor =
dynamic_cast<NodeViewItem *>(itemAt(event->pos())); dynamic_cast<NodeViewItem *>(itemAt(event->position().toPoint()));
if (item_at_cursor) { if (item_at_cursor) {
item_at_cursor->toggle_expanded(); item_at_cursor->toggle_expanded();
} }
@@ -841,9 +842,9 @@ void NodeView::dragMoveEvent(QDragMoveEvent *event)
if (attached_items_.empty()) { if (attached_items_.empty()) {
event->ignore(); event->ignore();
} else { } else {
process_moving_attached_nodes(event->pos()); process_moving_attached_nodes(event->position().toPoint());
if (get_context_at_mouse_pos(event->pos())) { if (get_context_at_mouse_pos(event->position().toPoint())) {
event->accept(); event->accept();
} else { } else {
event->ignore(); event->ignore();
@@ -853,12 +854,12 @@ void NodeView::dragMoveEvent(QDragMoveEvent *event)
void NodeView::dropEvent(QDropEvent *event) void NodeView::dropEvent(QDropEvent *event)
{ {
oak::Node drop_ctx = get_context_at_mouse_pos(event->pos()); oak::Node drop_ctx = get_context_at_mouse_pos(event->position().toPoint());
if (!drop_ctx.is_null()) { if (!drop_ctx.is_null()) {
// WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper) // WRAPPER-GAP: oakengine_undo_* command assembly (no wrapper)
void *command = oakengine_undo_command_create_multi(); void *command = oakengine_undo_command_create_multi();
QVector<oak::Node> select_nodes = QVector<oak::Node> select_nodes =
process_dropping_attached_nodes(command, drop_ctx, event->pos()); process_dropping_attached_nodes(command, drop_ctx, event->position().toPoint());
oakengine_undo_push( oakengine_undo_push(
command, tr("Dropped %1 Node(s)").arg(select_nodes.size()).toUtf8().constData()); command, tr("Dropped %1 Node(s)").arg(select_nodes.size()).toUtf8().constData());
@@ -1192,7 +1193,7 @@ void NodeView::move_attached_nodes_to_cursor(const QPoint &p)
{ {
QPointF item_pos = mapToScene(p); QPointF item_pos = mapToScene(p);
for (const AttachedItem &i : qAsConst(attached_items_)) { for (const AttachedItem &i : std::as_const(attached_items_)) {
if (i.item) { if (i.item) {
i.item->setPos(item_pos + i.original_pos); i.item->setPos(item_pos + i.original_pos);
} }
@@ -1219,7 +1220,7 @@ void NodeView::process_moving_attached_nodes(const QPoint &pos)
NodeViewEdge *new_drop_edge = nullptr; NodeViewEdge *new_drop_edge = nullptr;
// See if there is an edge here // See if there is an edge here
for (QGraphicsItem *item : qAsConst(items)) { for (QGraphicsItem *item : std::as_const(items)) {
new_drop_edge = dynamic_cast<NodeViewEdge *>(item); new_drop_edge = dynamic_cast<NodeViewEdge *>(item);
if (new_drop_edge) { if (new_drop_edge) {
+2 -2
View File
@@ -118,8 +118,8 @@ NodeViewItem::NodeViewItem(oak::Node node, const QString &input,
bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED); bridge_->subscribe(node_.handle(), OAKENGINE_EVENT_NODE_INPUT_ARRAY_SIZE_CHANGED);
connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this, connect(bridge_, &EngineEventBridge::node_input_array_size_changed, this,
[this](OakEngineNode *, const QString &input, int, int) { [this](OakEngineNode *, const QString &changed_input, int, int) {
input_array_size_changed(input); input_array_size_changed(changed_input);
}); });
} }
@@ -37,8 +37,8 @@ public:
ChannelLayoutComboBox(QWidget *parent = nullptr) ChannelLayoutComboBox(QWidget *parent = nullptr)
: QComboBox(parent) : QComboBox(parent)
{ {
foreach (const uint64_t &ch_layout, for (const uint64_t &ch_layout :
AudioParams::k_supported_channel_layouts) { AudioParams::k_supported_channel_layouts) {
this->addItem(HumanStrings::channel_layout_to_string(ch_layout), this->addItem(HumanStrings::channel_layout_to_string(ch_layout),
QVariant::fromValue(ch_layout)); QVariant::fromValue(ch_layout));
} }
@@ -55,7 +55,7 @@ public:
} }
clear(); clear();
foreach (const SampleFormat &of, formats) { for (const SampleFormat &of : formats) {
add_format_item(of); add_format_item(of);
} }
@@ -38,7 +38,7 @@ public:
SampleRateComboBox(QWidget *parent = nullptr) SampleRateComboBox(QWidget *parent = nullptr)
: QComboBox(parent) : QComboBox(parent)
{ {
foreach (int sr, AudioParams::k_supported_sample_rates) { for (int sr : AudioParams::k_supported_sample_rates) {
this->addItem(HumanStrings::sample_rate_to_string(sr), sr); this->addItem(HumanStrings::sample_rate_to_string(sr), sr);
} }
} }
+1 -1
View File
@@ -215,7 +215,7 @@ void TimeBasedView::drawForeground(QPainter *painter, const QRectF &rect)
if (snapped_) { if (snapped_) {
painter->setPen(palette().text().color()); painter->setPen(palette().text().color());
foreach (const Rational &r, snap_time_) { for (const Rational &r : snap_time_) {
double x = time_to_scene(r); double x = time_to_scene(r);
painter->drawLine(x, rect.top(), x, rect.height()); painter->drawLine(x, rect.top(), x, rect.height());
@@ -405,7 +405,7 @@ public:
QRectF(rubberband_scene_start_, current).normalized(); QRectF(rubberband_scene_start_, current).normalized();
selected_ = rubberband_preselected_; selected_ = rubberband_preselected_;
foreach (const DrawnObject &kp, drawn_objects_) { for (const DrawnObject &kp : drawn_objects_) {
if (scene_rect.intersects(kp.second)) { if (scene_rect.intersects(kp.second)) {
select(kp.first); select(kp.first);
} }
+4 -3
View File
@@ -19,6 +19,7 @@
***/ ***/
#include <utility>
#include "timebasedwidget.h" #include "timebasedwidget.h"
#include <QInputDialog> #include <QInputDialog>
@@ -48,8 +49,8 @@ TimeBasedWidget::TimeBasedWidget(bool ruler_text_visible,
bool ruler_cache_status_visible, bool ruler_cache_status_visible,
QWidget *parent) QWidget *parent)
: TimelineScaledWidget(parent) : TimelineScaledWidget(parent)
, viewer_node_(nullptr)
, bridge_(new EngineEventBridge(this)) , bridge_(new EngineEventBridge(this))
, viewer_node_(nullptr)
, auto_max_scrollbar_(false) , auto_max_scrollbar_(false)
, toggle_show_all_(false) , toggle_show_all_(false)
, auto_set_timebase_(true) , auto_set_timebase_(true)
@@ -408,7 +409,7 @@ void TimeBasedWidget::connect_timeline_view(TimeBasedView *base)
&QScrollBar::setValue); &QScrollBar::setValue);
// Connect scrollbar to other scrollbars // Connect scrollbar to other scrollbars
for (TimeBasedView *other : qAsConst(timeline_views_)) { for (TimeBasedView *other : std::as_const(timeline_views_)) {
connect(other->horizontalScrollBar(), &QScrollBar::valueChanged, connect(other->horizontalScrollBar(), &QScrollBar::valueChanged,
base->horizontalScrollBar(), &QScrollBar::setValue); base->horizontalScrollBar(), &QScrollBar::setValue);
connect(base->horizontalScrollBar(), &QScrollBar::valueChanged, connect(base->horizontalScrollBar(), &QScrollBar::valueChanged,
@@ -1229,7 +1230,7 @@ bool TimeBasedWidget::snap_point(const std::vector<Rational> &start_times,
// Find all points at this movement // Find all points at this movement
std::vector<Rational> snap_times; std::vector<Rational> snap_times;
foreach (const SnapData &d, potential_snaps) { for (const SnapData &d : potential_snaps) {
if (d.movement == *movement) { if (d.movement == *movement) {
snap_times.push_back(d.time); snap_times.push_back(d.time);
} }
+4 -3
View File
@@ -19,6 +19,7 @@
***/ ***/
#include <utility>
#include "timelinewidget.h" #include "timelinewidget.h"
#include "timelinewidgetwaveformsync.h" #include "timelinewidgetwaveformsync.h"
@@ -2715,7 +2716,7 @@ void TimelineWidget::cache_clips_in_out()
const TimeRange r(Rational(int(wa.in_num), int(wa.in_den)), const TimeRange r(Rational(int(wa.in_num), int(wa.in_den)),
Rational(int(wa.out_num), int(wa.out_den))); Rational(int(wa.out_num), int(wa.out_den)));
for (OakEngineBlock *b : qAsConst(selected_blocks_)) { for (OakEngineBlock *b : std::as_const(selected_blocks_)) {
if (OakEngineBlock *clip = block_as_clip(b)) { if (OakEngineBlock *clip = block_as_clip(b)) {
if (OakEngineNode *connected = clip_connected_node(clip)) { if (OakEngineNode *connected = clip_connected_node(clip)) {
TimeRange adjusted = TimeRange adjusted =
@@ -2751,7 +2752,7 @@ void TimelineWidget::multicam_enabled_triggered(bool e)
{ {
void *command = oakengine_undo_command_create_multi(); void *command = oakengine_undo_command_create_multi();
for (OakEngineBlock *b : qAsConst(selected_blocks_)) { for (OakEngineBlock *b : std::as_const(selected_blocks_)) {
if (OakEngineBlock *c = block_as_clip(b)) { if (OakEngineBlock *c = block_as_clip(b)) {
OakEngineNode *viewer = oakengine_clip_get_connected_viewer(c); OakEngineNode *viewer = oakengine_clip_get_connected_viewer(c);
OakEngineSequence *s = oakengine_node_is_sequence(viewer) ? OakEngineSequence *s = oakengine_node_is_sequence(viewer) ?
@@ -3399,7 +3400,7 @@ TimelineWidget::generate_existing_paste_map(void *clipboard)
for (int i = 0; i < node_count; i++) { for (int i = 0; i < node_count; i++) {
OakEngineNode *n = oakengine_clipboard_get_loaded_node_at(cb, i); OakEngineNode *n = oakengine_clipboard_get_loaded_node_at(cb, i);
for (OakEngineBlock *b : qAsConst(this->selected_blocks_)) { for (OakEngineBlock *b : std::as_const(this->selected_blocks_)) {
// WRAPPER-GAP: no C ABI for Node::get_context_positions(); the // WRAPPER-GAP: no C ABI for Node::get_context_positions(); the
// block's track-owning sequence is the context that matters here // block's track-owning sequence is the context that matters here
// (blocks on a timeline always live in their sequence's context). // (blocks on a timeline always live in their sequence's context).
+2 -2
View File
@@ -575,7 +575,7 @@ void ImportTool::drop_ghosts(bool insert, void *parent_command)
oakengine_node_connect_command( oakengine_node_connect_command(
footage_stream.footage, footage_stream.footage,
reinterpret_cast<OakEngineNode *>(transform), reinterpret_cast<OakEngineNode *>(transform),
QLatin1String(oakengine_transform_texture_input_id()).toUtf8().constData(), oakengine_transform_texture_input_id(),
-1)); -1));
oakengine_undo_command_multi_add_child( oakengine_undo_command_multi_add_child(
command, command,
@@ -607,7 +607,7 @@ void ImportTool::drop_ghosts(bool insert, void *parent_command)
oakengine_node_connect_command( oakengine_node_connect_command(
footage_stream.footage, footage_stream.footage,
reinterpret_cast<OakEngineNode *>(volume_node), reinterpret_cast<OakEngineNode *>(volume_node),
QLatin1String(oakengine_volume_samples_input_id()).toUtf8().constData(), oakengine_volume_samples_input_id(),
-1)); -1));
oakengine_undo_command_multi_add_child( oakengine_undo_command_multi_add_child(
command, command,
+2 -2
View File
@@ -887,7 +887,7 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event)
oakengine_node_connect_command( oakengine_node_connect_command(
reinterpret_cast<OakEngineNode *>(it.value()), reinterpret_cast<OakEngineNode *>(it.value()),
reinterpret_cast<OakEngineNode *>(cp_in_transition), reinterpret_cast<OakEngineNode *>(cp_in_transition),
QLatin1String(oakengine_transition_in_block_input_id()).toUtf8().constData(), oakengine_transition_in_block_input_id(),
-1)); -1));
} }
@@ -900,7 +900,7 @@ void PointerTool::finish_drag(TimelineViewMouseEvent *event)
oakengine_node_connect_command( oakengine_node_connect_command(
reinterpret_cast<OakEngineNode *>(it.value()), reinterpret_cast<OakEngineNode *>(it.value()),
reinterpret_cast<OakEngineNode *>(cp_out_transition), reinterpret_cast<OakEngineNode *>(cp_out_transition),
QLatin1String(oakengine_transition_out_block_input_id()).toUtf8().constData(), oakengine_transition_out_block_input_id(),
-1)); -1));
} }
} }
@@ -198,7 +198,7 @@ void TransitionTool::mouse_release(TimelineViewMouseEvent *event)
oakengine_node_connect_command( oakengine_node_connect_command(
reinterpret_cast<OakEngineNode *>(out_block), reinterpret_cast<OakEngineNode *>(out_block),
transition, transition,
QLatin1String(oakengine_transition_out_block_input_id()).toUtf8().constData(), oakengine_transition_out_block_input_id(),
-1)); -1));
oakengine_undo_command_multi_add_child( oakengine_undo_command_multi_add_child(
@@ -206,7 +206,7 @@ void TransitionTool::mouse_release(TimelineViewMouseEvent *event)
oakengine_node_connect_command( oakengine_node_connect_command(
reinterpret_cast<OakEngineNode *>(in_block), reinterpret_cast<OakEngineNode *>(in_block),
transition, transition,
QLatin1String(oakengine_transition_in_block_input_id()).toUtf8().constData(), oakengine_transition_in_block_input_id(),
-1)); -1));
oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast<void *>(out_block), reinterpret_cast<void *>(transition), -1, -0.5, 0)); oakengine_undo_command_multi_add_child(command, oakengine_node_set_position_command(reinterpret_cast<void *>(out_block), reinterpret_cast<void *>(transition), -1, -0.5, 0));
@@ -263,7 +263,7 @@ TimelineView::TimelineView(Qt::Alignment vertical_alignment, QWidget *parent)
void TimelineView::mousePressEvent(QMouseEvent *event) void TimelineView::mousePressEvent(QMouseEvent *event)
{ {
// If we click on marker, jump to that point in the timeline // If we click on marker, jump to that point in the timeline
QPointF scene_pos = mapToScene(event->pos()); QPointF scene_pos = mapToScene(event->position().toPoint());
for (auto it = clip_marker_rects_.cbegin(); it != clip_marker_rects_.cend(); for (auto it = clip_marker_rects_.cbegin(); it != clip_marker_rects_.cend();
it++) { it++) {
if (it.value().contains(scene_pos)) { if (it.value().contains(scene_pos)) {
@@ -358,7 +358,7 @@ void TimelineView::mouseDoubleClickEvent(QMouseEvent *event)
void TimelineView::dragEnterEvent(QDragEnterEvent *event) void TimelineView::dragEnterEvent(QDragEnterEvent *event)
{ {
TimelineViewMouseEvent timeline_event = CreateMouseEvent( TimelineViewMouseEvent timeline_event = CreateMouseEvent(
event->pos(), Qt::NoButton, event->keyboardModifiers()); event->position().toPoint(), Qt::NoButton, event->modifiers());
timeline_event.set_mime_data(event->mimeData()); timeline_event.set_mime_data(event->mimeData());
timeline_event.SetEvent(event); timeline_event.SetEvent(event);
@@ -369,7 +369,7 @@ void TimelineView::dragEnterEvent(QDragEnterEvent *event)
void TimelineView::dragMoveEvent(QDragMoveEvent *event) void TimelineView::dragMoveEvent(QDragMoveEvent *event)
{ {
TimelineViewMouseEvent timeline_event = CreateMouseEvent( TimelineViewMouseEvent timeline_event = CreateMouseEvent(
event->pos(), Qt::NoButton, event->keyboardModifiers()); event->position().toPoint(), Qt::NoButton, event->modifiers());
timeline_event.set_mime_data(event->mimeData()); timeline_event.set_mime_data(event->mimeData());
timeline_event.SetEvent(event); timeline_event.SetEvent(event);
@@ -385,7 +385,7 @@ void TimelineView::dragLeaveEvent(QDragLeaveEvent *event)
void TimelineView::dropEvent(QDropEvent *event) void TimelineView::dropEvent(QDropEvent *event)
{ {
TimelineViewMouseEvent timeline_event = CreateMouseEvent( TimelineViewMouseEvent timeline_event = CreateMouseEvent(
event->pos(), Qt::NoButton, event->keyboardModifiers()); event->position().toPoint(), Qt::NoButton, event->modifiers());
timeline_event.set_mime_data(event->mimeData()); timeline_event.set_mime_data(event->mimeData());
timeline_event.SetEvent(event); timeline_event.SetEvent(event);
@@ -446,7 +446,7 @@ void TimelineView::drawForeground(QPainter *painter, const QRectF &rect)
static_cast<TrackReference::Type>(connected_track_type_)) { static_cast<TrackReference::Type>(connected_track_type_)) {
int track_index = it.key().index(); int track_index = it.key().index();
foreach (const TimeRange &range, it.value()) { for (const TimeRange &range : it.value()) {
painter->drawRect(time_to_scene(range.in()), painter->drawRect(time_to_scene(range.in()),
get_track_y(track_index), get_track_y(track_index),
time_to_scene(range.length()), time_to_scene(range.length()),
@@ -594,7 +594,7 @@ TimelineCoordinate TimelineView::scene_to_coordinate(const QPointF &pt)
TimelineViewMouseEvent TimelineView::CreateMouseEvent(QMouseEvent *event) TimelineViewMouseEvent TimelineView::CreateMouseEvent(QMouseEvent *event)
{ {
return CreateMouseEvent(event->pos(), event->button(), event->modifiers()); return CreateMouseEvent(event->position().toPoint(), event->button(), event->modifiers());
} }
TimelineViewMouseEvent TimelineViewMouseEvent
+2 -2
View File
@@ -499,8 +499,8 @@ void SeekableWidget::deselect_all_markers()
void SeekableWidget::set_marker_color(int c) void SeekableWidget::set_marker_color(int c)
{ {
QVector<OakEngineMarker *> oak_markers; QVector<OakEngineMarker *> oak_markers;
foreach (OakEngineMarker *marker, for (OakEngineMarker *marker :
selection_manager_.get_selected_objects()) { selection_manager_.get_selected_objects()) {
oak_markers.append(marker); oak_markers.append(marker);
} }
oakengine_marker_set_properties( oakengine_marker_set_properties(
+9 -8
View File
@@ -19,6 +19,7 @@
***/ ***/
#include <utility>
#include "viewer.h" #include "viewer.h"
#include <QDateTime> #include <QDateTime>
@@ -172,11 +173,16 @@ const Rational k_video_playback_interval = Rational(1, 10);
ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent) ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
: super(false, true, parent) : super(false, true, parent)
, overlay_(nullptr)
, info_chip_(nullptr)
, safe_frame_btn_(nullptr)
, overlay_zoom_index_(5)
, playback_speed_(0) , playback_speed_(0)
, color_menu_enabled_(true) , color_menu_enabled_(true)
, time_changed_from_timer_(false) , time_changed_from_timer_(false)
, prequeuing_video_(false) , prequeuing_video_(false)
, prequeuing_audio_(0) , prequeuing_audio_(0)
, audio_processor_(oakengine_audio_processor_create())
, record_armed_(false) , record_armed_(false)
, recording_(false) , recording_(false)
, first_requeue_watcher_(nullptr) , first_requeue_watcher_(nullptr)
@@ -185,11 +191,6 @@ ViewerWidget::ViewerWidget(ViewerDisplayWidget *display, QWidget *parent)
, ignore_scrub_(0) , ignore_scrub_(0)
, multicam_panel_(nullptr) , multicam_panel_(nullptr)
, bridge_(new EngineEventBridge(this)) , bridge_(new EngineEventBridge(this))
, audio_processor_(oakengine_audio_processor_create())
, overlay_(nullptr)
, info_chip_(nullptr)
, safe_frame_btn_(nullptr)
, overlay_zoom_index_(5)
{ {
// Set up main layout // Set up main layout
QVBoxLayout *layout = new QVBoxLayout(this); QVBoxLayout *layout = new QVBoxLayout(this);
@@ -958,13 +959,13 @@ void ViewerWidget::detect_multicam_node(const Rational &time)
const Rational seq_tb = sequence_timebase(seq); const Rational seq_tb = sequence_timebase(seq);
// Prefer selected nodes // Prefer selected nodes
for (OakEngineNode *n : qAsConst(node_view_selected_)) { for (OakEngineNode *n : std::as_const(node_view_selected_)) {
// MultiCam check via facade predicate (replaces // MultiCam check via facade predicate (replaces
// dynamic_cast<MultiCamNode*>) // dynamic_cast<MultiCamNode*>)
if (oakengine_node_is_multicam(n)) { if (oakengine_node_is_multicam(n)) {
multicam = n; multicam = n;
// Found multicam, now try to find corresponding clip from selected timeline blocks // Found multicam, now try to find corresponding clip from selected timeline blocks
for (OakEngineBlock *b : qAsConst(timeline_selected_blocks_)) { for (OakEngineBlock *b : std::as_const(timeline_selected_blocks_)) {
// Clip check via facade predicate (replaces // Clip check via facade predicate (replaces
// dynamic_cast<ClipBlock*>) // dynamic_cast<ClipBlock*>)
if (oakengine_node_is_clip( if (oakengine_node_is_clip(
@@ -983,7 +984,7 @@ void ViewerWidget::detect_multicam_node(const Rational &time)
// Next, prefer multicam from selected block // Next, prefer multicam from selected block
if (!multicam) { if (!multicam) {
for (OakEngineBlock *b : qAsConst(timeline_selected_blocks_)) { for (OakEngineBlock *b : std::as_const(timeline_selected_blocks_)) {
if (block_range_contains(b, seq_tb, time) && if (block_range_contains(b, seq_tb, time) &&
oakengine_node_is_clip( oakengine_node_is_clip(
reinterpret_cast<OakEngineNode *>(b))) { reinterpret_cast<OakEngineNode *>(b))) {
+23 -23
View File
@@ -69,6 +69,7 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent)
, blank_shader_(nullptr) , blank_shader_(nullptr)
, signal_cursor_color_(false) , signal_cursor_color_(false)
, gizmos_(nullptr) , gizmos_(nullptr)
, gizmo_params_(empty_video_params())
, current_gizmo_(nullptr) , current_gizmo_(nullptr)
, gizmo_drag_started_(false) , gizmo_drag_started_(false)
, show_subtitles_(true) , show_subtitles_(true)
@@ -83,7 +84,6 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent)
, add_band_(false) , add_band_(false)
, queue_starved_(false) , queue_starved_(false)
, text_edit_(nullptr) , text_edit_(nullptr)
, gizmo_params_(empty_video_params())
{ {
connect(Core::instance(), &Core::tool_changed, this, connect(Core::instance(), &Core::tool_changed, this,
&ViewerDisplayWidget::tool_changed); &ViewerDisplayWidget::tool_changed);
@@ -1083,7 +1083,7 @@ void ViewerDisplayWidget::open_text_gizmo(void *text, QMouseEvent *event)
// Start text cursor where the user clicked // Start text cursor where the user clicked
if (event) { if (event) {
QPoint click_pos = text_transform_inverted_.map(event->pos()) - QPoint click_pos = text_transform_inverted_.map(event->position().toPoint()) -
text_edit_pos_.toPoint(); text_edit_pos_.toPoint();
text_edit_->setTextCursor(text_edit_->cursorForPosition(click_pos)); text_edit_->setTextCursor(text_edit_->cursorForPosition(click_pos));
} }
@@ -1099,7 +1099,7 @@ bool ViewerDisplayWidget::on_mouse_press(QMouseEvent *event)
{ {
if (is_hand_drag(event)) { if (is_hand_drag(event)) {
// Handle hand drag // Handle hand drag
hand_last_drag_pos_ = event->pos(); hand_last_drag_pos_ = event->position().toPoint();
hand_dragging_ = true; hand_dragging_ = true;
emit hand_drag_started(); emit hand_drag_started();
inner_widget()->setCursor(Qt::ClosedHandCursor); inner_widget()->setCursor(Qt::ClosedHandCursor);
@@ -1115,15 +1115,15 @@ bool ViewerDisplayWidget::on_mouse_press(QMouseEvent *event)
Tool::k_addable_shape || Tool::k_addable_shape ||
Core::instance()->get_selected_addable_object() == Core::instance()->get_selected_addable_object() ==
Tool::k_addable_title)) { Tool::k_addable_title)) {
add_band_start_ = event->pos(); add_band_start_ = event->position().toPoint();
add_band_end_ = add_band_start_; add_band_end_ = add_band_start_;
add_band_ = true; add_band_ = true;
} else if ((current_gizmo_ = try_gizmo_press( } else if ((current_gizmo_ = try_gizmo_press(
gizmo_last_draw_transform_inverted_.map( gizmo_last_draw_transform_inverted_.map(
event->pos())))) { event->position().toPoint())))) {
// Handle gizmo click // Handle gizmo click
gizmo_start_drag_ = event->pos(); gizmo_start_drag_ = event->position().toPoint();
gizmo_last_drag_ = gizmo_start_drag_; gizmo_last_drag_ = gizmo_start_drag_;
const TimeRange gizmo_time = generate_gizmo_time(); const TimeRange gizmo_time = generate_gizmo_time();
oakengine_gizmo_set_globals( oakengine_gizmo_set_globals(
@@ -1132,7 +1132,7 @@ bool ViewerDisplayWidget::on_mouse_press(QMouseEvent *event)
} else { } else {
// Handle standard drag // Handle standard drag
emit drag_started(event->pos()); emit drag_started(event->position().toPoint());
} }
return true; return true;
@@ -1146,10 +1146,10 @@ bool ViewerDisplayWidget::on_mouse_move(QMouseEvent *event)
// Handle hand dragging // Handle hand dragging
if (hand_dragging_) { if (hand_dragging_) {
// Emit movement // Emit movement
emit hand_drag_moved(event->x() - hand_last_drag_pos_.x(), emit hand_drag_moved(event->position().toPoint().x() - hand_last_drag_pos_.x(),
event->y() - hand_last_drag_pos_.y()); event->position().toPoint().y() - hand_last_drag_pos_.y());
hand_last_drag_pos_ = event->pos(); hand_last_drag_pos_ = event->position().toPoint();
return true; return true;
@@ -1157,7 +1157,7 @@ bool ViewerDisplayWidget::on_mouse_move(QMouseEvent *event)
return true; return true;
} else if (add_band_) { } else if (add_band_) {
add_band_end_ = event->pos(); add_band_end_ = event->position().toPoint();
update(); update();
return true; return true;
@@ -1184,11 +1184,11 @@ bool ViewerDisplayWidget::on_mouse_move(QMouseEvent *event)
gizmo_drag_started_ = true; gizmo_drag_started_ = true;
} }
QPointF v = screen_to_scene_point(event->pos()); QPointF v = screen_to_scene_point(event->position().toPoint());
switch (drag_behavior) { switch (drag_behavior) {
case 1: case 1:
v -= screen_to_scene_point(gizmo_last_drag_); v -= screen_to_scene_point(gizmo_last_drag_);
gizmo_last_drag_ = event->pos(); gizmo_last_drag_ = event->position().toPoint();
break; break;
case 2: case 2:
v -= screen_to_scene_point(gizmo_start_drag_); v -= screen_to_scene_point(gizmo_start_drag_);
@@ -1249,7 +1249,7 @@ bool ViewerDisplayWidget::on_mouse_double_click(QMouseEvent *event)
if (text_edit_ && forward_mouse_event_to_text_edit(event)) { if (text_edit_ && forward_mouse_event_to_text_edit(event)) {
return true; return true;
} else if (event->button() == Qt::LeftButton && gizmos_) { } else if (event->button() == Qt::LeftButton && gizmos_) {
QPointF ptr = transform_viewer_space_to_buffer_space(event->pos()); QPointF ptr = transform_viewer_space_to_buffer_space(event->position().toPoint());
OakEngineNode *gizmos_handle = OakEngineNode *gizmos_handle =
gizmos_; gizmos_;
// A text gizmo only exists on a text v3 node, so the node type id is // A text gizmo only exists on a text v3 node, so the node type id is
@@ -1303,7 +1303,7 @@ void ViewerDisplayWidget::emit_color_at_cursor(QMouseEvent *e)
if (texture_) { if (texture_) {
QPointF pixel_pos = QPointF pixel_pos =
generate_display_transform().inverted().map(e->pos()); generate_display_transform().inverted().map(e->position().toPoint());
oak_video_params tp = {}; oak_video_params tp = {};
oakengine_display_texture_get_params(texture_, &tp); oakengine_display_texture_get_params(texture_, &tp);
pixel_pos /= (tp.divider > 0 ? tp.divider : 1); pixel_pos /= (tp.divider > 0 ? tp.divider : 1);
@@ -1458,9 +1458,9 @@ template <typename T> void ViewerDisplayWidget::forward_drag_event_to_text_edit(
if constexpr (std::is_same_v<T, QDragLeaveEvent>) { if constexpr (std::is_same_v<T, QDragLeaveEvent>) {
text_edit_->dragLeaveEvent(e); text_edit_->dragLeaveEvent(e);
} else { } else {
T relay(adjust_pos_by_v_align(get_virtual_pos_for_text_edit(e->pos())).toPoint(), T relay(adjust_pos_by_v_align(get_virtual_pos_for_text_edit(e->position().toPoint())).toPoint(),
e->possibleActions(), e->mimeData(), e->mouseButtons(), e->possibleActions(), e->mimeData(), e->buttons(),
e->keyboardModifiers()); e->modifiers());
if (e->type() == QEvent::DragEnter) { if (e->type() == QEvent::DragEnter) {
text_edit_->dragEnterEvent(static_cast<QDragEnterEvent *>(&relay)); text_edit_->dragEnterEvent(static_cast<QDragEnterEvent *>(&relay));
@@ -1484,12 +1484,12 @@ bool ViewerDisplayWidget::forward_mouse_event_to_text_edit(QMouseEvent *event,
} }
// Transform screen mouse coords to world mouse coords // Transform screen mouse coords to world mouse coords
QPointF local_pos = get_virtual_pos_for_text_edit(event->pos()); QPointF local_pos = get_virtual_pos_for_text_edit(event->position().toPoint());
if (event->type() == QEvent::MouseMove && if (event->type() == QEvent::MouseMove &&
event->buttons() == Qt::NoButton) { event->buttons() == Qt::NoButton) {
QPointF mapped = QPointF mapped =
text_transform_inverted_.map(event->pos()) - text_edit_pos_; text_transform_inverted_.map(event->position().toPoint()) - text_edit_pos_;
if (mapped.x() >= 0 && mapped.y() >= 0 && if (mapped.x() >= 0 && mapped.y() >= 0 &&
mapped.x() < text_edit_->width() && mapped.x() < text_edit_->width() &&
mapped.y() < text_edit_->height()) { mapped.y() < text_edit_->height()) {
@@ -1504,7 +1504,7 @@ bool ViewerDisplayWidget::forward_mouse_event_to_text_edit(QMouseEvent *event,
local_pos.y() < 0 || local_pos.y() >= text_edit_->height()) { local_pos.y() < 0 || local_pos.y() >= text_edit_->height()) {
// Allow clicking other gizmos so the user can resize while the text editor is active // Allow clicking other gizmos so the user can resize while the text editor is active
if ((current_gizmo_ = try_gizmo_press( if ((current_gizmo_ = try_gizmo_press(
gizmo_last_draw_transform_inverted_.map(event->pos())))) { gizmo_last_draw_transform_inverted_.map(event->position().toPoint())))) {
return false; return false;
} else { } else {
close_text_editor(); close_text_editor();
@@ -1515,8 +1515,8 @@ bool ViewerDisplayWidget::forward_mouse_event_to_text_edit(QMouseEvent *event,
local_pos = adjust_pos_by_v_align(local_pos); local_pos = adjust_pos_by_v_align(local_pos);
QMouseEvent derived(event->type(), local_pos, event->windowPos(), QMouseEvent derived(event->type(), local_pos, event->scenePosition(),
event->screenPos(), event->button(), event->buttons(), event->globalPosition(), event->button(), event->buttons(),
event->modifiers(), event->source()); event->modifiers(), event->source());
return forward_event_to_text_edit(&derived); return forward_event_to_text_edit(&derived);
} }
+4 -4
View File
@@ -185,7 +185,7 @@ void ViewerTextEditor::update_tool_bar(ViewerTextEditorToolBar *toolbar,
} }
QString style = f.fontStyleName().toString(); QString style = f.fontStyleName().toString();
QStringList styles = QFontDatabase().styles(family); QStringList styles = QFontDatabase::styles(family);
if (!styles.isEmpty() && (style.isEmpty() || !styles.contains(style))) { if (!styles.isEmpty() && (style.isEmpty() || !styles.contains(style))) {
// There seems to be no better way to find the "regular" style outside of this heuristic. // There seems to be no better way to find the "regular" style outside of this heuristic.
// Feel free to add more if a font isn't working right. // Feel free to add more if a font isn't working right.
@@ -298,8 +298,8 @@ void ViewerTextEditor::apply_style(QTextCharFormat *format,
{ {
// NOTE: Windows appears to require setting weight and italic manually, while macOS and Linux are // NOTE: Windows appears to require setting weight and italic manually, while macOS and Linux are
// perfectly fine with just the style name // perfectly fine with just the style name
format->setFontWeight(QFontDatabase().weight(family, style)); format->setFontWeight(QFontDatabase::weight(family, style));
format->setFontItalic(QFontDatabase().italic(family, style)); format->setFontItalic(QFontDatabase::italic(family, style));
format->setFontStyleName(style); format->setFontStyleName(style);
} }
@@ -594,7 +594,7 @@ void ViewerTextEditorToolBar::update_font_style_list(const QString &family)
style_combo_->blockSignals(true); style_combo_->blockSignals(true);
style_combo_->clear(); style_combo_->clear();
QStringList l = QFontDatabase().styles(family); QStringList l = QFontDatabase::styles(family);
foreach (const QString &style, l) { foreach (const QString &style, l) {
style_combo_->addItem(style); style_combo_->addItem(style);
} }
+6 -6
View File
@@ -164,15 +164,15 @@ MainWindow::~MainWindow()
void MainWindow::load_layout(const SerializedLayoutInfo &info) void MainWindow::load_layout(const SerializedLayoutInfo &info)
{ {
foreach (OakEngineNode *folder, info.open_folders) { for (OakEngineNode *folder : info.open_folders) {
open_folder(folder, true); open_folder(folder, true);
} }
foreach (OakEngineNode *sequence, info.open_sequences) { for (OakEngineNode *sequence : info.open_sequences) {
open_sequence(sequence, info.open_sequences.size() == 1); open_sequence(sequence, info.open_sequences.size() == 1);
} }
foreach (OakEngineNode *viewer, info.open_viewers) { for (OakEngineNode *viewer : info.open_viewers) {
open_node_in_viewer(viewer); open_node_in_viewer(viewer);
} }
@@ -470,15 +470,15 @@ void MainWindow::set_application_progress_status(ProgressStatus status)
#if defined(Q_OS_WINDOWS) #if defined(Q_OS_WINDOWS)
if (taskbar_interface_) { if (taskbar_interface_) {
switch (status) { switch (status) {
case kProgressShow: case k_progress_show:
taskbar_interface_->SetProgressState( taskbar_interface_->SetProgressState(
reinterpret_cast<HWND>(this->winId()), TBPF_NORMAL); reinterpret_cast<HWND>(this->winId()), TBPF_NORMAL);
break; break;
case kProgressNone: case k_progress_none:
taskbar_interface_->SetProgressState( taskbar_interface_->SetProgressState(
reinterpret_cast<HWND>(this->winId()), TBPF_NOPROGRESS); reinterpret_cast<HWND>(this->winId()), TBPF_NOPROGRESS);
break; break;
case kProgressError: case k_progress_error:
taskbar_interface_->SetProgressState( taskbar_interface_->SetProgressState(
reinterpret_cast<HWND>(this->winId()), TBPF_ERROR); reinterpret_cast<HWND>(this->winId()), TBPF_ERROR);
break; break;
+1
View File
@@ -103,6 +103,7 @@ if (BUILD_TESTS)
) )
set_tests_properties(oak_cli_transcode PROPERTIES set_tests_properties(oak_cli_transcode PROPERTIES
SKIP_RETURN_CODE 2 SKIP_RETURN_CODE 2
TIMEOUT 300
) )
add_test(NAME oak_cli_transcode_verify add_test(NAME oak_cli_transcode_verify
COMMAND ${CMAKE_COMMAND} -DOUT=${CMAKE_CURRENT_BINARY_DIR}/oak_cli_transcode.mp4 -P ${CMAKE_CURRENT_SOURCE_DIR}/verify_transcode_mp4.cmake COMMAND ${CMAKE_COMMAND} -DOUT=${CMAKE_CURRENT_BINARY_DIR}/oak_cli_transcode.mp4 -P ${CMAKE_CURRENT_SOURCE_DIR}/verify_transcode_mp4.cmake
+1 -1
View File
@@ -380,7 +380,7 @@ public:
void shift(const Rational &diff) void shift(const Rational &diff)
{ {
for (int i = 0; i < array_.size(); i++) { for (size_t i = 0; i < array_.size(); i++) {
array_[i] += diff; array_[i] += diff;
} }
} }
+280
View File
@@ -0,0 +1,280 @@
# Eliminating EventBridge: Good First Issues
Goal: the app stops receiving engine events via `EngineEventBridge` /
`oakengine_event_subscribe`; the engine becomes a pure library (its internal
Qt signals are for its own use only).
Each issue is independent and can be claimed and shipped separately (do the
two infrastructure ones first — they simplify everything else).
---
## Ground rules
- **Run the tests before submitting, every time**:
```bash
cmake --build cmake-build-debug -j8 && cd cmake-build-debug && ctest -j4
```
Zero build errors and 122/122 tests passing are the definition of done,
plus the manual acceptance checks listed in each issue.
- Only three migration patterns, copy them:
- **A. app-internal Qt signal**: the event is app-initiated (playhead,
edits, undo) — emit an app-internal signal at the origin and re-point
subscribers to it.
- **B. keep async**: truly async events (tasks, caches, audio beats) go
through one dispatcher (issue 0b) that queues everything back to the
GUI thread.
- **C. delete**: the call site already knows — refresh inline and drop
the subscription.
- When done: remove the matching `bridge_->subscribe` /
`oakengine_event_subscribe` calls and check off the item here.
- Architecture background: `docs/zh/plans/eliminate-event-bridge.md`
---
## Infrastructure (do first)
### issue 0a — Fix the audio event ID collision (real bug, half a day)
`engine/include/oakengine/events.h:210`: `AUDIO_MANAGER_OUTPUT_NOTIFY = 141`
collides with `PLAYBACK_CACHE_INVALIDATED = 141` at :213; the case 141 in
events.cpp only performs the PlaybackCache conversion,
`AudioManager::output_notify` is never wired up, and the audio keep-alive
subscription in `viewer.cpp:1406` never actually fires.
In other words: `AUDIO_MANAGER_OUTPUT_NOTIFY` shares ID 141 with
`PLAYBACK_CACHE_INVALIDATED`; the audio notification is never wired up, so
the viewer's audio keep-alive subscription never fires.
- Change: assign OUTPUT_NOTIFY a fresh ID (>=144) and wire up AudioManager
in events.cpp.
- Acceptance: audio keeps playing past 5 seconds on long footage without
cutting out.
- **Required: build + ctest all green (see ground rules).**
### issue 0b — Single async event dispatcher (1 day)
Create `app/asyncengineevents.{h,cpp}` (a QObject held as a Core singleton)
subscribing only to the TASK family (120127),
PLAYBACK_CACHE_VALIDATED/INVALIDATED, FRAME_CACHE_INVALIDATED, and
AUDIO_OUTPUT_PARAMS/NOTIFY. C callbacks just do
`QMetaObject::invokeMethod(this, ..., Qt::QueuedConnection)` and then emit
typed Qt signals.
In other words: one dispatcher subscribing only to the truly async events;
C callbacks just queue to the GUI thread and re-emit typed Qt signals.
- Acceptance: transcode once — the status bar progress still works. All
later (b) migrations must use it instead of their own subscriptions.
- **Required: build + ctest all green.**
### issue 0c — App-internal PlaybackController (1 day)
Create `app/playback/playbackcontroller.{h,cpp}` with a
`playhead_changed(oak::Node viewer, Rational)` signal. Route every
`oakengine_viewer_set_playhead` call site in the app (ViewerWidget playback
loop, timelinewidget, timeruler, multicam, export) through it and emit the
signal there.
In other words: one app-side controller that every
`oakengine_viewer_set_playhead` call site goes through; it re-broadcasts
`playhead_changed` as an app-internal Qt signal.
- Acceptance: all views keep updating during playback/drag/seek (bridge may
coexist until later issues migrate each subscriber).
- **Required: build + ctest all green.**
---
## Playhead migrations (pattern A, about half a day each)
### issue 1 — timebasedview playhead subscription
`app/widget/timebased/timebasedview.cpp:168` (raw C callback). Reconnect to
`PlaybackController::playhead_changed`.
- Acceptance: the ruler playhead line moves during playback; no raw C
subscription remains.
- **Required: build + ctest all green.**
### issue 2 — NodeParamViewWidgetBridge playhead
`app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp:1140` (raw C).
- Acceptance: keyframe-interpolated slider values follow the playhead
during playback.
- **Required: build + ctest all green.**
### issue 3 — NodeParamViewKeyframeControl playhead
`app/widget/nodeparamview/nodeparamviewkeyframecontrol.cpp:222` (raw C).
- Acceptance: prev/next/toggle keyframe buttons have the correct state as
the playhead moves.
- **Required: build + ctest all green.**
### issue 4 — NodeParamViewConnectedLabel playhead
`app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp:131` (raw C).
- Acceptance: the value tree refreshes with the playhead.
- **Required: build + ctest all green.**
### issue 5 — ExportDialog playhead
`app/dialog/export/export.cpp:311` (raw C).
- Acceptance: the export dialog's in/out times stay in sync with the
playhead.
- **Required: build + ctest all green.**
### issue 6 — timelinewidget / viewerdisplay playhead family (bridge subscriptions)
`app/widget/timelinewidget/timelinewidget.cpp` (around :741-743) and the
related viewerdisplay subscriptions. Reconnect to PlaybackController.
- Acceptance: timeline timecode/playhead position display correctly.
- **Required: build + ctest all green.**
---
## Undo & modified migrations (pattern A, about half a day each)
### issue 7 — historywidget UNDO_INDEX_CHANGED
`app/widget/history/historywidget.cpp:33,127` (two raw C callbacks).
Emit an app-internal `undo_index_changed(int)` at Core's undo/redo/push
exit points and reconnect historywidget to it.
- Acceptance: after undo/redo the history list resets its model and selects
the correct row.
- **Required: build + ctest all green.**
### issue 8 — core.cpp PROJECT_MODIFIED_CHANGED
`app/core.cpp:1108` (raw C). The modified flag is driven by the undo stack,
so the app can derive it at push/undo/redo/load; drive `setWindowModified`
from an app-internal signal instead.
- Acceptance: the title bar modified marker is correct after
edit/undo/save.
- **Required: build + ctest all green.**
---
## Structure migrations (patterns C/A, 0.51 day each)
### issue 9 — seekablewidget marker/workarea subscriptions
`app/widget/seekable/seekablewidget.cpp:95-101,146-154`.
Edits go through undo commands: call `viewport()->update()` directly where
the command runs; also refresh after undo (reuse the issue 7 signal).
- Acceptance: the ruler refreshes immediately after adding/removing markers
or changing the workarea; undo is equally correct.
- **Required: build + ctest all green.**
### issue 10 — resizabletimelinescrollbar marker/workarea
`app/widget/resizablescrollbar/resizabletimelinescrollbar.cpp:83-98,124-129`.
- Acceptance: markers render correctly on the scrollbar.
- **Required: build + ctest all green.**
### issue 11 — nodeviewitem label/color/message/array subscriptions
`app/widget/nodeview/nodeviewitem.cpp:85-119`.
label/color edits refresh in place; array size goes through the structure
command call sites; undo is covered by the issue 7 signal.
- Acceptance: node blocks refresh immediately after rename/recolor/array
add/remove.
- **Required: build + ctest all green.**
### issue 12 — NodeParamViewItem / arraywidget / keyframecontrol bridge subscriptions
`app/widget/nodeparamview/nodeparamviewitem.cpp:95-296`,
`nodeparamviewarraywidget.cpp:43-45`, and the keyframe family in
keyframecontrol.
- Acceptance: parameter item label/flags/array/keyframe buttons refresh
correctly with edits and undo.
- **Required: build + ctest all green.**
### issue 13 — nodeparamviewwidgetbridge parameter value subscriptions
`app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp:161-178`.
The slider edit path sets values directly; re-read uniformly after
undo/load.
- Acceptance: slider values are correct after edit, undo, and load.
- **Required: build + ctest all green.**
### issue 14 — NodeParamView group passthrough / context subscriptions
`app/widget/nodeparamview/nodeparamview.cpp:189-205,350-352,871-874`.
- Acceptance: the parameter panel rebuilds correctly when groups open/close
or nodes are added to/removed from contexts.
- **Required: build + ctest all green.**
### issue 15 — nodeviewcontext node/edge add-remove subscriptions (structure core)
`app/widget/nodeview/nodeviewcontext.cpp:99-138,180-183,427-430`.
Additions/removals come from: app edit commands (handled at call sites),
undo (rebuild covered by the issue 7 signal), and project load (add a new
"project load finished" hook — Core broadcasts it after TaskDialog success,
replacing the batch event-driven graph build).
- Acceptance: node/edge add/remove refreshes immediately; undo restores
correctly; reopening a project shows the complete view.
- **Required: build + ctest all green.**
### issue 16 — nodeview NODE_REMOVED_FROM_GRAPH
`app/widget/nodeview/nodeview.cpp:91,1911` and `mainwindow.cpp:58,351`.
Handle at the delete-node command site plus the undo signal.
- Acceptance: after deleting a node its item/ViewerPanel closes; undo
restores it.
- **Required: build + ctest all green.**
### issue 17 — timelinewidget track/block structure subscriptions
`app/widget/timelinewidget/timelinewidget.cpp:665-753,2177-2228`.
block/track add/remove creates/removes items directly at the timeline
command sites; undo and load rebuild uniformly.
- Acceptance: clip/track add/remove refreshes immediately; undo/load is
correct.
- **Required: build + ctest all green.**
### issue 18 — trackviewitem index/muted
`app/widget/timelinewidget/trackview/trackviewitem.cpp:66,117`.
- Acceptance: track move/mute state refreshes immediately.
- **Required: build + ctest all green.**
### issue 19 — projectviewmodel folder/label subscriptions
`app/panel/project/projectviewmodel.cpp:45-61,505-512`.
Import/create-folder/rename go through the command sites; on load
completion do a uniform model reset (reuse the issue 15 hook).
- Acceptance: the project browser refreshes immediately on add/remove/
rename.
- **Required: build + ctest all green.**
### issue 20 — misc leftovers
multicamwidget (:107/114 raw C), manageddisplay (OCIO, :143),
audiowaveformview (:57,76), viewerdisplay (subtitles, :100,264),
panel/timebased (label, :162).
- Acceptance: each UI refreshes with edits.
- **Required: build + ctest all green.**
---
## Async migrations (pattern B, about half a day each, depends on 0b)
### issue 21 — mainstatusbar / taskmanager / taskviewitem / task dialog
`app/window/mainwindow/mainstatusbar.cpp:88-146`,
`app/panel/taskmanager/taskmanager.cpp:41-56`,
`app/widget/taskview/taskviewitem.cpp:82-88`,
`app/dialog/task/task.cpp:47-48`. Reconnect all of them to the dispatcher's
task signals.
- Acceptance: transcode/export task progress bars and the task list update
in real time.
- **Required: build + ctest all green.**
### issue 22 — timeruler cache bar + viewer cache invalidated
`app/widget/timeruler/timeruler.cpp:82-113`,
`app/widget/viewer/viewer.cpp:366-394`.
- Acceptance: the green cache bar grows with background rendering during
playback; edits invalidate the cache bar correctly.
- **Required: build + ctest all green.**
### issue 23 — viewer audio keep-alive (AUDIO_OUTPUT_NOTIFY)
`app/widget/viewer/viewer.cpp:1406` (raw C, currently broken by the ID
collision). Depends on 0a+0b.
- Acceptance: audio keeps playing continuously on long footage.
- **Required: build + ctest all green.**
---
## Final teardown (do last)
### issue 24 — Delete EngineEventBridge
After every issue above is checked off:
`grep -rn "EngineEventBridge\|bridge_->subscribe" app` should find zero
references. Delete `app/engineeventbridge.{h,cpp}` and its CMake entries.
- Acceptance: build passes; full functional regression
(playback/node graph/timeline/params/history/tasks).
- **Required: build + ctest all green + complete manual regression.**
### issue 25 — Narrow the engine event surface
`engine/src/capi/events.cpp` keeps only the async events the dispatcher
needs plus UNDO_INDEX_CHANGED / PROJECT_MODIFIED_CHANGED (the minimal
exception reserved for external writers such as a future AI assistant);
the synchronous event constants in `engine/include/oakengine/events.h` are
deleted or moved to an internal header; the engine's internal Qt signals
stay unchanged.
- Acceptance: ctest all green; nm confirms the C ABI no longer exposes the
synchronous event subscription surface.
- **Required: build + ctest all green.**
+56 -12
View File
@@ -55,6 +55,13 @@ add_library(oakengine-obj OBJECT
add_library(oakengine SHARED $<TARGET_OBJECTS:oakengine-obj>) add_library(oakengine SHARED $<TARGET_OBJECTS:oakengine-obj>)
if (WIN32)
# Windows DLLs cannot have undefined symbols; embed the version objects
# here too (each module keeps its own copy, unlike ELF where the final
# executable provides the definition).
target_sources(oakengine PRIVATE $<TARGET_OBJECTS:olive-version-obj>)
endif ()
if (COMMAND oak_copy_otio_runtime) if (COMMAND oak_copy_otio_runtime)
oak_copy_otio_runtime(oakengine) oak_copy_otio_runtime(oakengine)
endif() endif()
@@ -107,6 +114,9 @@ target_include_directories(oakengine
target_link_libraries(oakengine PUBLIC ${OLIVE_LIBRARIES} OfxHost) target_link_libraries(oakengine PUBLIC ${OLIVE_LIBRARIES} OfxHost)
# OAKENGINE_BUILD marks the library side of the C ABI export macros (dllexport) # OAKENGINE_BUILD marks the library side of the C ABI export macros (dllexport)
target_compile_definitions(oakengine-obj PRIVATE ${OLIVE_DEFINITIONS} OAKENGINE_BUILD) target_compile_definitions(oakengine-obj PRIVATE ${OLIVE_DEFINITIONS} OAKENGINE_BUILD)
# Consumers that link the object library directly (app, tests, Windows oakgl)
# must see plain C declarations, not __declspec(dllimport) ones.
target_compile_definitions(oakengine-obj INTERFACE OAKENGINE_STATIC)
target_compile_options(oakengine-obj PRIVATE ${OLIVE_COMPILE_OPTIONS}) target_compile_options(oakengine-obj PRIVATE ${OLIVE_COMPILE_OPTIONS})
# Version script: only oakengine_* + render-backend plugin ABI are exported; # Version script: only oakengine_* + render-backend plugin ABI are exported;
@@ -158,12 +168,25 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
endif () endif ()
endforeach () endforeach ()
add_library(oakgl SHARED if (WIN32)
render/opengl/openglbackend_c.cpp # Engine-internal C++ symbols are not exported from liboakengine.dll,
render/opengl/openglrenderer.cpp # so embed the object library like the engine tests do. oakengine-obj
render/opengl/openglrenderer.h # already contains openglrenderer.cpp and its moc output, so on
) # Windows the plugin only needs the C entry point (duplicating the
target_link_libraries(oakgl PRIVATE oakengine) # sources would cause duplicate definitions at link time). MinGW
# compares type_info by name, so cross-DLL RTTI still works.
add_library(oakgl SHARED render/opengl/openglbackend_c.cpp)
set_target_properties(oakgl PROPERTIES AUTOMOC OFF)
target_link_libraries(oakgl PRIVATE oakengine-obj
$<TARGET_OBJECTS:olive-version-obj>)
else ()
add_library(oakgl SHARED
render/opengl/openglbackend_c.cpp
render/opengl/openglrenderer.cpp
render/opengl/openglrenderer.h
)
target_link_libraries(oakgl PRIVATE oakengine)
endif ()
if (COMMAND oak_copy_otio_runtime) if (COMMAND oak_copy_otio_runtime)
oak_copy_otio_runtime(oakgl) oak_copy_otio_runtime(oakgl)
endif() endif()
@@ -185,12 +208,27 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
) )
if (Vulkan_FOUND) if (Vulkan_FOUND)
add_library(oakvulkan SHARED if (WIN32)
render/vulkan/vulkanbackend_c.cpp # Engine-internal C++ symbols are not exported from
render/vulkan/vulkanrenderer.cpp # liboakengine.dll; link the object library like the engine tests
render/vulkan/vulkanrenderer.h # (plus version obj for k_app_version). Unlike openglrenderer.cpp,
) # vulkanrenderer.cpp is NOT part of oakengine-obj, so the plugin
target_link_libraries(oakvulkan PRIVATE oakengine) # must compile it itself.
add_library(oakvulkan SHARED
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_link_libraries(oakvulkan PRIVATE oakengine-obj
$<TARGET_OBJECTS:olive-version-obj>)
else ()
add_library(oakvulkan SHARED
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_link_libraries(oakvulkan PRIVATE oakengine)
endif ()
if (COMMAND oak_copy_otio_runtime) if (COMMAND oak_copy_otio_runtime)
oak_copy_otio_runtime(oakvulkan) oak_copy_otio_runtime(oakvulkan)
endif() endif()
@@ -289,6 +327,12 @@ if (BUILD_TESTS)
if (UNIX AND NOT APPLE) if (UNIX AND NOT APPLE)
target_link_options(${name} PRIVATE "LINKER:--export-dynamic") target_link_options(${name} PRIVATE "LINKER:--export-dynamic")
endif () endif ()
if (WIN32)
# The test embeds oakengine-obj; its OAKENGINE_API references must
# match the library side (dllexport), otherwise MinGW looks for
# __imp_* import stubs that only a real DLL import lib provides.
target_compile_definitions(${name} PRIVATE OAKENGINE_BUILD)
endif ()
# Freshly linked binaries can exceed the 5s default discovery # Freshly linked binaries can exceed the 5s default discovery
# timeout on first run (dyld cold cache + Qt/OCIO init) # timeout on first run (dyld cold cache + Qt/OCIO init)
if (WIN32) if (WIN32)
+8
View File
@@ -324,6 +324,14 @@ bool FFmpegEncoder::open()
bool FFmpegEncoder::write_frame(FramePtr frame, Rational time) bool FFmpegEncoder::write_frame(FramePtr frame, Rational time)
{ {
// The render worker pool finishes tickets without a result when no
// worker is available (or the worker crashed); a null frame must fail
// the encode cleanly instead of crashing the export task.
if (!frame) {
qWarning() << "FFmpegEncoder::write_frame called with null frame";
return false;
}
// We may need to convert this frame to a frame that the bridge will understand // We may need to convert this frame to a frame that the bridge will understand
if (frame->format() != video_conversion_fmt_) { if (frame->format() != video_conversion_fmt_) {
frame = frame->convert(video_conversion_fmt_); frame = frame->convert(video_conversion_fmt_);
+1 -1
View File
@@ -275,7 +275,7 @@ void Html::write_char_format(QString *style, const QTextCharFormat &fmt)
} }
if (fmt.foreground().style() != Qt::NoBrush) { if (fmt.foreground().style() != Qt::NoBrush) {
const QColor &color = fmt.foreground().color(); const QColor color = fmt.foreground().color();
QString cs; QString cs;
if (color.alpha() == 255) { if (color.alpha() == 255) {
+2 -2
View File
@@ -225,7 +225,7 @@ public:
~Arena() ~Arena()
{ {
std::list<Element *> copy = lent_elements_; std::list<Element *> copy = lent_elements_;
foreach (Element *e, copy) { for (Element *e : copy) {
e->release(); e->release();
} }
@@ -348,7 +348,7 @@ public:
QMutexLocker locker(&lock_); QMutexLocker locker(&lock_);
// Attempt to get an element from an arena // Attempt to get an element from an arena
foreach (Arena *a, arenas_) { for (Arena *a : arenas_) {
ElementPtr e = a->Get(); ElementPtr e = a->Get();
if (e) { if (e) {
+4
View File
@@ -49,6 +49,10 @@ PixelFormat OIIOUtils::get_format_from_oiio_basetype(OIIO::TypeDesc::BASETYPE ty
switch (type) { switch (type) {
case OIIO::TypeDesc::UNKNOWN: case OIIO::TypeDesc::UNKNOWN:
case OIIO::TypeDesc::NONE: case OIIO::TypeDesc::NONE:
#if OIIO_VERSION >= 20500
case OIIO::TypeDesc::USTRINGHASH:
#endif
default:
break; break;
case OIIO::TypeDesc::INT8: case OIIO::TypeDesc::INT8:
+5 -4
View File
@@ -544,10 +544,11 @@ void EngineCore::save_autorecovery()
{ {
QFile realname_file(project_autorecovery_dir.filePath( QFile realname_file(project_autorecovery_dir.filePath(
QStringLiteral("realname.txt"))); QStringLiteral("realname.txt")));
realname_file.open(QFile::WriteOnly); if (realname_file.open(QFile::WriteOnly)) {
realname_file.write( realname_file.write(
open_project_->pretty_filename().toUtf8()); open_project_->pretty_filename().toUtf8());
realname_file.close(); realname_file.close();
}
} }
int64_t max_recoveries_per_file = int64_t max_recoveries_per_file =
+5 -1
View File
@@ -30,7 +30,11 @@
* is still built with default symbol visibility, so the legacy C++ symbols * is still built with default symbol visibility, so the legacy C++ symbols
* remain exported alongside the C ABI. * remain exported alongside the C ABI.
*/ */
#if defined(_WIN32) || defined(__CYGWIN__) #if defined(OAKENGINE_STATIC)
/* Internal consumers link the engine object files directly (oakengine-obj)
instead of the shared library; no dllimport/dllexport is wanted. */
#define OAKENGINE_API
#elif defined(_WIN32) || defined(__CYGWIN__)
#ifdef OAKENGINE_BUILD #ifdef OAKENGINE_BUILD
#define OAKENGINE_API __declspec(dllexport) #define OAKENGINE_API __declspec(dllexport)
#else #else
-1
View File
@@ -251,7 +251,6 @@ OAKENGINE_API int oakengine_clipboard_foreach_connection(
} }
#include <QtCore/qmetatype.h> #include <QtCore/qmetatype.h>
Q_DECLARE_OPAQUE_POINTER(OakEngineClipboard *) Q_DECLARE_OPAQUE_POINTER(OakEngineClipboard *)
Q_DECLARE_OPAQUE_POINTER(OakEngineMarker *)
#endif #endif
#endif /* OAKENGINE_SERIALIZER_H */ #endif /* OAKENGINE_SERIALIZER_H */
+1
View File
@@ -1316,6 +1316,7 @@ oakengine_clip_get_connected_viewer(const OakEngineBlock *clip);
Q_DECLARE_OPAQUE_POINTER(OakEngineBlock *) Q_DECLARE_OPAQUE_POINTER(OakEngineBlock *)
Q_DECLARE_OPAQUE_POINTER(OakEngineClip *) Q_DECLARE_OPAQUE_POINTER(OakEngineClip *)
Q_DECLARE_OPAQUE_POINTER(OakEngineMarkerList *) Q_DECLARE_OPAQUE_POINTER(OakEngineMarkerList *)
Q_DECLARE_OPAQUE_POINTER(OakEngineMarker *)
Q_DECLARE_OPAQUE_POINTER(OakEngineWorkarea *) Q_DECLARE_OPAQUE_POINTER(OakEngineWorkarea *)
Q_DECLARE_OPAQUE_POINTER(OakEngineTrack *) Q_DECLARE_OPAQUE_POINTER(OakEngineTrack *)
Q_DECLARE_OPAQUE_POINTER(OakEngineTrackList *) Q_DECLARE_OPAQUE_POINTER(OakEngineTrackList *)
+7 -3
View File
@@ -21,7 +21,9 @@
#include "node.h" #include "node.h"
#ifndef _WIN32
#include <execinfo.h> #include <execinfo.h>
#endif
#include <QApplication> #include <QApplication>
#include <QGuiApplication> #include <QGuiApplication>
@@ -1939,6 +1941,7 @@ void Node::report_invalid_input(const char *attempted_action, const QString &id,
<< "Failed to" << attempted_action << "parameter" << id << "element" << "Failed to" << attempted_action << "parameter" << id << "element"
<< element << "in node" << this->id() << "- input doesn't exist"; << element << "in node" << this->id() << "- input doesn't exist";
#ifndef _WIN32
if (qEnvironmentVariableIsSet("OAK_DEBUG_INVALID_INPUT")) { if (qEnvironmentVariableIsSet("OAK_DEBUG_INVALID_INPUT")) {
void *frames[32]; void *frames[32];
const int n = backtrace(frames, 32); const int n = backtrace(frames, 32);
@@ -1950,6 +1953,7 @@ void Node::report_invalid_input(const char *attempted_action, const QString &id,
free(symbols); free(symbols);
} }
} }
#endif
} }
NodeInputImmediate *Node::create_immediate(const QString &input) NodeInputImmediate *Node::create_immediate(const QString &input)
@@ -2382,14 +2386,14 @@ TimeRange Node::transform_time_to(TimeRange time, Node *target,
Node *from = this; Node *from = this;
Node *to = target; Node *to = target;
if (dir == k_transform_towards_input) { if (dir == k_towards_input) {
std::swap(from, to); std::swap(from, to);
} }
std::list<NodeInput> path = find_path(from, to, path_index); std::list<NodeInput> path = find_path(from, to, path_index);
if (!path.empty()) { if (!path.empty()) {
if (dir == k_transform_towards_input) { if (dir == k_towards_input) {
for (auto it = path.crbegin(); it != path.crend(); it++) { for (auto it = path.crbegin(); it != path.crend(); it++) {
const NodeInput &i = (*it); const NodeInput &i = (*it);
time = i.node()->input_time_adjustment(i.input(), i.element(), time = i.node()->input_time_adjustment(i.input(), i.element(),
@@ -2610,7 +2614,7 @@ void Node::invalidate_from_keyframe_time_change()
// Invalidate entire area surrounding the keyframe (either where it currently is, or where it used to be before it // Invalidate entire area surrounding the keyframe (either where it currently is, or where it used to be before it
// was resorted in the if block above) // was resorted in the if block above)
foreach (const TimeRange &r, invalidate_range) { for (const TimeRange &r : invalidate_range) {
parameter_value_changed(key->key_track_ref().input(), r); parameter_value_changed(key->key_track_ref().input(), r);
} }
+2 -2
View File
@@ -929,8 +929,8 @@ public:
static QString get_category_name(const CategoryID &c); static QString get_category_name(const CategoryID &c);
enum TransformTimeDirection { enum TransformTimeDirection {
k_transform_towards_input, k_towards_input,
k_transform_towards_output k_towards_output
}; };
/** /**
+5 -7
View File
@@ -232,8 +232,6 @@ QHash<QString, QVariant> build_default_values(
ofx_type == kOfxParamTypePushButton) { ofx_type == kOfxParamTypePushButton) {
continue; continue;
} }
const auto &props = param.second->getProperties();
bool is_secret = props.getIntProperty(kOfxParamPropSecret) != 0;
const QString input_id = const QString input_id =
QString::fromStdString(param.second->getName()); QString::fromStdString(param.second->getName());
if (input_id.isEmpty()) { if (input_id.isEmpty()) {
@@ -263,10 +261,10 @@ clip_label_for_name(const std::string &name,
} }
if (desc) { if (desc) {
const std::string &label = const std::string &param_label =
desc->getProps().getStringProperty(kOfxPropLabel); desc->getProps().getStringProperty(kOfxPropLabel);
if (!label.empty()) { if (!param_label.empty()) {
return QString::fromStdString(label); return QString::fromStdString(param_label);
} }
} }
@@ -432,9 +430,9 @@ olive::plugin::PluginNode::PluginNode(OFX::Host::ImageEffect::Instance *plugin)
const int value_count = props.getDimension(kOfxParamPropChoiceEnum); const int value_count = props.getDimension(kOfxParamPropChoiceEnum);
for (int i = 0; i < label_count; ++i) { for (int i = 0; i < label_count; ++i) {
const std::string &label = const std::string &choice_label =
props.getStringProperty(kOfxParamPropChoiceOption, i); props.getStringProperty(kOfxParamPropChoiceOption, i);
option_labels.append(QString::fromStdString(label)); option_labels.append(QString::fromStdString(choice_label));
} }
for (int i = 0; i < value_count; ++i) { for (int i = 0; i < value_count; ++i) {
+2 -2
View File
@@ -62,7 +62,7 @@ public:
*/ */
virtual void process_samples(const NodeValueRow &values, virtual void process_samples(const NodeValueRow &values,
const SampleBuffer &input, SampleBuffer &output, const SampleBuffer &input, SampleBuffer &output,
int index) const; int index) const override;
/** /**
* @brief If Value() pushes a GenerateJob, override this function for the image to create * @brief If Value() pushes a GenerateJob, override this function for the image to create
@@ -71,7 +71,7 @@ public:
* *
* The destination buffer. It will already be allocated and ready for writing to. * The destination buffer. It will already be allocated and ready for writing to.
*/ */
virtual void generate_frame(FramePtr frame, const GenerateJob &job) const; virtual void generate_frame(FramePtr frame, const GenerateJob &job) const override;
private: private:
QString sub_category_; QString sub_category_;
+7 -6
View File
@@ -377,8 +377,8 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
this, QStringLiteral("length")); this, QStringLiteral("length"));
// Push each stream as a footage job // Push each stream as a footage job
for (int i = 0; i < get_total_stream_count(); i++) { for (int si = 0; si < get_total_stream_count(); si++) {
Track::Reference ref = get_reference_from_real_index(i); Track::Reference ref = get_reference_from_real_index(si);
FootageJob job(globals.time(), decoder_, filename(), ref.type(), FootageJob job(globals.time(), decoder_, filename(), ref.type(),
get_length(), globals.loop_mode()); get_length(), globals.loop_mode());
@@ -427,9 +427,10 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
ProxyManager::k_proxy_ready && ProxyManager::k_proxy_ready &&
ProxyManager::proxy_filename_has_audio(proxy_path_)) { ProxyManager::proxy_filename_has_audio(proxy_path_)) {
int audio_rank = 0; int audio_rank = 0;
for (int i = 0; i < get_total_stream_count(); i++) { for (int sj = 0; sj < get_total_stream_count(); sj++) {
const Track::Reference other = const Track::Reference other =
get_reference_from_real_index(i); get_reference_from_real_index(sj);
if (other.type() == Track::k_audio && if (other.type() == Track::k_audio &&
get_audio_params(other.index()).stream_index() < get_audio_params(other.index()).stream_index() <
ap.stream_index()) { ap.stream_index()) {
@@ -448,8 +449,8 @@ void Footage::value(const NodeValueRow &value, const NodeGlobals &globals,
// Media is offline: push a generated warning frame for each video // Media is offline: push a generated warning frame for each video
// stream so missing media is clearly visible in the timeline instead // stream so missing media is clearly visible in the timeline instead
// of a transparent/black hole. generate_frame() draws the slat. // of a transparent/black hole. generate_frame() draws the slat.
for (int i = 0; i < get_total_stream_count(); i++) { for (int si = 0; si < get_total_stream_count(); si++) {
Track::Reference ref = get_reference_from_real_index(i); Track::Reference ref = get_reference_from_real_index(si);
if (ref.type() != Track::k_video) { if (ref.type() != Track::k_video) {
continue; continue;
} }
@@ -28,7 +28,7 @@ void SerializedLayoutInfo::to_xml(QXmlStreamWriter *writer) const
writer->writeStartElement(QStringLiteral("folders")); writer->writeStartElement(QStringLiteral("folders"));
foreach (Folder *folder, open_folders) { for (Folder *folder : open_folders) {
writer->writeTextElement( writer->writeTextElement(
QStringLiteral("folder"), QStringLiteral("folder"),
QString::number(reinterpret_cast<quintptr>(folder))); QString::number(reinterpret_cast<quintptr>(folder)));
@@ -38,7 +38,7 @@ void SerializedLayoutInfo::to_xml(QXmlStreamWriter *writer) const
writer->writeStartElement(QStringLiteral("timeline")); writer->writeStartElement(QStringLiteral("timeline"));
foreach (Sequence *sequence, open_sequences) { for (Sequence *sequence : open_sequences) {
writer->writeTextElement( writer->writeTextElement(
QStringLiteral("sequence"), QStringLiteral("sequence"),
QString::number(reinterpret_cast<quintptr>(sequence))); QString::number(reinterpret_cast<quintptr>(sequence)));
@@ -48,7 +48,7 @@ void SerializedLayoutInfo::to_xml(QXmlStreamWriter *writer) const
writer->writeStartElement(QStringLiteral("viewers")); writer->writeStartElement(QStringLiteral("viewers"));
foreach (ViewerOutput *viewer, open_viewers) { for (ViewerOutput *viewer : open_viewers) {
writer->writeTextElement( writer->writeTextElement(
QStringLiteral("viewer"), QStringLiteral("viewer"),
QString::number(reinterpret_cast<quintptr>(viewer))); QString::number(reinterpret_cast<quintptr>(viewer)));
+3 -1
View File
@@ -22,6 +22,7 @@
#include "timeformat.h" #include "timeformat.h"
#include <QDateTime> #include <QDateTime>
#include <QTimeZone>
namespace olive namespace olive
{ {
@@ -75,7 +76,8 @@ void TimeFormatNode::value(const NodeValueRow &value,
qint64 ms_since_epoch = value[k_time_input].to_double() * 1000; qint64 ms_since_epoch = value[k_time_input].to_double() * 1000;
bool time_is_local = value[k_local_time_input].to_bool(); bool time_is_local = value[k_local_time_input].to_bool();
QDateTime dt = QDateTime::fromMSecsSinceEpoch( QDateTime dt = QDateTime::fromMSecsSinceEpoch(
ms_since_epoch, time_is_local ? Qt::LocalTime : Qt::UTC); ms_since_epoch,
time_is_local ? QTimeZone::systemTimeZone() : QTimeZone::utc());
QString format = value[k_format_input].to_string(); QString format = value[k_format_input].to_string();
QString output = dt.toString(format); QString output = dt.toString(format);
table->push(NodeValue(NodeValue::k_text, output, this)); table->push(NodeValue(NodeValue::k_text, output, this));
+1 -3
View File
@@ -502,9 +502,7 @@ void NodeTraverser::resolve_jobs(NodeValue &val)
} }
val.set_value(tex); val.set_value(tex);
} else if (plugin::PluginJob *plugin_job = } else if (dynamic_cast<plugin::PluginJob *>(base_job)) {
dynamic_cast<plugin::PluginJob *>(
base_job)) {
VideoParams tex_params = job_tex->params(); VideoParams tex_params = job_tex->params();
// Force internal working format (F32) for plugin processing, // Force internal working format (F32) for plugin processing,
// matching FootageJob/GenerateJob behavior. // matching FootageJob/GenerateJob behavior.
+2
View File
@@ -4,6 +4,8 @@
/* Render backend plugin ABI (oakgl / oakvulkan dlopen) */ /* Render backend plugin ABI (oakgl / oakvulkan dlopen) */
_ZN5olive8Renderer*; _ZN5olive8Renderer*;
_ZTVN5olive8RendererE; _ZTVN5olive8RendererE;
_ZTIN5olive8RendererE;
_ZTSN5olive8RendererE;
_ZN5olive11VideoParams19get_bytes_per_pixelE*; _ZN5olive11VideoParams19get_bytes_per_pixelE*;
_ZN5olive11VideoParams21get_bytes_per_channelE*; _ZN5olive11VideoParams21get_bytes_per_channelE*;
_ZN5olive13FileFunctions19read_file_as_stringE*; _ZN5olive13FileFunctions19read_file_as_stringE*;
+3
View File
@@ -37,8 +37,11 @@ static const char *pixel_depth_to_ofx(core::PixelFormat format)
return kOfxBitDepthShort; return kOfxBitDepthShort;
case core::PixelFormat::f16: case core::PixelFormat::f16:
return kOfxBitDepthHalf; return kOfxBitDepthHalf;
default:
break;
case core::PixelFormat::f32: case core::PixelFormat::f32:
return kOfxBitDepthFloat; return kOfxBitDepthFloat;
case core::PixelFormat::u10:
case core::PixelFormat::invalid: case core::PixelFormat::invalid:
case core::PixelFormat::count: case core::PixelFormat::count:
break; break;
+6 -6
View File
@@ -79,18 +79,18 @@ public:
makeDescriptor(const std::string &bundle_path, makeDescriptor(const std::string &bundle_path,
OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override; OFX::Host::ImageEffect::ImageEffectPlugin *plugin) override;
/// vmessage /// vmessage
virtual OfxStatus vmessage(const char *type, const char *id, OfxStatus vmessage(const char *type, const char *id,
const char *format, va_list args); const char *format, va_list args) override;
/// vmessage /// vmessage
virtual OfxStatus setPersistentMessage(const char *type, const char *id, OfxStatus setPersistentMessage(const char *type, const char *id,
const char *format, va_list args); const char *format, va_list args) override;
/// vmessage /// vmessage
virtual OfxStatus clearPersistentMessage(); OfxStatus clearPersistentMessage() override;
#ifdef OFX_SUPPORTS_OPENGLRENDER #ifdef OFX_SUPPORTS_OPENGLRENDER
/// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources() /// @see OfxImageEffectOpenGLRenderSuiteV1.flushResources()
virtual OfxStatus flushOpenGLResources() const virtual OfxStatus flushOpenGLResources() const override
{ {
return kOfxStatFailed; return kOfxStatFailed;
}; };
+3 -3
View File
@@ -209,13 +209,13 @@ public:
/// get the current time on the timeline. This is not necessarily the same /// get the current time on the timeline. This is not necessarily the same
/// time as being passed to an action (eg render) /// time as being passed to an action (eg render)
virtual double timeLineGetTime(); double timeLineGetTime() override;
/// set the timeline to a specific time /// set the timeline to a specific time
virtual void timeLineGotoTime(double t); void timeLineGotoTime(double t) override;
/// get the first and last times available on the effect's timeline /// get the first and last times available on the effect's timeline
virtual void timeLineGetBounds(double &t1, double &t2); void timeLineGetBounds(double &t1, double &t2) override;
void setCustomInArgs(const std::string &action, void setCustomInArgs(const std::string &action,
OFX::Host::Property::Set &in_args) override; OFX::Host::Property::Set &in_args) override;

Some files were not shown because too many files have changed in this diff Show More