Merge branch 'c-abi-migration': complete C ABI facade migration

The app/worker/cli now reach liboakengine exclusively through the
oakengine_* pure-C ABI (557 -> 0 undefined olive:: symbols in
oak-editor, 0 in oak-render-worker). Includes the full facade,
app-side migration, EngineEventBridge event mechanism, undo-group
semantics, and the campaign documentation (roadmap through R7 plan).

Verified: full build 0 errors, ctest 45/45 (oak_cli_transcode
intermittent SEGFAULT is pre-existing flaky).
This commit is contained in:
2026-07-26 22:44:12 +08:00
459 changed files with 52746 additions and 5335 deletions
File diff suppressed because it is too large Load Diff
+301
View File
@@ -0,0 +1,301 @@
/***
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
@@ -0,0 +1,76 @@
/***
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
@@ -0,0 +1,412 @@
/*
* 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
@@ -0,0 +1,116 @@
/*
* 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
@@ -0,0 +1,260 @@
/***
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
@@ -0,0 +1,149 @@
/***
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
@@ -0,0 +1,907 @@
/***
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
@@ -0,0 +1,246 @@
/***
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
@@ -0,0 +1,102 @@
/***
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
@@ -0,0 +1,52 @@
/***
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
@@ -0,0 +1,161 @@
/***
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
@@ -0,0 +1,78 @@
/***
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
@@ -0,0 +1,120 @@
/***
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
@@ -0,0 +1,87 @@
/***
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
@@ -0,0 +1,76 @@
/*
* 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
@@ -0,0 +1,59 @@
/*
* 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
@@ -0,0 +1,55 @@
/***
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
@@ -0,0 +1,57 @@
/***
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
@@ -0,0 +1,947 @@
/***
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
@@ -0,0 +1,208 @@
/***
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
@@ -0,0 +1,35 @@
/***
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_);
}
}
@@ -0,0 +1,45 @@
/***
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
@@ -0,0 +1,110 @@
/***
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);
}
}
}
@@ -0,0 +1,47 @@
/***
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
@@ -0,0 +1,35 @@
/***
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_);
}
}
@@ -0,0 +1,45 @@
/***
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
@@ -0,0 +1,59 @@
/***
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();
}
}
}
@@ -0,0 +1,63 @@
/***
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
@@ -0,0 +1,91 @@
/***
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);
}
}
@@ -0,0 +1,47 @@
/***
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
@@ -0,0 +1,103 @@
/***
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);
}
}
@@ -0,0 +1,118 @@
/***
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
@@ -0,0 +1,59 @@
/***
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();
}
}
}
@@ -0,0 +1,65 @@
/***
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
@@ -0,0 +1,32 @@
/***
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
@@ -0,0 +1,573 @@
/***
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
@@ -0,0 +1,176 @@
/***
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
+1
View File
@@ -105,3 +105,4 @@ AGENTS.md
operations-log.md
verification.md
act
.tmp/
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+2
View File
@@ -22,6 +22,7 @@ submitted should abide by the following standards:
* Documentation comments should use **Javadoc-style** (`/** ... */`) where appropriate.
* Naming rules (enforced by `readability-identifier-naming` in `.clang-tidy`):
* Types (`class`, `struct`, `enum`, type aliases, template parameters): `PascalCase`
* `typedef` of structs is permitted (e.g. the opaque-handle pattern `typedef struct OakEngineNode OakEngineNode;`); struct typedefs follow `PascalCase`
* Functions, variables, member variables: `snake_case`
* Private/protected members: trailing underscore, `class_member_variables_`
* Constants and enum values: `snake_case` (e.g. `k_dry_run_interval`, `k_linear`); `ALL_CAPS` is reserved for macros — save the fear for things that are actually dangerous
@@ -30,5 +31,6 @@ submitted should abide by the following standards:
* Namespaces: short `snake_case`
* Getters: same name as the private member without the trailing underscore (`foo_``foo()`); setters: `set_foo()`
* Exception: Qt and third-party (e.g. OpenFX) virtual overrides and framework callbacks keep their original names (`paintEvent`, `getParams`, ...) — renaming them would break the override
* Tests are written with **Google Test** (`TEST`/`TEST_F`/`TEST_P` + `EXPECT_*`/`ASSERT_*`). Do not add hand-written test `main()`s, raw `assert()`-based test files, or custom test macros/frameworks. CTest stays the runner only — register cases through `gtest_discover_tests()`; use `GTEST_SKIP()` for environment-dependent cases (GPU, missing codecs) instead of relying on crashes or timeouts..
* 100 column limit (where it doesn't impair readability)
* Unix line endings (only LF no CRLF)
+96
View File
@@ -0,0 +1,96 @@
# Oak Video Editor
[![CI](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml/badge.svg)](https://github.com/OakVideoEditorCommunity/oak/actions/workflows/ci.yml)
[中文](docs/zh/README.new.md)
Oak Video Editor is a free, open-source **non-linear video editor** for Windows, macOS, and Linux.
This project is a community-maintained fork of Olive Video Editor.
> **NOTE: Oak Video Editor is alpha software and is considered highly unstable. We appreciate users testing it and sharing feedback, but please use it at your own risk.**
<!-- SCREENSHOT: main editing window (timeline + viewer) -->
![Screenshot main window](docs/images/screenshot-main.png)
## Features
- Responsive timeline editing with smart disk/playback caching
- Node-based compositing and effects, including an OpenFX (OFX) plugin host
- Full color management (OpenColorIO): `.cube`/`.3dl` LUTs, configurable display/view/look transforms
- Scopes: waveform, vectorscope, histogram, and audio meters (LUFS/VU)
- Bézier keyframe animation with a curve editor
- Multicam editing and waveform-based audio sync
- Proxy media workflow for smooth 4K/8K editing
- Hardware-accelerated and batch export (H.264/H.265, image sequences, audio)
- Project crash recovery and autosave
<!-- SCREENSHOT: node editor -->
![Screenshot node editor](docs/images/screenshot-node.png)
## Download
Pre-built binaries for Windows, macOS, and Linux are on the [Releases](https://github.com/OakVideoEditorCommunity/oak/releases) page.
Latest: [v0.4.2-alpha](https://github.com/OakVideoEditorCommunity/oak/releases/tag/v0.4.2-alpha)
## Architecture
Oak is split into small, independently testable components with a pure C ABI at the boundary:
| Component | Kind | Purpose |
|---|---|---|
| `liboakcore` | shared library | Qt-free core types (rational, timecode, bezier, sample buffer, audio/video params) with a pure C ABI |
| `liboakengine` | shared library | the editing engine (node graph, timeline, render, codec, tasks), exposed only through the `oakengine_*` C ABI facade |
| `oak-editor` | application | the Qt GUI; talks to the engine **only** through the C ABI |
| `oak-render-worker` | process | headless render process that executes frames off the GUI thread (NDJSON IPC) |
| `oak-cli` | tool | command-line frontend for the engine: media info, probing, rendering, and transcoding without the GUI |
The C ABI boundary is what makes the engine embeddable and is the foundation for a planned module-by-module rewrite of the engine in Rust (see [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md)).
<!-- DIAGRAM: component / ABI layout -->
![Architecture diagram](docs/images/architecture.png)
## Command-Line Tools
`oak-cli` is a standalone, pure-C-ABI consumer of the engine:
```bash
oak-cli info <file> # media information
oak-cli probe <file> # stream/decoder probe
oak-cli render <project.ove> <out> # render a project range
oak-cli transcode <in> <out> # transcode media
```
## Building from Source
See [`docs/build.md`](docs/build.md) for full instructions (Windows/MSYS2, Linux Debian/Ubuntu/Fedora/Arch, and [`docs/build_macos.md`](docs/build_macos.md) for macOS). In short:
```bash
cmake -B build -G Ninja
cmake --build build
ctest --test-dir build --output-on-failure
```
## Roadmap
| Version | Theme | Core Deliverables |
|:--|:--|:--|
| **0.3** | **Plugin Architecture** | Production-ready OpenFX host support — "any OFX plugin loads without crashing" |
| **0.4** | **Color, Audio & Performance** | `.cube`/`.3dl`, scopes, three-way color wheels, waveform auto-sync, BWF timecode sync, audio meters, proxy media, hardware-accelerated export, batch render queue |
| **0.5** | **Animation, Tracking & Collaboration** | Bézier keyframe curve editor, point tracking, image stabilizer, full multicam, OpenTimelineIO, EDL/XML interchange |
| **0.6** | **Stability** | Project file format freeze (backward compatibility), crash recovery, autosave, memory optimization |
| **1.0** | **Production Ready** | Complete documentation, installers, known-issues list, community support |
## Contributing
Contributions are welcome. Please read [`CONTRIBUTING.md`](CONTRIBUTING.md) first — it covers:
- the code style (naming rules, including `PascalCase` struct typedefs),
- the **Google Test** requirement for all tests,
- the C ABI boundary contract for engine-facing code.
Useful project docs: [`docs/zh/`](docs/zh/) (中文文档), [`docs/zh/facade-migration-roadmap.md`](docs/zh/facade-migration-roadmap.md), [`docs/zh/plans/riir.md`](docs/zh/plans/riir.md).
## License
Oak Video Editor is free software licensed under the [GNU General Public License v3](LICENSE).
+28
View File
@@ -19,6 +19,33 @@
set(OLIVE_SOURCES
core.h
core.cpp
engineeventbridge.h
engineeventbridge.cpp
common/htmlapp.cpp
common/filefunctionsapp.cpp
common/colorcodingapp.h
common/colorcodingapp.cpp
common/xmlutilsapp.cpp
common/hashstreamapp.cpp
common/qtutilsapp.cpp
common/nodevaluehandle.h
)
# The app-side *app.cpp helpers define symbols whose qualified names also
# exist in liboakengine.so (B10 moved utilities). Build them with hidden
# visibility so the executable's definitions are not ELF-interposable: the
# engine library keeps binding to its own copies and static data members
# (ColorCoding::colors, Html::k_block_tags) are not double-initialized and
# double-destroyed at process exit (was the exit-time heap corruption in
# timeline-tests / olive-gtest).
set_source_files_properties(
common/htmlapp.cpp
common/filefunctionsapp.cpp
common/colorcodingapp.cpp
common/xmlutilsapp.cpp
common/hashstreamapp.cpp
common/qtutilsapp.cpp
PROPERTIES COMPILE_OPTIONS "-fvisibility=hidden"
)
#set(OLIVE_RESOURCES)
@@ -27,6 +54,7 @@ set(OLIVE_SOURCES
add_subdirectory(dialog)
add_subdirectory(packaging)
add_subdirectory(panel)
add_subdirectory(timeline)
add_subdirectory(ts)
add_subdirectory(ui)
add_subdirectory(widget)
+81
View File
@@ -0,0 +1,81 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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 "common/colorcodingapp.h"
#include <QObject>
namespace olive
{
QVector<Color> ColorCoding::colors = {
Color(0.545f, 0.255f, 0.255f), Color(0.412f, 0.188f, 0.259f),
Color(0.561f, 0.427f, 0.239f), Color(0.486f, 0.306f, 0.235f),
Color(0.631f, 0.612f, 0.212f), Color(0.404f, 0.478f, 0.243f),
Color(0.349f, 0.576f, 0.275f), Color(0.224f, 0.459f, 0.251f),
Color(0.259f, 0.471f, 0.541f), Color(0.184f, 0.376f, 0.329f),
Color(0.259f, 0.365f, 0.541f), Color(0.196f, 0.216f, 0.412f),
Color(0.612f, 0.294f, 0.502f), Color(0.404f, 0.220f, 0.459f),
Color(0.800f, 0.800f, 0.800f), Color(0.502f, 0.502f, 0.502f)
};
const QVector<Color> &ColorCoding::standard_colors()
{
return colors;
}
QString ColorCoding::get_color_name(int c)
{
switch (c) {
case k_red: return QObject::tr("Red");
case k_maroon: return QObject::tr("Maroon");
case k_orange: return QObject::tr("Orange");
case k_brown: return QObject::tr("Brown");
case k_yellow: return QObject::tr("Yellow");
case k_olive: return QObject::tr("Olive");
case k_lime: return QObject::tr("Lime");
case k_green: return QObject::tr("Green");
case k_cyan: return QObject::tr("Cyan");
case k_teal: return QObject::tr("Teal");
case k_blue: return QObject::tr("Blue");
case k_navy: return QObject::tr("Navy");
case k_pink: return QObject::tr("Pink");
case k_purple: return QObject::tr("Purple");
case k_silver: return QObject::tr("Silver");
case k_gray: return QObject::tr("Gray");
}
return QString();
}
Color ColorCoding::get_color(int c)
{
return colors.at(c);
}
Qt::GlobalColor ColorCoding::get_ui_selector_color(const Color &c)
{
if (c.get_rough_luminance() > 0.40f) {
return Qt::black;
} else {
return Qt::white;
}
}
}
+75
View File
@@ -0,0 +1,75 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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_COLORCODINGAPP_H
#define OAK_COLORCODINGAPP_H
#include <olive/core/core.h>
#include <QString>
#include <QVector>
namespace olive
{
using namespace core;
/**
* @brief App-side ColorCoding (moved from engine/ui/colorcoding.h)
*
* Provides the same static color-label mapping as the engine version but
* without QObject inheritance (no moc symbols). Only the static methods
* used by app code are included.
*/
class ColorCoding {
public:
enum Code {
k_red,
k_maroon,
k_orange,
k_brown,
k_yellow,
k_olive,
k_lime,
k_green,
k_cyan,
k_teal,
k_blue,
k_navy,
k_pink,
k_purple,
k_silver,
k_gray
};
static QString get_color_name(int c);
static Color get_color(int c);
static Qt::GlobalColor get_ui_selector_color(const Color &c);
static const QVector<Color> &standard_colors();
private:
static QVector<Color> colors;
};
} // namespace olive
#endif // OAK_COLORCODINGAPP_H
+206
View File
@@ -0,0 +1,206 @@
/***
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_CONFIGWRAPPER_H
#define OAK_CONFIGWRAPPER_H
#include <QVariant>
#include "olive/core/util/rational.h"
#include "oakengine/config.h"
// Facade migration B9b: replace the engine's OAK_CONFIG macro (which
// references olive::Config::current()/operator[] and brings C++ symbols into
// the editor binary) with a thin header-only wrapper around the C ABI.
//
// Include this header instead of "config/config.h" in app code. It undefines
// the engine macros and redefines them to return an inline OakConfigValue that
// forwards reads/writes to oakengine_config_*().
namespace olive
{
class OakConfigValue {
public:
explicit OakConfigValue(const QString &key) : key_(key) {}
operator bool() const
{
return oakengine_config_get_int(key_utf8(), 0) != 0;
}
operator int() const
{
return static_cast<int>(oakengine_config_get_int(key_utf8(), 0));
}
operator qint64() const
{
return static_cast<qint64>(oakengine_config_get_int(key_utf8(), 0));
}
operator quint64() const
{
return static_cast<quint64>(oakengine_config_get_int(key_utf8(), 0));
}
operator int64_t() const
{
return oakengine_config_get_int(key_utf8(), 0);
}
operator uint64_t() const
{
return static_cast<uint64_t>(oakengine_config_get_int(key_utf8(), 0));
}
operator QString() const
{
char buf[1024];
const int len = oakengine_config_get_string(key_utf8(), buf,
sizeof(buf));
return QString::fromUtf8(buf, len);
}
operator QVariant() const
{
return QVariant(static_cast<QString>(*this));
}
OakConfigValue &operator=(bool v)
{
oakengine_config_set_int(key_utf8(), v ? 1 : 0);
return *this;
}
OakConfigValue &operator=(int v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(uint v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(qint64 v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(quint64 v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(int64_t v)
{
oakengine_config_set_int(key_utf8(), v);
return *this;
}
OakConfigValue &operator=(uint64_t v)
{
oakengine_config_set_int(key_utf8(), static_cast<int64_t>(v));
return *this;
}
OakConfigValue &operator=(const QString &v)
{
const QByteArray utf8 = v.toUtf8();
oakengine_config_set_string(key_utf8(), utf8.constData());
return *this;
}
OakConfigValue &operator=(const char *v)
{
oakengine_config_set_string(key_utf8(), v ? v : "");
return *this;
}
OakConfigValue &operator=(const QVariant &v)
{
switch (v.typeId()) {
case QMetaType::Bool:
*this = v.toBool();
break;
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Long:
case QMetaType::Short:
case QMetaType::Char:
case QMetaType::ULong:
case QMetaType::UShort:
case QMetaType::UChar:
*this = v.toLongLong();
break;
case QMetaType::Double:
case QMetaType::Float:
*this = static_cast<int64_t>(v.toDouble());
break;
default:
*this = v.toString();
break;
}
return *this;
}
bool toBool() const { return static_cast<bool>(*this); }
int toInt() const { return static_cast<int>(*this); }
qint64 toLongLong() const { return static_cast<qint64>(*this); }
quint64 toULongLong() const { return static_cast<quint64>(*this); }
QString toString() const { return static_cast<QString>(*this); }
bool operator==(int rhs) const { return toInt() == rhs; }
bool operator!=(int rhs) const { return toInt() != rhs; }
bool operator==(qint64 rhs) const { return toLongLong() == rhs; }
bool operator!=(qint64 rhs) const { return toLongLong() != rhs; }
bool operator==(const QString &rhs) const { return toString() == rhs; }
bool operator!=(const QString &rhs) const { return toString() != rhs; }
bool operator==(const char *rhs) const { return toString() == QString::fromUtf8(rhs); }
bool operator!=(const char *rhs) const { return toString() != QString::fromUtf8(rhs); }
template <typename T> T value() const
{
if constexpr (std::is_same_v<T, olive::core::Rational>) {
const QString s = static_cast<QString>(*this);
const QByteArray utf8 = s.toUtf8();
return olive::core::Rational::from_string(
std::string(utf8.constData(), size_t(utf8.size())));
} else {
return static_cast<T>(*this);
}
}
private:
const char *key_utf8() const
{
key_utf8_ = key_.toUtf8();
return key_utf8_.constData();
}
QString key_;
mutable QByteArray key_utf8_;
};
} // namespace olive
#ifdef OAK_CONFIG
#undef OAK_CONFIG
#endif
#ifdef OAK_CONFIG_STR
#undef OAK_CONFIG_STR
#endif
#define OAK_CONFIG(x) olive::OakConfigValue(QStringLiteral(x))
#define OAK_CONFIG_STR(x) olive::OakConfigValue(x)
#endif // OAK_CONFIGWRAPPER_H
+87
View File
@@ -0,0 +1,87 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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_DEBUGAPP_H
#define OAK_DEBUGAPP_H
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
#include <QMutex>
#include <QTextStream>
#include <iostream>
namespace olive {
/**
* @brief App-side debug handler (moved from engine/common/debug.cpp)
*
* Replaces engine's olive::debug_handler so oak-editor doesn't import
* that symbol. Only used in main.cpp's qInstallMessageHandler.
*/
static void debug_handler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
// Suppress noisy warnings from Qt's QXcbIntegration
if (type == QtWarningMsg && msg.contains("QXcbIntegration")) {
return;
}
// Suppress all Qt warnings during automated testing
static const bool is_testing = qEnvironmentVariableIsSet("OAK_TESTING");
if (is_testing && type == QtWarningMsg) {
return;
}
QString log_line;
switch (type) {
case QtDebugMsg:
log_line = QStringLiteral("Debug: %1 (%2:%3, %4)\n");
break;
case QtInfoMsg:
log_line = QStringLiteral("Info: %1 (%2:%3, %4)\n");
break;
case QtWarningMsg:
log_line = QStringLiteral("Warning: %1 (%2:%3, %4)\n");
break;
case QtCriticalMsg:
log_line = QStringLiteral("Critical: %1 (%2:%3, %4)\n");
break;
case QtFatalMsg:
log_line = QStringLiteral("Fatal: %1 (%2:%3, %4)\n");
break;
}
log_line = log_line.arg(msg, context.file != nullptr ? context.file : "<null>",
QString::number(context.line), context.function != nullptr ?
context.function : "<null>");
std::cerr << log_line.toUtf8().constData();
if (type == QtFatalMsg) {
abort();
}
}
} // namespace olive
#endif // OAK_DEBUGAPP_H
+98
View File
@@ -0,0 +1,98 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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/>.
***/
// App-side implementations of FileFunctions methods that would otherwise
// be imported from liboakengine. The declarations live in the engine header
// (common/filefunctions.h) which is on the public include path; these
// definitions resolve the symbols locally in the app binary.
#include "common/filefunctions.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
#include <QTextStream>
namespace olive
{
bool FileFunctions::directory_is_valid(const QDir &d,
bool try_to_create_if_not_exists)
{
return d.exists() ||
(try_to_create_if_not_exists && d.mkpath(QStringLiteral(".")));
}
QString FileFunctions::read_file_as_string(const QString &filename)
{
QFile f(filename);
QString file_data;
if (f.open(QFile::ReadOnly | QFile::Text)) {
QTextStream text_stream(&f);
file_data = text_stream.readAll();
f.close();
}
return file_data;
}
QString FileFunctions::get_auto_recovery_root()
{
return QDir(QStandardPaths::writableLocation(
QStandardPaths::AppLocalDataLocation))
.filePath(QStringLiteral("autorecovery"));
}
QString FileFunctions::ensure_filename_extension(QString fn,
const QString &extension)
{
if (!fn.isEmpty() && !extension.isEmpty()) {
QString extension_with_dot;
extension_with_dot.append('.');
extension_with_dot.append(extension);
if (!fn.endsWith(extension_with_dot, Qt::CaseInsensitive)) {
fn.append(extension_with_dot);
}
}
return fn;
}
QString FileFunctions::get_configuration_location()
{
if (is_portable()) {
return get_application_path();
} else {
QString s = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir(s).mkpath(".");
return s;
}
}
bool FileFunctions::is_portable()
{
return QFileInfo::exists(QDir(get_application_path()).filePath("portable"));
}
QString FileFunctions::get_application_path()
{
return QCoreApplication::applicationDirPath();
}
}
+67
View File
@@ -0,0 +1,67 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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/>.
***/
// App-side implementations of qHash overloads and stream operators
// used by QHash containers and QDataStream serialization in app code.
// Provides local definitions so the app doesn't import these from liboakengine.
#include "node/param.h"
#include "node/output/track/track.h"
namespace olive
{
uint qHash(const NodeInput &i)
{
return qHash(i.node()) ^ qHash(i.input()) ^ ::qHash(i.element());
}
uint qHash(const NodeInputPair &i)
{
return qHash(i.node) ^ qHash(i.input);
}
uint qHash(const NodeKeyframeTrackReference &i)
{
return qHash(i.input()) ^ ::qHash(i.track());
}
uint qHash(const Track::Reference &r, uint seed)
{
return ::qHash(QStringLiteral("%1:%2").arg(QString::number(r.type()),
QString::number(r.index())),
seed);
}
QDataStream &operator<<(QDataStream &out, const Track::Reference &ref)
{
out << static_cast<int>(ref.type()) << ref.index();
return out;
}
QDataStream &operator>>(QDataStream &in, Track::Reference &ref)
{
int type, index;
in >> type >> index;
ref = Track::Reference(static_cast<Track::Type>(type), index);
return in;
}
}
+507
View File
@@ -0,0 +1,507 @@
/*
* 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 "htmlapp.h"
#include <QAbstractTextDocumentLayout>
#include <QFont>
#include <QTextBlock>
#include <QTextBlockFormat>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTextFragment>
#include <QTextList>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include "common/xmlutils.h"
#include <QDebug>
#include <QTextBlock>
#include "common/xmlutils.h"
namespace olive
{
const QVector<QString> Html::k_block_tags = { QStringLiteral("p"),
QStringLiteral("div") };
inline bool str_equals(const QStringView &a, const QStringView &b)
{
return !a.compare(b, Qt::CaseInsensitive);
}
QString Html::doc_to_html(const QTextDocument *doc)
{
QString html;
QXmlStreamWriter writer(&html);
//writer.setAutoFormatting(true);
for (auto it = doc->begin(); it != doc->end(); it = it.next()) {
write_block(&writer, it);
}
return html;
}
struct HtmlNode {
QString tag;
QTextCharFormat format;
};
QTextCharFormat merge_html_formats(const QVector<HtmlNode> &stack)
{
QTextCharFormat f;
for (int i = 0; i < stack.size(); i++) {
f.merge(stack.at(i).format);
}
return f;
}
void Html::html_to_doc(QTextDocument *doc, const QString &html)
{
// Empty doc
doc->clear();
bool inside_block = true;
// Create cursor, which appears to be Qt's official way of inserting blocks and fragments
QTextCursor c(doc);
QString wrapped = QStringLiteral("<html>").append(html).append("</html>");
QXmlStreamReader reader(wrapped);
QVector<HtmlNode> fmt_stack;
QTextCharFormat default_fmt;
default_fmt.setFontWeight(QFont::Normal);
fmt_stack.append({ QStringLiteral("html"), default_fmt });
QTextCharFormat current_fmt;
while (!reader.atEnd()) {
reader.readNext();
if (reader.tokenType() == QXmlStreamReader::StartElement) {
QString tag = reader.name().toString().toLower();
fmt_stack.append({ tag, read_char_format(reader.attributes()) });
current_fmt = merge_html_formats(fmt_stack);
if (k_block_tags.contains(tag)) {
QTextBlockFormat block_fmt =
read_block_format(reader.attributes());
if (inside_block) {
c.setBlockFormat(block_fmt);
c.setBlockCharFormat(current_fmt);
} else {
c.insertBlock(block_fmt, current_fmt);
inside_block = true;
}
}
} else if (reader.tokenType() == QXmlStreamReader::Characters) {
QString characters = reader.text().toString();
c.insertText(characters, current_fmt);
} else if (reader.tokenType() == QXmlStreamReader::EndElement) {
QString tag = reader.name().toString().toLower();
for (int i = fmt_stack.size() - 1; i >= 0; i--) {
if (fmt_stack.at(i).tag == tag) {
fmt_stack.removeAt(i);
current_fmt = merge_html_formats(fmt_stack);
if (k_block_tags.contains(tag)) {
inside_block = false;
}
break;
}
}
}
}
if (reader.error()) {
qCritical() << "Failed to parse HTML:" << reader.errorString();
}
}
void Html::write_block(QXmlStreamWriter *writer, const QTextBlock &block)
{
writer->writeStartElement(QStringLiteral("p"));
const QTextBlockFormat &fmt = block.blockFormat();
// Write block alignment
if (!(fmt.alignment() & Qt::AlignLeft)) {
if (fmt.alignment() & Qt::AlignRight) {
writer->writeAttribute(QStringLiteral("align"),
QStringLiteral("right"));
} else if (fmt.alignment() & Qt::AlignHCenter) {
writer->writeAttribute(QStringLiteral("align"),
QStringLiteral("center"));
} else if (fmt.alignment() & Qt::AlignJustify) {
writer->writeAttribute(QStringLiteral("align"),
QStringLiteral("justify"));
}
}
// RTL support
if (block.textDirection() == Qt::RightToLeft) {
writer->writeAttribute(QStringLiteral("dir"), QStringLiteral("rtl"));
}
// Write CSS attributes
QString style;
if (fmt.lineHeightType() != QTextBlockFormat::SingleHeight) {
write_css_property(&style, QStringLiteral("line-height"),
QStringLiteral("%1%").arg(fmt.lineHeight()));
}
write_char_format(&style, block.charFormat());
if (!style.isEmpty()) {
writer->writeAttribute(QStringLiteral("style"), style);
}
auto it = block.begin();
if (it != block.end()) {
for (; it != block.end(); it++) {
write_fragment(writer, it.fragment());
}
}
writer->writeEndElement(); // p
}
void Html::write_fragment(QXmlStreamWriter *writer,
const QTextFragment &fragment)
{
const QTextCharFormat &fmt = fragment.charFormat();
writer->writeStartElement(QStringLiteral("span"));
// Write CSS attributes
QString style;
write_char_format(&style, fmt);
if (!style.isEmpty()) {
writer->writeAttribute(QStringLiteral("style"), style);
}
QStringList lines = fragment.text().split(QChar::LineSeparator);
bool first_line = true;
foreach (const QString &l, lines) {
if (first_line) {
first_line = false;
} else {
writer->writeEmptyElement(QStringLiteral("br"));
}
writer->writeCharacters(l);
}
writer->writeEndElement(); // span
}
void Html::write_css_property(QString *style, const QString &key,
const QStringList &values)
{
QString value;
foreach (QString v, values) {
if (v.contains(' ')) {
v = QStringLiteral("'%1'").arg(v);
}
append_string_auto_space(&value, v);
}
append_string_auto_space(style, QStringLiteral("%1: %2;").arg(key, value));
}
void Html::write_char_format(QString *style, const QTextCharFormat &fmt)
{
QStringList families = fmt.fontFamilies().toStringList();
if (!families.isEmpty()) {
write_css_property(style, QStringLiteral("font-family"),
families.first());
}
if (fmt.hasProperty(QTextFormat::FontPointSize)) {
write_css_property(
style, QStringLiteral("font-size"),
QStringLiteral("%1pt").arg(QString::number(fmt.fontPointSize())));
}
if (fmt.hasProperty(QTextFormat::FontWeight)) {
write_css_property(style, QStringLiteral("font-weight"),
QString::number(fmt.fontWeight() * 8));
}
if (fmt.hasProperty(QTextFormat::FontItalic)) {
write_css_property(style, QStringLiteral("font-style"),
fmt.fontItalic() ? QStringLiteral("italic") :
QStringLiteral("normal"));
}
if (fmt.hasProperty(QTextFormat::FontStyleName)) {
write_css_property(style, QStringLiteral("-ove-font-style"),
fmt.fontStyleName().toString());
}
QStringList deco;
if (fmt.fontUnderline()) {
deco.append(QStringLiteral("underline"));
}
if (fmt.fontStrikeOut()) {
deco.append(QStringLiteral("line-through"));
}
if (fmt.fontOverline()) {
deco.append(QStringLiteral("overline"));
}
if (!deco.isEmpty()) {
write_css_property(style, QStringLiteral("text-decoration"), deco);
}
if (fmt.foreground().style() != Qt::NoBrush) {
const QColor &color = fmt.foreground().color();
QString cs;
if (color.alpha() == 255) {
cs = color.name();
} else if (color.alpha()) {
cs = QStringLiteral("rgba(%1, %2, %3, %4)")
.arg(QString::number(color.red()),
QString::number(color.green()),
QString::number(color.blue()),
QString::number(color.alphaF()));
}
write_css_property(style, QStringLiteral("color"), cs);
}
if (fmt.fontCapitalization() != QFont::MixedCase) {
if (fmt.fontCapitalization() == QFont::SmallCaps) {
write_css_property(style, QStringLiteral("font-variant"),
QStringLiteral("small-caps"));
// TODO: Add others
}
}
if (fmt.fontLetterSpacing() != 0.0) {
write_css_property(style, QStringLiteral("letter-spacing"),
QStringLiteral("%1%").arg(
QString::number(fmt.fontLetterSpacing())));
}
if (fmt.fontStretch() != 0) {
write_css_property(
style, QStringLiteral("font-stretch"),
QStringLiteral("%1%").arg(QString::number(fmt.fontStretch())));
}
}
QTextCharFormat Html::read_char_format(const QXmlStreamAttributes &attributes)
{
QTextCharFormat fmt;
foreach (const QXmlStreamAttribute &attr, attributes) {
if (str_equals(attr.name(), QStringLiteral("style"))) {
auto css = get_css_from_style(attr.value().toString());
for (auto it = css.begin(); it != css.end(); it++) {
const QString &first_val = it.value().first();
if (it.key() == QStringLiteral("font-family")) {
fmt.setFontFamilies({ first_val });
} else if (it.key() == QStringLiteral("font-size")) {
if (first_val.endsWith(QStringLiteral("pt"),
Qt::CaseInsensitive)) {
fmt.setFontPointSize(first_val.chopped(2).toDouble());
}
} else if (it.key() == QStringLiteral("font-weight")) {
fmt.setFontWeight(first_val.toInt() / 8);
} else if (it.key() == QStringLiteral("font-style")) {
fmt.setFontItalic(
str_equals(first_val, QStringLiteral("italic")));
} else if (it.key() == QStringLiteral("text-decoration")) {
foreach (const QString &v, it.value()) {
if (str_equals(v, QStringLiteral("underline"))) {
fmt.setFontUnderline(true);
} else if (str_equals(v,
QStringLiteral("line-through"))) {
fmt.setFontStrikeOut(true);
} else if (str_equals(v, QStringLiteral("overline"))) {
fmt.setFontOverline(true);
}
}
} else if (it.key() == QStringLiteral("color")) {
if (first_val.startsWith(QStringLiteral("rgba"),
Qt::CaseInsensitive)) {
QString vals_only = first_val;
vals_only.remove(QStringLiteral("rgba"));
vals_only.remove(QStringLiteral("("));
vals_only.remove(QStringLiteral(")"));
QStringList rgba = vals_only.split(',');
if (rgba.size() == 4) {
QColor c;
c.setRed(rgba.at(0).toInt()); // Writer emits 0-255 RGB (CSS rgba() convention)
c.setGreen(rgba.at(1).toInt());
c.setBlue(rgba.at(2).toInt());
c.setAlphaF(rgba.at(3).toDouble());
fmt.setForeground(c);
}
} else {
fmt.setForeground(QColor(first_val));
}
} else if (it.key() == QStringLiteral("font-variant")) {
if (str_equals(first_val, QStringLiteral("small-caps"))) {
fmt.setFontCapitalization(QFont::SmallCaps);
}
} else if (it.key() == QStringLiteral("letter-spacing")) {
if (first_val.contains(QChar('%'))) {
fmt.setFontLetterSpacing(
first_val.chopped(1).toDouble());
}
} else if (it.key() == QStringLiteral("font-stretch")) {
if (first_val.contains(QChar('%'))) {
fmt.setFontStretch(first_val.chopped(1).toInt());
}
} else if (it.key() == QStringLiteral("-ove-font-style")) {
fmt.setFontStyleName(first_val);
}
}
}
}
return fmt;
}
QTextBlockFormat Html::read_block_format(const QXmlStreamAttributes &attributes)
{
QTextBlockFormat block_fmt;
foreach (const QXmlStreamAttribute &attr, attributes) {
if (str_equals(attr.name(), QStringLiteral("align"))) {
if (str_equals(attr.value(), QStringLiteral("right"))) {
block_fmt.setAlignment(Qt::AlignRight);
} else if (str_equals(attr.value(), QStringLiteral("center"))) {
block_fmt.setAlignment(Qt::AlignHCenter);
} else if (str_equals(attr.value(), QStringLiteral("justify"))) {
block_fmt.setAlignment(Qt::AlignJustify);
}
} else if (str_equals(attr.name(), QStringLiteral("dir"))) {
if (str_equals(attr.value(), QStringLiteral("rtl"))) {
block_fmt.setLayoutDirection(Qt::RightToLeft);
}
} else if (str_equals(attr.name(), QStringLiteral("style"))) {
auto css = get_css_from_style(attr.value().toString());
for (auto it = css.begin(); it != css.end(); it++) {
if (it.key() == QStringLiteral("line-height")) {
const QString &first_val = it.value().constFirst();
if (first_val.contains(QChar('%'))) {
block_fmt.setLineHeight(
first_val.chopped(1).toDouble(),
QTextBlockFormat::ProportionalHeight);
}
}
}
}
}
return block_fmt;
}
void Html::append_string_auto_space(QString *s, const QString &append)
{
if (!s->isEmpty()) {
s->append(QChar(' '));
}
s->append(append);
}
QMap<QString, QStringList> Html::get_css_from_style(const QString &s)
{
QMap<QString, QStringList> map;
QStringList list = s.split(QChar(';'));
foreach (const QString &a, list) {
QStringList kv = a.split(QChar(':'));
if (kv.size() != 2) {
continue;
}
// I'm sure there's regex that could do this, but I couldn't figure it out. It needs to split
// by space EXCEPT within quotes OR double-quotes, and said quotes should be EXCLUDED from each
// match. Also commas should be filtered out.
QStringList values;
const QString &val = kv.at(1);
QChar in_quote(0);
QString current_str;
for (int i = 0; i < val.size(); i++) {
const QChar &current_char = val.at(i);
if (!in_quote.isNull()) {
// If inside quotes and character isn't quote, indiscriminately append char
if (current_char == in_quote) {
in_quote = QChar(0);
} else {
current_str.append(current_char);
}
} else if (current_char.isSpace() || current_char == QChar(',')) {
// Dump current
if (!current_str.isEmpty()) {
values.append(current_str);
current_str.clear();
}
} else if (in_quote.isNull() && (current_char == QChar('\'') ||
current_char == QChar('"'))) {
in_quote = current_char;
} else {
current_str.append(current_char);
}
}
if (!current_str.isEmpty()) {
values.append(current_str);
}
// Not sure if this will ever happen, but just in case, we will avoid assert failures with this
if (values.isEmpty()) {
values.append(QString());
}
map[kv.at(0).trimmed().toLower()] = values;
}
return map;
}
}
+83
View File
@@ -0,0 +1,83 @@
/*
* 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_HTMLAPP_H
#define OAK_HTML_H
#include <QTextDocument>
#include <QTextFragment>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
namespace olive
{
/**
* @brief Functions for converting HTML to QTextDocument and vice versa
*
* Qt does contain its own functions for this, however they have some limitations. Some things that
* we want to support (e.g. kerning/spacing and font stretch) are not implemented in Qt's
* QTextHtmlExporter and QTextHtmlParser. Additionally, since these functions are not part of Qt's
* public API, and make many references to other parts of Qt that are not part of the public API,
* there is no way to subclass or extend their functionality without forking Qt as a whole.
*
* Therefore, it became necessary to write a custom class for the conversion so that we can
* ensure support for the features we need.
*
* If someone wishes to extend this class for more feature support, feel free to open a pull
* request. But this is NOT intended to be an exhaustive HTML implementation, and is primarily
* designed to store rich text in a standard format for the purpose of text formatting for video.
*/
class Html {
public:
static QString doc_to_html(const QTextDocument *doc);
static void html_to_doc(QTextDocument *doc, const QString &html);
private:
static void write_block(QXmlStreamWriter *writer, const QTextBlock &block);
static void write_fragment(QXmlStreamWriter *writer,
const QTextFragment &fragment);
static void write_css_property(QString *style, const QString &key,
const QStringList &value);
static void write_css_property(QString *style, const QString &key,
const QString &value)
{
write_css_property(style, key, QStringList({ value }));
}
static void write_char_format(QString *style, const QTextCharFormat &fmt);
static QTextCharFormat
read_char_format(const QXmlStreamAttributes &attributes);
static QTextBlockFormat
read_block_format(const QXmlStreamAttributes &attributes);
static void append_string_auto_space(QString *s, const QString &append);
static QMap<QString, QStringList> get_css_from_style(const QString &s);
static const QVector<QString> k_block_tags;
};
}
#endif // OAK_HTML_H
+68
View File
@@ -0,0 +1,68 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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_NODEVALUEHANDLE_H
#define OAK_NODEVALUEHANDLE_H
#include "node/value.h"
#include "oakengine/node.h"
namespace olive
{
/**
* @brief Convert engine NodeValue::Type to oak_node_value_type (app-side).
*
* The two enums do NOT share ordinals (e.g. k_boolean=4 vs BOOL=3), so a
* plain int cast is a bug. Mirrors from_c_type() in
* engine/src/capi/node.cpp. Lives in an app header, NOT in the public
* facade headers the C ABI surface stays pure C (see
* docs/zh/r6-cleanup-plan.md red line 3 context). Returns -1 for types the
* facade cannot represent (caller falls back to the input's declared type).
*/
inline int node_value_type_to_c(NodeValue::Type t)
{
switch (t) {
case NodeValue::k_int: return OAK_NODE_VALUE_INT;
case NodeValue::k_float: return OAK_NODE_VALUE_FLOAT;
case NodeValue::k_boolean: return OAK_NODE_VALUE_BOOL;
case NodeValue::k_rational: return OAK_NODE_VALUE_RATIONAL;
case NodeValue::k_color: return OAK_NODE_VALUE_COLOR;
case NodeValue::k_vec2: return OAK_NODE_VALUE_VEC2;
case NodeValue::k_vec3: return OAK_NODE_VALUE_VEC3;
case NodeValue::k_vec4: return OAK_NODE_VALUE_VEC4;
case NodeValue::k_combo: return OAK_NODE_VALUE_COMBO;
case NodeValue::k_file: return OAK_NODE_VALUE_STRING;
case NodeValue::k_text: return OAK_NODE_VALUE_TEXT;
case NodeValue::k_font: return OAK_NODE_VALUE_FONT;
case NodeValue::k_str_combo: return OAK_NODE_VALUE_STR_COMBO;
case NodeValue::k_binary: return OAK_NODE_VALUE_BINARY;
case NodeValue::k_bezier: return OAK_NODE_VALUE_BEZIER;
case NodeValue::k_texture: return OAK_NODE_VALUE_TEXTURE;
case NodeValue::k_samples: return OAK_NODE_VALUE_SAMPLES;
case NodeValue::k_video_params: return OAK_NODE_VALUE_VIDEO_PARAMS;
case NodeValue::k_audio_params: return OAK_NODE_VALUE_AUDIO_PARAMS;
default: return -1;
}
}
} // namespace olive
#endif // OAK_NODEVALUEHANDLE_H
+226
View File
@@ -0,0 +1,226 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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 OAKVALUEHELPER_H
#define OAKVALUEHELPER_H
#include <QVariant>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#include "node/keyframe.h"
#include "node/value.h"
#include "oakengine/node.h"
#include "olive/core/util/color.h"
namespace olive {
/**
* @brief Convert a per-track component QVariant into the C ABI oak_node_value POD.
*
* `type` is the declared input data type (e.g. k_float/k_color). For split-track
* types the component is the track-0 scalar (float for k_color's red channel, etc.).
* Returns false for types that have no POD representation.
*/
static inline bool QVariantToOakNodeValue(NodeValue::Type type, const QVariant &v,
oak_node_value *out)
{
memset(out, 0, sizeof(*out));
switch (type) {
case NodeValue::k_int:
case NodeValue::k_combo:
out->type = (type == NodeValue::k_combo) ? OAK_NODE_VALUE_COMBO
: OAK_NODE_VALUE_INT;
out->num = v.toLongLong();
return true;
case NodeValue::k_float:
out->type = OAK_NODE_VALUE_FLOAT;
out->f[0] = v.toDouble();
return true;
case NodeValue::k_boolean:
out->type = OAK_NODE_VALUE_BOOL;
out->num = v.toBool() ? 1 : 0;
return true;
case NodeValue::k_rational:
out->type = OAK_NODE_VALUE_RATIONAL;
{
const Rational r = v.value<Rational>();
out->num = r.numerator();
out->den = r.denominator();
}
return true;
case NodeValue::k_color:
out->type = OAK_NODE_VALUE_COLOR;
{
const core::Color c = v.value<core::Color>();
out->f[0] = c.red();
out->f[1] = c.green();
out->f[2] = c.blue();
out->f[3] = c.alpha();
}
return true;
case NodeValue::k_vec2:
out->type = OAK_NODE_VALUE_VEC2;
{
const QVector2D vec = v.value<QVector2D>();
out->f[0] = vec.x();
out->f[1] = vec.y();
}
return true;
case NodeValue::k_vec3:
out->type = OAK_NODE_VALUE_VEC3;
{
const QVector3D vec = v.value<QVector3D>();
out->f[0] = vec.x();
out->f[1] = vec.y();
out->f[2] = vec.z();
}
return true;
case NodeValue::k_vec4:
out->type = OAK_NODE_VALUE_VEC4;
{
const QVector4D vec = v.value<QVector4D>();
out->f[0] = vec.x();
out->f[1] = vec.y();
out->f[2] = vec.z();
out->f[3] = vec.w();
}
return true;
default:
return false;
}
}
/**
* @brief Convert a per-track component QVariant into the C ABI oak_node_value POD.
*
* Unlike QVariantToOakNodeValue() which takes a full normal value, this takes a
* single track's component (e.g. one float for a k_color channel). The resulting
* POD has the input's declared type with the component in f[0]/num, exactly what
* the per-track facade commands expect.
*/
static inline bool NodeTrackComponentToOakNodeValue(NodeValue::Type type,
const QVariant &v,
oak_node_value *out)
{
memset(out, 0, sizeof(*out));
switch (type) {
case NodeValue::k_int:
case NodeValue::k_combo:
out->type = (type == NodeValue::k_combo) ? OAK_NODE_VALUE_COMBO
: OAK_NODE_VALUE_INT;
out->num = v.toLongLong();
return true;
case NodeValue::k_float:
case NodeValue::k_bezier:
out->type = OAK_NODE_VALUE_FLOAT;
out->f[0] = v.toDouble();
return true;
case NodeValue::k_boolean:
out->type = OAK_NODE_VALUE_BOOL;
out->num = v.toBool() ? 1 : 0;
return true;
case NodeValue::k_rational:
out->type = OAK_NODE_VALUE_RATIONAL;
{
const Rational r = v.value<Rational>();
out->num = r.numerator();
out->den = r.denominator();
}
return true;
case NodeValue::k_color:
out->type = OAK_NODE_VALUE_COLOR;
out->f[0] = v.toFloat();
return true;
case NodeValue::k_vec2:
out->type = OAK_NODE_VALUE_VEC2;
out->f[0] = v.toFloat();
return true;
case NodeValue::k_vec3:
out->type = OAK_NODE_VALUE_VEC3;
out->f[0] = v.toFloat();
return true;
case NodeValue::k_vec4:
out->type = OAK_NODE_VALUE_VEC4;
out->f[0] = v.toFloat();
return true;
default:
return false;
}
}
/**
* @brief Convert a full C ABI oak_node_value POD back into a QVariant.
*
* Mirrors QVariantToOakNodeValue(). String/binary/bezier are not represented
* in the POD and return an invalid QVariant; use the dedicated string/binary/
* bezier facade getters for those.
*/
static inline QVariant OakNodeValueToQVariant(const oak_node_value &v)
{
switch (v.type) {
case OAK_NODE_VALUE_INT:
return QVariant::fromValue<qlonglong>(v.num);
case OAK_NODE_VALUE_FLOAT:
return QVariant::fromValue(v.f[0]);
case OAK_NODE_VALUE_BOOL:
return QVariant::fromValue(v.num != 0);
case OAK_NODE_VALUE_RATIONAL:
return QVariant::fromValue(
Rational(int(v.num), int(v.den)));
case OAK_NODE_VALUE_COLOR:
return QVariant::fromValue(core::Color(
float(v.f[0]), float(v.f[1]), float(v.f[2]), float(v.f[3])));
case OAK_NODE_VALUE_VEC2:
return QVariant::fromValue(
QVector2D(float(v.f[0]), float(v.f[1])));
case OAK_NODE_VALUE_VEC3:
return QVariant::fromValue(
QVector3D(float(v.f[0]), float(v.f[1]), float(v.f[2])));
case OAK_NODE_VALUE_VEC4:
return QVariant::fromValue(
QVector4D(float(v.f[0]), float(v.f[1]), float(v.f[2]), float(v.f[3])));
case OAK_NODE_VALUE_COMBO:
return QVariant::fromValue<int>(int(v.num));
default:
return QVariant();
}
}
/**
* @brief Map an engine NodeKeyframe::Type to the facade easing type.
*/
static inline int NodeKeyframeTypeToFacade(NodeKeyframe::Type type)
{
switch (type) {
case NodeKeyframe::k_bezier:
return 1;
case NodeKeyframe::k_hold:
return 2;
case NodeKeyframe::k_linear:
default:
return 0;
}
}
} // namespace olive
#endif // OAKVALUEHELPER_H
+143
View File
@@ -0,0 +1,143 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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 "common/qtutils.h"
namespace olive
{
int QtUtils::q_font_metrics_width(QFontMetrics fm, const QString &s)
{
return fm.horizontalAdvance(s);
}
QFrame *QtUtils::create_horizontal_line()
{
QFrame *horizontal_line = new QFrame();
horizontal_line->setFrameShape(QFrame::HLine);
horizontal_line->setFrameShadow(QFrame::Sunken);
return horizontal_line;
}
QFrame *QtUtils::create_vertical_line()
{
QFrame *l = create_horizontal_line();
l->setFrameShape(QFrame::VLine);
return l;
}
QString QtUtils::get_formatted_date_time(const QDateTime &dt)
{
return dt.toString(Qt::TextDate);
}
QStringList QtUtils::word_wrap_string(const QString &s, const QFontMetrics &fm,
int bounding_width)
{
QStringList list;
QStringList lines = s.split('\n');
for (int i = 0; i < lines.size(); i++) {
QString this_line = lines.at(i);
while (this_line.size() > 1 &&
q_font_metrics_width(fm, this_line) >= bounding_width) {
int old_size = this_line.size();
int hard_break = -1;
for (int j = this_line.size() - 1; j >= 0; j--) {
const QChar &char_test = this_line.at(j);
if (char_test.isSpace() || char_test == '-') {
if (q_font_metrics_width(fm, this_line.left(j)) <
bounding_width) {
if (!char_test.isSpace()) j++;
list.append(this_line.left(j));
while (j < this_line.size() &&
this_line.at(j).isSpace()) j++;
this_line.remove(0, j);
break;
}
} else if (hard_break == -1 &&
q_font_metrics_width(fm, this_line.left(j)) <
bounding_width) {
hard_break = j;
}
}
if (old_size == this_line.size()) {
if (hard_break != -1) {
list.append(this_line.left(hard_break));
this_line.remove(0, hard_break);
} else {
break;
}
}
}
if (!this_line.isEmpty()) {
list.append(this_line);
}
}
return list;
}
Qt::KeyboardModifiers
QtUtils::flip_control_and_shift_modifiers(Qt::KeyboardModifiers e)
{
if (e & Qt::ControlModifier & Qt::ShiftModifier) return e;
if (e & Qt::ShiftModifier) {
e |= Qt::ControlModifier;
e &= ~Qt::ShiftModifier;
} else if (e & Qt::ControlModifier) {
e |= Qt::ShiftModifier;
e &= ~Qt::ControlModifier;
}
return e;
}
void QtUtils::set_combo_box_data(QComboBox *cb, int data)
{
for (int i = 0; i < cb->count(); i++) {
if (cb->itemData(i).toInt() == data) {
cb->setCurrentIndex(i);
break;
}
}
}
void QtUtils::set_combo_box_data(QComboBox *cb, const QString &data)
{
for (int i = 0; i < cb->count(); i++) {
if (cb->itemData(i).toString() == data) {
cb->setCurrentIndex(i);
break;
}
}
}
QColor QtUtils::to_q_color(const core::Color &i)
{
QColor c;
// QColor only supports values from 0.0 to 1.0 and are only used for UI representations
c.setRedF(std::clamp(i.red(), 0.0f, 1.0f));
c.setGreenF(std::clamp(i.green(), 0.0f, 1.0f));
c.setBlueF(std::clamp(i.blue(), 0.0f, 1.0f));
c.setAlphaF(std::clamp(i.alpha(), 0.0f, 1.0f));
return c;
}
}
+60
View File
@@ -0,0 +1,60 @@
/***
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_UNDOWRAPPER_H
#define OAK_UNDOWRAPPER_H
#include "oakengine/undo.h"
namespace olive
{
/**
* Wrap an app-side undo command object in the facade custom-command API.
*
* `Cmd` must provide public `redo()` and `undo()` methods. Ownership of `cmd`
* is transferred to the returned opaque command pointer; the wrapper deletes
* `cmd` when the engine command is destroyed.
*
* This helper lets app code keep small app-state undo commands (selections,
* splitter sizes, etc.) without defining new subclasses of olive::UndoCommand,
* which would keep olive::UndoCommand symbols in the editor binary.
*/
template <typename Cmd>
void *wrap_app_undo_command(const char *name, Cmd *cmd)
{
return oakengine_undo_command_create(
name,
[](void *userdata) {
static_cast<Cmd *>(userdata)->redo();
},
[](void *userdata) {
static_cast<Cmd *>(userdata)->undo();
},
[](void *userdata) {
delete static_cast<Cmd *>(userdata);
},
cmd);
}
} // namespace olive
#endif // OAK_UNDOWRAPPER_H
+47
View File
@@ -0,0 +1,47 @@
/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak 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/>.
***/
// App-side implementation of xml_read_next_start_element
// Provides a local definition so the app doesn't import this from liboakengine.
#include "common/xmlutils.h"
namespace olive
{
bool xml_read_next_start_element(QXmlStreamReader *reader,
CancelAtom *cancel_atom)
{
QXmlStreamReader::TokenType token;
while ((token = reader->readNext()) != QXmlStreamReader::Invalid &&
token != QXmlStreamReader::EndDocument &&
(!cancel_atom || !cancel_atom->is_cancelled())) {
if (reader->isEndElement()) {
return false;
} else if (reader->isStartElement()) {
return true;
}
}
return false;
}
}
+590 -199
View File
File diff suppressed because it is too large Load Diff
+115 -25
View File
@@ -23,6 +23,11 @@
#define OAK_CORE_H
#include "coreengine.h"
#include <QObject>
#include "oakengine/app.h"
#include "oakengine/undo.h"
#include "oakengine/init.h"
#include "oakengine/task.h"
namespace olive
{
@@ -32,35 +37,40 @@ class MainWindow;
/**
* @brief The main central Olive application instance_
*
* This is the UI-facing derivation of EngineCore. It runs both in GUI and
* CLI modes (and handles what to init based on that). All UI-independent
* engine state lives in the base class EngineCore; this class adds the main
* window, dialogs and other user interaction on top of it.
* This is the UI-facing application controller. It holds an EngineCore
* member for UI-independent engine state and adds the main window, dialogs
* and other user interaction on top of it.
*
* EngineCore is NOT a base class it is a member, so the MOC-generated
* code for Core does not pull in EngineCore's Q_OBJECT symbols.
*
* The "public slots" are usually user-triggered actions and can be connected to UI elements (e.g. creating a folder,
* opening the import dialog, etc.)
*/
class Core : public EngineCore {
class Core : public QObject {
Q_OBJECT
public:
/**
* @brief Core Constructor
*
* Registers the UI handlers that EngineCore uses to request user
* interaction.
* Creates the EngineCore engine instance and registers the UI handlers
* that the engine uses to request user interaction.
*/
Core(const CoreParams &params);
Core(const OakEngineAppParams *params = nullptr);
~Core()
{
instance_ = nullptr;
}
/**
* @brief Core object accessible from anywhere in the code
*
* Use this to access Core functions. This is simply EngineCore::instance()
* cast to Core, which is safe because the application entry point (main())
* always constructs a Core.
* Returns the application Core singleton (no EngineCore::instance() call).
*/
static Core *instance()
{
return static_cast<Core *>(EngineCore::instance());
return instance_;
}
/**
@@ -113,7 +123,7 @@ public:
* @brief Show a dialog to the user to rename a set of nodes
*/
bool label_nodes(const QVector<Node *> &nodes,
MultiUndoCommand *parent = nullptr);
void *parent = nullptr);
/**
* @brief Opens a project from the recently opened list
@@ -137,6 +147,9 @@ public:
void open_export_dialog_for_viewer(ViewerOutput *viewer,
bool start_still_image);
bool add_open_project_from_task(OakEngineTask *task, bool add_to_recents);
bool add_recovery_project_from_task(OakEngineTask *task);
public slots:
/**
* @brief Starts an open file dialog to load a project from file
@@ -180,13 +193,6 @@ public slots:
*/
void dialog_export_show();
/**
* @brief Show OTIO import dialog
*/
#ifdef USE_OTIO
bool DialogImportOTIOShow(const QList<Sequence *> &sequences);
#endif
/**
* @brief Create a new folder in the currently active project
*/
@@ -201,6 +207,85 @@ public slots:
void browse_auto_recoveries();
public:
// The following methods are ordinary member functions, NOT slots. They are
// deliberately kept out of the `public slots:` section because their
// signatures reference engine C++ types (Project*, Sequence*, UndoStack*).
// If MOC processed them as slots it would instantiate QMetaType for those
// types and pull their staticMetaObject symbols across the ABI boundary.
// None of them are connect() targets: every connection involving Core uses
// the new-style member-function syntax, which works with plain methods.
/**
* @brief Show OTIO import dialog
*/
#ifdef USE_OTIO
bool DialogImportOTIOShow(const QList<Sequence *> &sequences);
#endif
// ---- Facade-wrapping methods (shadow EngineCore to avoid symbol refs) ----
UndoStack *undo_stack() const;
Tool::Item tool() const;
void set_tool(const Tool::Item &tool);
bool snapping() const;
void set_snapping(const bool &b);
Timecode::Display get_timecode_display() const;
void set_timecode_display(Timecode::Display d);
void show_status_bar_message(const QString &s, int timeout = 0);
void clear_status_bar_message();
static QString footage_file_dialog_filter();
static bool is_footage_extension_allowed(const QString &path);
void create_new_project();
Sequence *create_new_sequence_for_project(const QString &format,
Project *project);
static Sequence *create_new_sequence_for_project(Project *project);
void clear_open_recent_list();
void set_use_proxy_media(bool enabled);
void request_pixel_sampling_in_viewers(bool e);
Tool::AddableObject get_selected_addable_object() const;
void set_selected_addable_object(const Tool::AddableObject &obj);
void set_selected_transition_object(const QString &obj);
static void copy_string_to_clipboard(const QString &s);
void set_magic(bool e);
// Recent project list accessors (replaces EngineCore::get_recent_projects())
int get_recent_project_count() const;
QString get_recent_project_at(int index) const;
// Facade-wrapping methods (delegate through the C ABI)
bool set_language(const QString &locale);
void set_autorecovery_interval(int minutes);
void on_project_saved(Project *p);
static QString get_auto_recovery_index_filename();
void add_open_project(olive::Project *p, bool add_to_recents = false);
void remove_recently_opened_project(int index);
void set_active_project(Project *p);
QString get_selected_transition() const;
signals:
// Forwarding signals (shadow EngineCore signals so connect() resolves here)
void tool_changed(const Tool::Item &tool);
void addable_object_changed(Tool::AddableObject o);
void snapping_changed(const bool &b);
void timecode_display_changed(Timecode::Display d);
void open_recent_list_changed();
void color_picker_enabled(bool e);
void color_picker_color_emitted(const Color &reference, const Color &display);
private:
/**
* @brief Get the file filter than can be used with QFileDialog to open and save compatible projects
@@ -242,15 +327,20 @@ private:
*/
MainWindow *main_window_;
private slots:
void project_save_succeeded(Task *task);
/**
* @brief Cached Core* singleton
*/
static Core *instance_;
bool add_open_project_from_task_and_add_to_recents(Task *task)
private slots:
void project_save_succeeded(OakEngineTask *task);
bool add_open_project_from_task_and_add_to_recents(OakEngineTask *task)
{
return add_open_project_from_task(task, true);
return instance()->add_open_project_from_task(task, true);
}
void import_task_complete(Task *task);
void import_task_complete(OakEngineTask *task);
bool confirm_image_sequence(const QString &filename);
+1 -1
View File
@@ -26,7 +26,7 @@
#include <QLabel>
#include <QVBoxLayout>
#include "config/config.h"
#include "common/configwrapper.h"
#include "patreon.h"
#include "scrollinglabel.h"
+56 -16
View File
@@ -30,7 +30,7 @@
namespace olive
{
ColorDialog::ColorDialog(ColorManager *color_manager, const ManagedColor &start,
ColorDialog::ColorDialog(OakEngineColorManager *color_manager, const ManagedColor &start,
QWidget *parent)
: QDialog(parent)
, color_manager_(color_manager)
@@ -142,11 +142,23 @@ void ColorDialog::set_color(const ManagedColor &start)
} else {
// Convert reference color to the input space
ColorProcessorPtr linear_to_input = ColorProcessor::create(
color_manager_, color_manager_->get_reference_color_space(),
start.color_input());
QByteArray ref_cs = oak_query_string([this](char *buf, int size) {
return oakengine_color_manager_reference_color_space(
color_manager_, buf, size);
}).toUtf8();
QByteArray in_cs = start.color_input().toUtf8();
oak_color_transform in_pod;
in_pod.is_display = 0;
in_pod.output = in_cs.constData();
in_pod.view = nullptr;
in_pod.look = nullptr;
ColorProcessorHandlePtr linear_to_input(
oakengine_color_processor_create(color_manager_, ref_cs.constData(),
&in_pod,
OAKENGINE_COLOR_PROCESSOR_NORMAL),
ColorProcessorHandleDeleter());
managed_start = linear_to_input->convert_color(start);
managed_start = oak_convert_color(linear_to_input, start);
}
color_wheel_->set_selected_color(managed_start);
@@ -161,7 +173,7 @@ ManagedColor ColorDialog::get_selected_color() const
// Convert to linear and return a linear color
if (input_to_ref_processor_) {
selected = input_to_ref_processor_->convert_color(selected);
selected = oak_convert_color(input_to_ref_processor_, selected);
}
selected.set_color_input(get_color_space_input());
@@ -183,22 +195,50 @@ ColorTransform ColorDialog::get_color_space_output() const
void ColorDialog::color_space_changed(const QString &input,
const ColorTransform &output)
{
input_to_ref_processor_ = ColorProcessor::create(
color_manager_, input, color_manager_->get_reference_color_space());
QByteArray ref_cs = oak_query_string([this](char *buf, int size) {
return oakengine_color_manager_reference_color_space(
color_manager_, buf, size);
}).toUtf8();
QByteArray in = input.toUtf8();
QByteArray o, v, l;
oak_color_transform out_pod = oak_to_transform(output, &o, &v, &l);
ColorProcessorPtr ref_to_display = ColorProcessor::create(
color_manager_, color_manager_->get_reference_color_space(), output);
auto make_proc = [&](const char *input_cs, const oak_color_transform *dest,
int dir) -> ColorProcessorHandlePtr {
return ColorProcessorHandlePtr(
oakengine_color_processor_create(color_manager_, input_cs, dest,
dir),
ColorProcessorHandleDeleter());
};
ColorProcessorPtr ref_to_input = ColorProcessor::create(
color_manager_, color_manager_->get_reference_color_space(), input);
input_to_ref_processor_ = make_proc(in.constData(), &out_pod,
OAKENGINE_COLOR_PROCESSOR_NORMAL);
oak_color_transform ref_display_pod;
ref_display_pod.is_display = out_pod.is_display;
ref_display_pod.output = out_pod.output;
ref_display_pod.view = out_pod.view;
ref_display_pod.look = out_pod.look;
ColorProcessorHandlePtr ref_to_display = make_proc(
ref_cs.constData(), &ref_display_pod,
OAKENGINE_COLOR_PROCESSOR_NORMAL);
oak_color_transform ref_input_pod;
ref_input_pod.is_display = 0;
ref_input_pod.output = in.constData();
ref_input_pod.view = nullptr;
ref_input_pod.look = nullptr;
ColorProcessorHandlePtr ref_to_input = make_proc(
ref_cs.constData(), &ref_input_pod,
OAKENGINE_COLOR_PROCESSOR_NORMAL);
// Display -> reference is the inverse of the display transform. Older OCIO
// versions crashed on TRANSFORM_DIR_INVERSE; guard by requiring a valid
// processor and fall back to disabling the display tab if creation fails.
ColorProcessorPtr display_to_ref = ColorProcessor::create(
color_manager_, color_manager_->get_reference_color_space(), output,
ColorProcessor::k_inverse);
if (display_to_ref && !display_to_ref->get_processor()) {
ColorProcessorHandlePtr display_to_ref = make_proc(
ref_cs.constData(), &ref_display_pod,
OAKENGINE_COLOR_PROCESSOR_INVERSE);
if (display_to_ref && !oakengine_color_processor_is_valid(display_to_ref.get())) {
display_to_ref = nullptr;
}
+5 -5
View File
@@ -24,8 +24,8 @@
#include <QDialog>
#include "node/color/colormanager/colormanager.h"
#include "render/managedcolor.h"
#include "oakengine/color.h"
#include "widget/manageddisplay/colorprocessorhandle.h"
#include "widget/colorwheel/colorgradientwidget.h"
#include "widget/colorwheel/colorspacechooser.h"
#include "widget/colorwheel/colorswatchchooser.h"
@@ -57,7 +57,7 @@ public:
*
* QWidget parent.
*/
ColorDialog(ColorManager *color_manager,
ColorDialog(OakEngineColorManager *color_manager,
const ManagedColor &start = Color(1.0f, 1.0f, 1.0f),
QWidget *parent = nullptr);
@@ -76,7 +76,7 @@ public slots:
void set_color(const ManagedColor &c);
private:
ColorManager *color_manager_;
OakEngineColorManager *color_manager_;
ColorWheelWidget *color_wheel_;
@@ -84,7 +84,7 @@ private:
ColorGradientWidget *hsv_value_gradient_;
ColorProcessorPtr input_to_ref_processor_;
ColorProcessorHandlePtr input_to_ref_processor_;
ColorSpaceChooser *chooser_;
+3 -2
View File
@@ -27,6 +27,7 @@
#include "core.h"
#include "oakengine/undo.h"
namespace olive
{
@@ -70,13 +71,13 @@ void ConfigDialogBase::accept()
}
}
MultiUndoCommand *command = new MultiUndoCommand();
void *command = oakengine_undo_command_create_multi();
foreach (ConfigDialogBaseTab *tab, tabs_) {
tab->accept(command);
}
Core::instance()->undo_stack()->push(command, tr("Set Configuration"));
oakengine_undo_push(command, tr("Set Configuration").toUtf8().constData());
AcceptEvent();
+2 -3
View File
@@ -24,8 +24,7 @@
#include <QWidget>
#include "config/config.h"
#include "undo/undocommand.h"
#include "common/configwrapper.h"
namespace olive
{
@@ -36,7 +35,7 @@ public:
virtual bool validate();
virtual void accept(MultiUndoCommand *parent) = 0;
virtual void accept(void *parent) = 0;
};
}
+3 -1
View File
@@ -26,6 +26,8 @@
#include <QLabel>
#include <QMessageBox>
#include "oakengine/disk.h"
namespace olive
{
@@ -109,7 +111,7 @@ void DiskCacheDialog::clear_disk_cache(const QString &path, QWidget *parent,
if (clear_btn)
clear_btn->setEnabled(false);
if (DiskManager::instance()->clear_disk_cache(path)) {
if (oakengine_disk_clear_cache(path.toUtf8().constData())) {
if (clear_btn)
clear_btn->setText(tr("Disk Cache Cleared"));
} else {
+7 -5
View File
@@ -89,19 +89,21 @@ AV1Section::AV1Section(int default_crf, QWidget *parent)
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
void AV1Section::add_opts(EncodingParams *params)
void AV1Section::add_opts(OakEngineEncodingParams *params)
{
CompressionMethod method = static_cast<CompressionMethod>(
compression_method_stack_->currentIndex());
if (method == k_constant_rate_factor) {
// Set Quantizer value
params->set_video_option(QStringLiteral("qp"),
QString::number(crf_section_->get_value()));
oakengine_encoding_params_set_video_option(
params, "qp",
QByteArray::number(crf_section_->get_value()).constData());
}
params->set_video_option(QStringLiteral("preset"),
QString::number(preset_combobox_->currentIndex()));
oakengine_encoding_params_set_video_option(
params, "preset",
QByteArray::number(preset_combobox_->currentIndex()).constData());
}
AV1CRFSection::AV1CRFSection(int default_crf, QWidget *parent)
+1 -1
View File
@@ -58,7 +58,7 @@ public:
AV1Section(QWidget *parent = nullptr);
AV1Section(int default_crf, QWidget *parent);
virtual void add_opts(EncodingParams *params) override;
virtual void add_opts(OakEngineEncodingParams *params) override;
private:
QStackedWidget *compression_method_stack_;
+11 -7
View File
@@ -79,17 +79,21 @@ CineformSection::CineformSection(QWidget *parent)
layout->addWidget(quality_combobox_, row, 1);
}
void CineformSection::add_opts(EncodingParams *params)
void CineformSection::add_opts(OakEngineEncodingParams *params)
{
params->set_video_option(
QStringLiteral("quality"),
QString::number(quality_combobox_->currentIndex()));
oakengine_encoding_params_set_video_option(
params, "quality",
QByteArray::number(quality_combobox_->currentIndex()).constData());
}
void CineformSection::set_opts(const EncodingParams *p)
void CineformSection::set_opts(const OakEngineEncodingParams *p)
{
quality_combobox_->setCurrentIndex(
p->video_option(QStringLiteral("quality")).toInt());
char buf[64];
const int ret = oakengine_encoding_params_video_option(
p, "quality", buf, static_cast<int>(sizeof(buf)));
if (ret > 0) {
quality_combobox_->setCurrentIndex(QString::fromUtf8(buf).toInt());
}
}
}
+2 -2
View File
@@ -34,9 +34,9 @@ class CineformSection : public CodecSection {
public:
CineformSection(QWidget *parent = nullptr);
virtual void add_opts(EncodingParams *params) override;
virtual void add_opts(OakEngineEncodingParams *params) override;
virtual void set_opts(const EncodingParams *p) override;
virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QComboBox *quality_combobox_;
+3 -3
View File
@@ -24,7 +24,7 @@
#include <QWidget>
#include "codec/encoder.h"
#include "oakengine/encoding.h"
namespace olive
{
@@ -34,12 +34,12 @@ class CodecSection : public QWidget {
public:
CodecSection(QWidget *parent = nullptr);
virtual void add_opts(EncodingParams *params)
virtual void add_opts(OakEngineEncodingParams *params)
{
Q_UNUSED(params)
}
virtual void set_opts(const EncodingParams *p)
virtual void set_opts(const OakEngineEncodingParams *p)
{
Q_UNUSED(p)
}
+48 -25
View File
@@ -101,7 +101,7 @@ H264Section::H264Section(int default_crf, QWidget *parent)
compression_method_stack_, &QStackedWidget::setCurrentIndex);
}
void H264Section::add_opts(EncodingParams *params)
void H264Section::add_opts(OakEngineEncodingParams *params)
{
// FIXME: Implement two-pass
@@ -110,13 +110,15 @@ void H264Section::add_opts(EncodingParams *params)
// This option is not used by the encoder (nor is anything with the ove_ prefix), it's to help us
// identify which option was chosen when params are restored
params->set_video_option(QStringLiteral("ove_compressionmethod"),
QString::number(method));
oakengine_encoding_params_set_video_option(
params, "ove_compressionmethod",
QByteArray::number(method).constData());
if (method == k_constant_rate_factor) {
// Simply set CRF value
params->set_video_option(QStringLiteral("crf"),
QString::number(crf_section_->get_value()));
oakengine_encoding_params_set_video_option(
params, "crf",
QByteArray::number(crf_section_->get_value()).constData());
} else {
int64_t target_rate, max_rate, min_rate;
@@ -129,40 +131,58 @@ void H264Section::add_opts(EncodingParams *params)
} else {
// Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second)
int64_t target_fs = filesize_section_->get_file_size();
target_rate = qRound64(static_cast<double>(target_fs) /
params->get_export_length().to_double());
int export_len_num = 0, export_len_den = 1;
oakengine_encoding_params_get_export_length(
params, &export_len_num, &export_len_den);
const double export_len_sec =
(export_len_den > 0)
? static_cast<double>(export_len_num)
/ static_cast<double>(export_len_den)
: 1.0;
target_rate = qRound64(static_cast<double>(target_fs) / export_len_sec);
min_rate = target_rate;
max_rate = target_rate;
params->set_video_option(QStringLiteral("ove_targetfilesize"),
QString::number(target_fs));
oakengine_encoding_params_set_video_option(
params, "ove_targetfilesize",
QByteArray::number(target_fs).constData());
}
// Disable CRF encoding
params->set_video_option(QStringLiteral("crf"), QStringLiteral("-1"));
oakengine_encoding_params_set_video_option(params, "crf", "-1");
params->set_video_bit_rate(target_rate);
params->set_video_min_bit_rate(min_rate);
params->set_video_max_bit_rate(max_rate);
params->set_video_buffer_size(2000000);
oakengine_encoding_params_set_video_bit_rate(params, target_rate);
oakengine_encoding_params_set_video_min_bit_rate(params, min_rate);
oakengine_encoding_params_set_video_max_bit_rate(params, max_rate);
oakengine_encoding_params_set_video_buffer_size(params, 2000000);
}
params->set_video_option(QStringLiteral("preset"),
QString::number(preset_combobox_->currentIndex()));
oakengine_encoding_params_set_video_option(
params, "preset",
QByteArray::number(preset_combobox_->currentIndex()).constData());
}
void H264Section::set_opts(const EncodingParams *p)
void H264Section::set_opts(const OakEngineEncodingParams *p)
{
CompressionMethod method = static_cast<CompressionMethod>(
p->video_option(QStringLiteral("ove_compressionmethod")).toInt());
char buf[64];
CompressionMethod method = k_constant_rate_factor;
if (oakengine_encoding_params_video_option(
p, "ove_compressionmethod", buf,
static_cast<int>(sizeof(buf))) > 0) {
method = static_cast<CompressionMethod>(QString::fromUtf8(buf).toInt());
}
compression_method_stack_->setCurrentIndex(method);
if (method == k_constant_rate_factor) {
crf_section_->set_value(p->video_option(QStringLiteral("crf")).toInt());
if (oakengine_encoding_params_video_option(
p, "crf", buf, static_cast<int>(sizeof(buf))) > 0) {
crf_section_->set_value(QString::fromUtf8(buf).toInt());
}
} else {
int64_t target_rate = p->video_bit_rate();
int64_t max_rate = p->video_max_bit_rate();
int64_t target_rate = oakengine_encoding_params_video_bit_rate(p);
int64_t max_rate = oakengine_encoding_params_video_max_bit_rate(p);
if (method == k_target_bit_rate) {
// Use user-supplied values for the bit rate
@@ -170,9 +190,12 @@ void H264Section::set_opts(const EncodingParams *p)
bitrate_section_->set_maximum_bit_rate(max_rate);
} else {
// Calculate the bit rate from the file size divided by the sequence length in seconds (bits per second)
filesize_section_->set_file_size(
p->video_option(QStringLiteral("ove_targetfilesize"))
.toLongLong());
if (oakengine_encoding_params_video_option(
p, "ove_targetfilesize", buf,
static_cast<int>(sizeof(buf))) > 0) {
filesize_section_->set_file_size(
QString::fromUtf8(buf).toLongLong());
}
}
}
}
+2 -2
View File
@@ -100,9 +100,9 @@ public:
H264Section(QWidget *parent = nullptr);
H264Section(int default_crf, QWidget *parent);
virtual void add_opts(EncodingParams *params) override;
virtual void add_opts(OakEngineEncodingParams *params) override;
virtual void set_opts(const EncodingParams *p) override;
virtual void set_opts(const OakEngineEncodingParams *p) override;
private:
QStackedWidget *compression_method_stack_;
+358 -252
View File
@@ -33,16 +33,24 @@
#include "common/digit.h"
#include "common/qtutils.h"
#include "codec/ffmpeg/ffmpegencoder.h"
#include "codec/exportcodec.h"
#include "codec/exportformat.h"
#include "dialog/msgbox.h"
#include "dialog/task/task.h"
#include "exportsavepresetdialog.h"
#include "node/project.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/events.h"
#include "widget/manageddisplay/colorprocessorhandle.h"
#include "widget/viewer/vieweroutpututils.h"
#include "oakengine/exporter.h"
#include "task/taskmanager.h"
#include "oakengine/project.h"
#include "oakengine/task.h"
#include "oakengine/encoding.h"
#include "oakengine/viewer.h"
#include "ui/icons/icons.h"
#include "widget/timeruler/timeruler.h"
#include "common/configwrapper.h"
namespace olive
{
@@ -54,159 +62,126 @@ namespace
// pix_fmt string (e.g. "yuv420p") to its index in the codec's supported
// list; 0 (the codec's preferred format) when absent.
int pix_fmt_index(ExportCodec::Codec codec, const QString &pix_fmt)
int pix_fmt_index(int codec, const QString &pix_fmt)
{
if (pix_fmt.isEmpty()) {
return 0;
}
FFmpegEncoder probe{ EncodingParams() };
const int index = probe.get_pixel_formats_for_codec(codec).indexOf(pix_fmt);
return index >= 0 ? index : 0;
return oakengine_encoding_pix_fmt_index(codec, pix_fmt.toUtf8().constData());
}
// EncodingParams (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 params_to_ex(const EncodingParams &p)
oak_export_options_ex params_to_ex(const OakEngineEncodingParams *p)
{
oak_export_options_ex o = {};
const VideoParams &vp = p.video_params();
const Rational tb = vp.frame_rate().flipped();
int64_t vbrate = 0, abrate = 0;
int asample_rate = 0;
uint64_t ach_layout = 0;
int asample_fmt = 0;
int vthreads = 0;
int scaling = 0;
int is_img_seq = 0;
if (p.has_custom_range()) {
oak_video_params vp = {};
oakengine_encoding_params_get_video_params(p, &vp);
vbrate = oakengine_encoding_params_video_bit_rate(p);
abrate = oakengine_encoding_params_audio_bit_rate(p);
vthreads = oakengine_encoding_params_video_threads(p);
scaling = oakengine_encoding_params_video_scaling_method(p);
is_img_seq = oakengine_encoding_params_video_is_image_sequence(p);
if (oakengine_encoding_params_has_custom_range(p)) {
o.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM;
o.range_in_ts = Timecode::time_to_timestamp(p.custom_range().in(), tb);
int64_t r_in_num = 0, r_in_den = 1, r_out_num = 0, r_out_den = 1;
oakengine_encoding_params_get_custom_range(
p, &r_in_num, &r_in_den, &r_out_num, &r_out_den);
o.range_in_ts =
Timecode::time_to_timestamp(
Rational(r_in_num, r_in_den),
Rational(vp.time_base_num, vp.time_base_den));
o.range_out_ts =
Timecode::time_to_timestamp(p.custom_range().out(), tb);
Timecode::time_to_timestamp(
Rational(r_out_num, r_out_den),
Rational(vp.time_base_num, vp.time_base_den));
} else {
o.range_mode = OAKENGINE_EXPORT_RANGE_ENTIRE;
}
o.format = int(p.format());
o.video_enabled = p.video_enabled() ? 1 : 0;
o.video_codec = int(p.video_codec());
o.audio_enabled = p.audio_enabled() ? 1 : 0;
o.audio_codec = int(p.audio_codec());
o.subtitles_enabled = p.subtitles_enabled() ? 1 : 0;
o.subtitles_sidecar = p.subtitles_are_sidecar() ? 1 : 0;
o.format = oakengine_encoding_params_format(p);
o.video_enabled = oakengine_encoding_params_video_enabled(p) ? 1 : 0;
o.video_codec = oakengine_encoding_params_video_codec(p);
o.audio_enabled = oakengine_encoding_params_audio_enabled(p) ? 1 : 0;
o.audio_codec = oakengine_encoding_params_audio_codec(p);
o.subtitles_enabled = oakengine_encoding_params_subtitles_enabled(p) ? 1 : 0;
o.subtitles_sidecar = oakengine_encoding_params_subtitles_are_sidecar(p) ? 1 : 0;
o.subtitles_format =
p.subtitles_are_sidecar() ? int(p.subtitle_sidecar_fmt()) : 0;
o.subtitles_codec = p.subtitles_enabled() ? int(p.subtitles_codec()) : 0;
oakengine_encoding_params_subtitles_are_sidecar(p)
? oakengine_encoding_params_subtitles_sidecar_format(p)
: 0;
o.subtitles_codec = oakengine_encoding_params_subtitles_enabled(p)
? oakengine_encoding_params_subtitles_codec(p)
: 0;
o.video_bit_rate = p.video_bit_rate();
o.audio_bit_rate = p.audio_bit_rate();
o.video_pix_fmt = pix_fmt_index(p.video_codec(), p.video_pix_fmt());
o.video_bit_rate = vbrate;
o.audio_bit_rate = abrate;
o.audio_sample_rate = p.audio_params().sample_rate();
o.audio_channel_layout = p.audio_params().channel_layout();
o.audio_sample_format = int(p.audio_params().format());
const QString ct = p.color_transform().output();
if (ct.isEmpty()) {
o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE;
} else if (ct == QStringLiteral("sRGB OETF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
} else if (ct == QStringLiteral("Rec.709 OETF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF;
} else if (ct == QStringLiteral("BT.1886 EOTF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF;
char pix_fmt_buf[64];
if (oakengine_encoding_params_video_pix_fmt(
p, pix_fmt_buf, static_cast<int>(sizeof(pix_fmt_buf))) > 0) {
o.video_pix_fmt = oakengine_encoding_pix_fmt_index(
o.video_codec, pix_fmt_buf);
} else {
o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
const QByteArray utf = ct.toUtf8();
snprintf(o.color_transform_name, sizeof(o.color_transform_name),
"%s", utf.constData());
o.video_pix_fmt = 0;
}
o.video_width = vp.width();
o.video_height = vp.height();
o.frame_rate_num = vp.frame_rate().numerator();
o.frame_rate_den = vp.frame_rate().denominator();
o.pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
o.pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
o.interlacing = int(vp.interlacing());
o.pixel_format = int(vp.format());
o.scaling_method = int(p.video_scaling_method());
o.color_range = int(vp.color_range());
o.video_threads = p.video_threads();
o.is_image_sequence = p.video_is_image_sequence() ? 1 : 0;
if (oakengine_encoding_params_get_audio_params(
p, &asample_rate, &ach_layout, &asample_fmt) == OAKENGINE_OK) {
o.audio_sample_rate = asample_rate;
o.audio_channel_layout = ach_layout;
o.audio_sample_format = asample_fmt;
}
char ct_buf[128];
const int ct_ret = oakengine_encoding_params_color_transform_output(
p, ct_buf, static_cast<int>(sizeof(ct_buf)));
if (ct_ret <= 0 || ct_buf[0] == '\0') {
o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE;
} else {
const QString ct = QString::fromUtf8(ct_buf);
if (ct == QStringLiteral("sRGB OETF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
} else if (ct == QStringLiteral("Rec.709 OETF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF;
} else if (ct == QStringLiteral("BT.1886 EOTF")) {
o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF;
} else {
o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
snprintf(o.color_transform_name, sizeof(o.color_transform_name),
"%s", ct_buf);
}
}
o.video_width = vp.width;
o.video_height = vp.height;
o.frame_rate_num = vp.time_base_den; // time_base is frame duration, so rate = den/num
o.frame_rate_den = vp.time_base_num;
o.pixel_aspect_num = vp.pixel_aspect_num;
o.pixel_aspect_den = vp.pixel_aspect_den;
o.interlacing = vp.interlacing;
o.pixel_format = vp.format;
o.scaling_method = scaling;
o.color_range = vp.color_range;
o.video_threads = vthreads;
o.is_image_sequence = is_img_seq;
return o;
}
} // namespace
/**
* @brief ExportTask replacement driven by the liboakengine C ABI facade
*
* Same Task contract as the engine's ExportTask (progress via
* progress_changed, cancel via CancelEvent), but the actual
* render+encode goes through oakengine_export_render_ex(): the facade
* owns the ExportTask instance, its event-loop drive and the conform
* prewarm. Cancellation is forwarded to the facade
* (oakengine_export_cancel()), which reports OAKENGINE_E_CANCELLED back.
*/
class FacadeExportTask : public Task {
public:
FacadeExportTask(ViewerOutput *viewer_node, const EncodingParams &params)
: sequence_(reinterpret_cast<OakEngineSequence *>(viewer_node))
, params_(params)
{
set_title(tr("Exporting \"%1\"").arg(viewer_node->get_label()));
}
protected:
virtual bool run() override
{
oak_export_options_ex o = params_to_ex(params_);
// Pass the codec section's encoder-specific options through.
for (auto it = params_.video_opts().cbegin();
it != params_.video_opts().cend(); ++it) {
oakengine_export_set_video_option(it.key().toUtf8().constData(),
it.value().toUtf8().constData());
}
oakengine_export_set_progress_callback(
&FacadeExportTask::forward_progress, this);
const int rc = oakengine_export_render_ex(
sequence_, params_.filename().toUtf8().constData(), &o);
oakengine_export_set_progress_callback(nullptr, nullptr);
oakengine_export_set_video_option("", nullptr);
if (rc == OAKENGINE_E_CANCELLED) {
// Mirror the engine task's cancelled state for TaskDialog.
cancel();
return false;
}
if (rc != OAKENGINE_OK) {
char err[1024];
err[0] = '\0';
oakengine_export_last_error(err, sizeof(err));
set_error(err[0] ? QString::fromUtf8(err) :
QStringLiteral("Export failed"));
return false;
}
return true;
}
virtual void CancelEvent() override
{
oakengine_export_cancel();
}
private:
static void forward_progress(double fraction, void *userdata)
{
static_cast<FacadeExportTask *>(userdata)->emit_progress(fraction);
}
void emit_progress(double fraction)
{
emit progress_changed(fraction);
}
OakEngineSequence *sequence_;
EncodingParams params_;
};
ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
QWidget *parent)
: super(parent)
@@ -312,16 +287,32 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
preferences_tabs_ = new QTabWidget();
color_manager_ = viewer_node_->project()->color_manager();
color_manager_ = oak_color_manager(viewer_node_->project()->color_manager());
video_tab_ = new ExportVideoTab(color_manager_);
add_preferences_tab(video_tab_, tr("Video"));
// Set video tab time and make connections
connect(viewer_node, &ViewerOutput::playhead_changed, video_tab_,
&ExportVideoTab::set_time);
connect(video_tab_, &ExportVideoTab::time_changed, viewer_node,
&ViewerOutput::set_playhead);
video_tab_->set_time(viewer_node->get_playhead());
viewer_sub_ = oakengine_event_subscribe(
reinterpret_cast<OakEngineNode *>(viewer_node),
OAKENGINE_EVENT_VIEWER_PLAYHEAD_CHANGED,
[](const oakengine_event *event, void *userdata) {
auto *dlg = static_cast<ExportDialog *>(userdata);
auto *tab = dlg->video_tab_;
tab->set_time(Rational(event->a, event->b));
},
this);
connect(video_tab_, &ExportVideoTab::time_changed, this,
[viewer_node](const Rational &time) {
oakengine_viewer_set_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node),
time.numerator(), time.denominator());
});
{
int64_t pn, pd;
oakengine_viewer_get_playhead(
reinterpret_cast<OakEngineNode *>(viewer_node), &pn, &pd);
video_tab_->set_time(Rational(pn, pd));
}
audio_tab_ = new ExportAudioTab();
add_preferences_tab(audio_tab_, tr("Audio"));
@@ -394,11 +385,11 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
set_default_filename();
// Set defaults
previously_selected_format_ = ExportFormat::k_format_mpe_g4_video;
previously_selected_format_ = OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO;
connect(format_combobox_, &ExportFormatComboBox::format_changed, this,
&ExportDialog::format_changed);
VideoParams vp = viewer_node_->get_video_params();
VideoParams vp = viewer_output_video_params(viewer_node_);
video_aspect_ratio_ =
static_cast<double>(vp.width()) / static_cast<double>(vp.height());
@@ -430,7 +421,8 @@ ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
// If the viewer already has cached params, use them
if (!stills_only_mode_ &&
viewer_node_->get_last_used_encoding_params().is_valid()) {
oakengine_encoding_params_get_last_used(
reinterpret_cast<OakEngineSequence *>(viewer_node_)) != nullptr) {
// This will automatically set the param data
QtUtils::set_combo_box_data(preset_combobox_, k_preset_last_used);
} else {
@@ -477,8 +469,9 @@ void ExportDialog::start_export()
// Validate if the entered filename contains the correct extension (the extension is necessary
// for both FFmpeg and OIIO to determine the output format)
QString necessary_ext = QStringLiteral(".%1").arg(
ExportFormat::get_extension(format_combobox_->get_format()));
char ext_buf[64];
int ext_len = oakengine_encoding_format_extension(format_combobox_->get_format(), ext_buf, sizeof(ext_buf));
QString necessary_ext = QStringLiteral(".%1").arg(QString::fromUtf8(ext_buf, ext_len));
QString proposed_filename = filename_edit_->text().trimmed();
// If it doesn't, see if the user wants to append it automatically. If not, we don't abort the export.
@@ -513,7 +506,7 @@ void ExportDialog::start_export()
// Validate if this is an image sequence and if the filename contains enough digits
if (video_tab_->is_image_sequence_set()) {
// Ensure filename contains digits
if (!Encoder::filename_contains_digit_placeholder(proposed_filename)) {
if (!oakengine_encoding_filename_contains_digit_placeholder(proposed_filename.toUtf8().constData())) {
msg_box(
this, QMessageBox::Critical, tr("Invalid filename"),
tr("Export is set to an image sequence, but the filename does not have a section for digits "
@@ -524,7 +517,7 @@ void ExportDialog::start_export()
int64_t frame_count = get_export_length_in_timebase_units();
int64_t needed_digit_count = get_digit_count(frame_count);
int current_digit_count =
Encoder::get_image_sequence_placeholder_digit_count(proposed_filename);
oakengine_encoding_image_sequence_digit_count(proposed_filename.toUtf8().constData());
if (current_digit_count < needed_digit_count) {
msg_box(
this, QMessageBox::Critical, tr("Invalid filename"),
@@ -549,8 +542,8 @@ void ExportDialog::start_export()
// Validate video resolution
if (video_enabled_->isChecked() &&
(video_tab_->get_selected_codec() == ExportCodec::k_codec_h264 ||
video_tab_->get_selected_codec() == ExportCodec::k_codec_h265) &&
(video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H264 ||
video_tab_->get_selected_codec() == OAKENGINE_ENCODING_CODEC_H265) &&
(video_tab_->width_slider()->get_value() % 2 != 0 ||
video_tab_->height_slider()->get_value() % 2 != 0)) {
msg_box(this, QMessageBox::Critical, tr("Invalid Parameters"),
@@ -558,12 +551,13 @@ void ExportDialog::start_export()
return;
}
FacadeExportTask *task =
new FacadeExportTask(viewer_node_, generate_params());
OakEngineTask *task = oakengine_task_create_export(
reinterpret_cast<OakEngineSequence *>(viewer_node_),
generate_params());
if (export_bkg_box_->isChecked()) {
// Send to TaskManager to export in background
TaskManager::instance()->add_task(task);
oakengine_task_manager_add(task);
this->accept();
} else {
// Use modal dialog box
@@ -578,7 +572,7 @@ void ExportDialog::export_finished()
{
TaskDialog *td = static_cast<TaskDialog *>(sender());
if (td->get_task()->is_cancelled()) {
if (oakengine_task_is_cancelled(td->get_task())) {
// If this task was cancelled, we stay open so the user can potentially queue another export
} else {
// Accept this dialog and close
@@ -600,11 +594,14 @@ void ExportDialog::image_sequence_check_box_changed(bool e)
QString suffix = current_fileinfo.suffix();
if (e) {
if (!Encoder::filename_contains_digit_placeholder(basename)) {
if (!oakengine_encoding_filename_contains_digit_placeholder(basename.toUtf8().constData())) {
basename.append(QStringLiteral("_[#####]"));
}
} else {
basename = Encoder::filename_remove_digit_placeholder(basename);
char buf[1024];
oakengine_encoding_filename_remove_digit_placeholder(
basename.toUtf8().constData(), buf, sizeof(buf));
basename = QString::fromUtf8(buf);
}
// Set filename
@@ -636,7 +633,14 @@ void ExportDialog::preset_combo_box_changed()
if (preset_number == k_preset_default) {
set_defaults();
} else if (preset_number == k_preset_last_used) {
set_params(viewer_node_->get_last_used_encoding_params());
OakEngineEncodingParams *last =
oakengine_encoding_params_get_last_used(
reinterpret_cast<OakEngineSequence *>(viewer_node_));
if (last) {
set_params(last);
} else {
set_defaults();
}
} else {
set_params(presets_.at(preset_number));
}
@@ -653,12 +657,17 @@ void ExportDialog::add_preferences_tab(QWidget *inner_widget,
void ExportDialog::browse_filename()
{
ExportFormat::Format f = format_combobox_->get_format();
int f = format_combobox_->get_format();
char name_buf[256];
char ext_buf[64];
oakengine_encoding_format_name(f, name_buf, sizeof(name_buf));
oakengine_encoding_format_extension(f, ext_buf, sizeof(ext_buf));
QString browsed_fn = QFileDialog::getSaveFileName(
this, "", filename_edit_->text().trimmed(),
QStringLiteral("%1 (*.%2)")
.arg(ExportFormat::get_name(f), ExportFormat::get_extension(f)),
.arg(QString::fromUtf8(name_buf), QString::fromUtf8(ext_buf)),
nullptr,
// We don't confirm overwrite here because we do it later
@@ -669,12 +678,14 @@ void ExportDialog::browse_filename()
}
}
void ExportDialog::format_changed(ExportFormat::Format current_format)
void ExportDialog::format_changed(int current_format)
{
QString current_filename = filename_edit_->text().trimmed();
QString previously_selected_ext =
ExportFormat::get_extension(previously_selected_format_);
QString currently_selected_ext = ExportFormat::get_extension(current_format);
char ext_buf[64];
oakengine_encoding_format_extension(previously_selected_format_, ext_buf, sizeof(ext_buf));
QString previously_selected_ext = QString::fromUtf8(ext_buf);
oakengine_encoding_format_extension(current_format, ext_buf, sizeof(ext_buf));
QString currently_selected_ext = QString::fromUtf8(ext_buf);
// If the previous extension was added, remove it
if (current_filename.endsWith(previously_selected_ext,
@@ -742,25 +753,45 @@ void ExportDialog::load_presets()
preset_combobox_->addItem(tr("Default"), k_preset_default);
if (viewer_node_->get_last_used_encoding_params().is_valid()) {
if (oakengine_encoding_params_get_last_used(
reinterpret_cast<OakEngineSequence *>(viewer_node_)) != nullptr) {
preset_combobox_->addItem(tr("Last Used"), k_preset_last_used);
}
preset_combobox_->insertSeparator(preset_combobox_->count());
QStringList l = EncodingParams::get_list_of_presets();
QStringList l;
{
const int n = oakengine_encoding_preset_count();
for (int i = 0; i < n; i++) {
char name_buf[256];
if (oakengine_encoding_preset_name(
i, name_buf, static_cast<int>(sizeof(name_buf))) > 0) {
l.append(QString::fromUtf8(name_buf));
}
}
}
presets_.reserve(l.size());
for (const QString &preset : l) {
EncodingParams p;
OakEngineEncodingParams *p = oakengine_encoding_params_create();
QFile f(EncodingParams::get_preset_path().filePath(preset));
if (f.open(QFile::ReadOnly)) {
if (p.load(&f)) {
preset_combobox_->addItem(preset, int(presets_.size()));
presets_.push_back(p);
}
f.close();
char preset_path_buf[1024];
preset_path_buf[0] = '\0';
oakengine_encoding_preset_path(
preset_path_buf, static_cast<int>(sizeof(preset_path_buf)));
const QByteArray preset_path_utf =
QDir(QString::fromUtf8(preset_path_buf))
.filePath(preset)
.toUtf8();
const int rc = oakengine_encoding_params_load_file(
p, preset_path_utf.constData());
if (rc == OAKENGINE_OK) {
preset_combobox_->addItem(preset, int(presets_.size()));
presets_.push_back(p);
} else {
oakengine_encoding_params_destroy(p);
}
}
@@ -771,13 +802,17 @@ void ExportDialog::set_default_filename()
{
Project *p = viewer_node_->project();
char fn_buf[512];
oakengine_project_filename(
reinterpret_cast<OakEngineProject *>(p),
fn_buf, sizeof(fn_buf));
QDir doc_location;
if (p->filename().isEmpty()) {
if (fn_buf[0] == '\0') {
doc_location.setPath(QStandardPaths::writableLocation(
QStandardPaths::DocumentsLocation));
} else {
doc_location = QFileInfo(p->filename()).dir();
doc_location = QFileInfo(fn_buf).dir();
}
QString file_location = doc_location.filePath(viewer_node_->get_label());
@@ -801,14 +836,14 @@ bool ExportDialog::sequence_has_subtitles() const
void ExportDialog::set_defaults()
{
if (!stills_only_mode_) {
format_combobox_->set_format(ExportFormat::k_format_mpe_g4_video);
format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_MPEG4_VIDEO);
} else {
format_combobox_->set_format(ExportFormat::k_format_png);
format_combobox_->set_format(OAKENGINE_ENCODING_FORMAT_PNG);
}
format_changed(format_combobox_->get_format());
VideoParams vp = viewer_node_->get_video_params();
AudioParams ap = viewer_node_->get_audio_params();
VideoParams vp = viewer_output_video_params(viewer_node_);
AudioParams ap = viewer_output_audio_params(viewer_node_);
video_tab_->width_slider()->set_value(vp.width());
video_tab_->width_slider()->SetDefaultValue(vp.width());
@@ -826,151 +861,217 @@ void ExportDialog::set_defaults()
audio_tab_->channel_layout_combobox()->set_channel_layout(
ap.channel_layout());
subtitles_enabled_->setChecked(sequence_has_subtitles());
subtitle_tab_->set_sidecar_format(ExportFormat::k_format_srt);
subtitle_tab_->set_sidecar_format(OAKENGINE_ENCODING_FORMAT_SRT);
}
EncodingParams ExportDialog::generate_params() const
OakEngineEncodingParams *ExportDialog::generate_params() const
{
VideoParams video_render_params(
static_cast<int>(video_tab_->width_slider()->get_value()),
static_cast<int>(video_tab_->height_slider()->get_value()),
get_selected_timebase(),
video_tab_->pixel_format_field()->get_pixel_format(),
VideoParams::k_internal_channel_count,
video_tab_->pixel_aspect_combobox()->get_pixel_aspect_ratio(),
video_tab_->interlaced_combobox()->get_interlace_mode(), 1);
OakEngineEncodingParams *params = oakengine_encoding_params_create();
AudioParams audio_render_params(
audio_tab_->sample_rate_combobox()->get_sample_rate(),
audio_tab_->channel_layout_combobox()->get_channel_layout(),
audio_tab_->sample_format_combobox()->get_sample_format());
oakengine_encoding_params_set_format(
params, format_combobox_->get_format());
oakengine_encoding_params_set_filename(
params, filename_edit_->text().trimmed().toUtf8().constData());
EncodingParams params;
params.set_format(format_combobox_->get_format());
params.set_filename(filename_edit_->text().trimmed());
params.set_export_length(viewer_node_->get_length());
const Rational export_len = viewer_node_->get_length();
oakengine_encoding_params_set_export_length(
params, export_len.numerator(), export_len.denominator());
if (ExportCodec::is_codec_a_still_image(video_tab_->get_selected_codec()) &&
if (oakengine_encoding_codec_is_still_image(video_tab_->get_selected_codec()) &&
!video_tab_->is_image_sequence_set()) {
// Exporting as image without exporting image sequence, only export one frame
Rational export_time = video_tab_->get_still_image_time();
params.set_custom_range(
TimeRange(export_time, export_time + get_selected_timebase()));
const Rational tb = get_selected_timebase();
oakengine_encoding_params_set_custom_range(
params, export_time.numerator(), export_time.denominator(),
(export_time + tb).numerator(),
(export_time + tb).denominator());
} else if (range_combobox_->currentIndex() == k_range_in_to_out) {
// Assume if this combobox is enabled, workarea is enabled - a check that we make in this dialog's constructor
params.set_custom_range(viewer_node_->get_work_area()->range());
const TimeRange &r = viewer_node_->get_work_area()->range();
oakengine_encoding_params_set_custom_range(
params, r.in().numerator(), r.in().denominator(),
r.out().numerator(), r.out().denominator());
}
if (video_tab_->scaling_method_combobox()->isEnabled()) {
params.set_video_scaling_method(
static_cast<EncodingParams::VideoScalingMethod>(
video_tab_->scaling_method_combobox()->currentData().toInt()));
oakengine_encoding_params_set_video_scaling_method(
params,
video_tab_->scaling_method_combobox()->currentData().toInt());
}
if (video_enabled_->isChecked()) {
ExportCodec::Codec video_codec = video_tab_->get_selected_codec();
const int video_codec = video_tab_->get_selected_codec();
video_render_params.set_color_range(video_tab_->color_range());
// Build video params from the tab
const int vw = static_cast<int>(video_tab_->width_slider()->get_value());
const int vh = static_cast<int>(video_tab_->height_slider()->get_value());
const Rational tb = get_selected_timebase();
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 int interlace = video_tab_->interlaced_combobox()->get_interlace_mode();
params.enable_video(video_render_params, video_codec);
oak_video_params vp = {};
vp.width = vw;
vp.height = vh;
vp.time_base_num = tb.numerator();
vp.time_base_den = tb.denominator();
vp.format = pix_fmt;
vp.pixel_aspect_num = par.numerator();
vp.pixel_aspect_den = par.denominator();
vp.interlacing = interlace;
vp.color_range = video_tab_->color_range();
params.set_video_threads(video_tab_->threads());
oakengine_encoding_params_enable_video(params, &vp, video_codec);
oakengine_encoding_params_set_video_threads(
params, video_tab_->threads());
if (video_tab_->isVisible()) {
video_tab_->get_codec_section()->add_opts(&params);
video_tab_->get_codec_section()->add_opts(params);
}
params.set_color_transform(video_tab_->current_ocio_color_space());
{
const QString ct = video_tab_->current_ocio_color_space();
oakengine_encoding_params_set_color_transform(
params, ct.isEmpty() ? nullptr : ct.toUtf8().constData());
}
params.set_video_pix_fmt(video_tab_->pix_fmt());
{
const QString pix_fmt_name = video_tab_->pix_fmt();
oakengine_encoding_params_set_video_pix_fmt(
params,
pix_fmt_name.isEmpty() ? nullptr
: pix_fmt_name.toUtf8().constData());
}
params.set_video_is_image_sequence(video_tab_->is_image_sequence_set());
oakengine_encoding_params_set_video_is_image_sequence(
params, video_tab_->is_image_sequence_set() ? 1 : 0);
}
if (audio_enabled_->isChecked()) {
ExportCodec::Codec audio_codec = audio_tab_->get_codec();
params.enable_audio(audio_render_params, audio_codec);
const int audio_codec = audio_tab_->get_codec();
const int sample_rate = audio_tab_->sample_rate_combobox()->get_sample_rate();
const uint64_t ch_layout = audio_tab_->channel_layout_combobox()->get_channel_layout();
const int sample_fmt = audio_tab_->sample_format_combobox()->get_sample_format();
params.set_audio_bit_rate(audio_tab_->bit_rate_slider()->get_value() *
1000);
oakengine_encoding_params_enable_audio(
params, sample_rate, ch_layout, sample_fmt, audio_codec);
oakengine_encoding_params_set_audio_bit_rate(
params,
audio_tab_->bit_rate_slider()->get_value() * 1000);
}
if (subtitles_enabled_->isEnabled() && subtitles_enabled_->isChecked()) {
if (!subtitle_tab_->get_sidecar_enabled()) {
// Export subtitles embedded in container
params.enable_subtitles(subtitle_tab_->get_subtitle_codec());
oakengine_encoding_params_enable_subtitles(
params, subtitle_tab_->get_subtitle_codec());
} else {
// Export subtitles to a sidecar file
params.enable_sidecar_subtitles(subtitle_tab_->get_sidecar_format(),
subtitle_tab_->get_subtitle_codec());
oakengine_encoding_params_enable_sidecar_subtitles(
params, subtitle_tab_->get_sidecar_format(),
subtitle_tab_->get_subtitle_codec());
}
}
return params;
}
void ExportDialog::set_params(const EncodingParams &e)
void ExportDialog::set_params(const OakEngineEncodingParams *e)
{
format_combobox_->set_format(e.format());
format_combobox_->set_format(oakengine_encoding_params_format(e));
format_changed(format_combobox_->get_format());
if (e.has_custom_range() && viewer_node_->get_work_area()->enabled()) {
if (oakengine_encoding_params_has_custom_range(e) &&
viewer_node_->get_work_area()->enabled()) {
range_combobox_->setCurrentIndex(k_range_in_to_out);
}
QtUtils::set_combo_box_data(video_tab_->scaling_method_combobox(),
e.video_scaling_method());
oakengine_encoding_params_video_scaling_method(e));
video_enabled_->setChecked(e.video_enabled());
if (e.video_enabled()) {
video_tab_->width_slider()->set_value(e.video_params().width());
video_tab_->height_slider()->set_value(e.video_params().height());
set_selected_timebase(e.video_params().time_base());
const int video_enabled = oakengine_encoding_params_video_enabled(e);
video_enabled_->setChecked(video_enabled);
if (video_enabled) {
oak_video_params vp = {};
oakengine_encoding_params_get_video_params(e, &vp);
video_tab_->width_slider()->set_value(vp.width);
video_tab_->height_slider()->set_value(vp.height);
set_selected_timebase(Rational(vp.time_base_num, vp.time_base_den));
video_tab_->pixel_format_field()->set_pixel_format(
e.video_params().format());
static_cast<olive::core::PixelFormat::Format>(vp.format));
video_tab_->pixel_aspect_combobox()->set_pixel_aspect_ratio(
e.video_params().pixel_aspect_ratio());
video_tab_->interlaced_combobox()->set_interlace_mode(
e.video_params().interlacing());
Rational(vp.pixel_aspect_num, vp.pixel_aspect_den));
video_tab_->interlaced_combobox()->set_interlace_mode(vp.interlacing);
video_tab_->set_selected_codec(e.video_codec());
video_tab_->set_selected_codec(oakengine_encoding_params_video_codec(e));
video_tab_->set_color_range(e.video_params().color_range());
video_tab_->set_color_range(vp.color_range);
video_tab_->set_threads(e.video_threads());
video_tab_->set_threads(oakengine_encoding_params_video_threads(e));
if (video_tab_->isVisible()) {
video_tab_->get_codec_section()->set_opts(&e);
video_tab_->get_codec_section()->set_opts(e);
}
video_tab_->set_ocio_color_space(e.color_transform().output());
{
char ct_buf[128];
if (oakengine_encoding_params_color_transform_output(
e, ct_buf, static_cast<int>(sizeof(ct_buf))) > 0) {
video_tab_->set_ocio_color_space(QString::fromUtf8(ct_buf));
} else {
video_tab_->set_ocio_color_space(QString());
}
}
video_tab_->set_pix_fmt(e.video_pix_fmt());
{
char pix_fmt_buf[64];
if (oakengine_encoding_params_video_pix_fmt(
e, pix_fmt_buf, static_cast<int>(sizeof(pix_fmt_buf))) > 0) {
video_tab_->set_pix_fmt(QString::fromUtf8(pix_fmt_buf));
} else {
video_tab_->set_pix_fmt(QString());
}
}
video_tab_->set_image_sequence(e.video_is_image_sequence());
video_tab_->set_image_sequence(
oakengine_encoding_params_video_is_image_sequence(e));
}
audio_enabled_->setChecked(e.audio_enabled());
if (e.audio_enabled()) {
audio_tab_->sample_rate_combobox()->set_sample_rate(
e.audio_params().sample_rate());
audio_tab_->channel_layout_combobox()->set_channel_layout(
e.audio_params().channel_layout());
const int audio_enabled = oakengine_encoding_params_audio_enabled(e);
audio_enabled_->setChecked(audio_enabled);
if (audio_enabled) {
int asample_rate = 0;
uint64_t ach_layout = 0;
int asample_fmt = 0;
oakengine_encoding_params_get_audio_params(
e, &asample_rate, &ach_layout, &asample_fmt);
audio_tab_->sample_rate_combobox()->set_sample_rate(asample_rate);
audio_tab_->channel_layout_combobox()->set_channel_layout(ach_layout);
audio_tab_->sample_format_combobox()->set_sample_format(
e.audio_params().format());
static_cast<olive::core::SampleFormat::Format>(asample_fmt));
audio_tab_->set_codec(e.audio_codec());
audio_tab_->set_codec(oakengine_encoding_params_audio_codec(e));
audio_tab_->bit_rate_slider()->set_value(e.audio_bit_rate() / 1000);
audio_tab_->bit_rate_slider()->set_value(
oakengine_encoding_params_audio_bit_rate(e) / 1000);
}
if (subtitles_enabled_->isEnabled()) {
subtitles_enabled_->setChecked(e.subtitles_enabled());
subtitle_tab_->set_sidecar_enabled(e.subtitles_are_sidecar());
if (e.subtitles_enabled()) {
subtitle_tab_->set_subtitle_codec(e.subtitles_codec());
if (e.subtitles_are_sidecar()) {
subtitle_tab_->set_sidecar_format(e.subtitle_sidecar_fmt());
const int subs_enabled = oakengine_encoding_params_subtitles_enabled(e);
subtitles_enabled_->setChecked(subs_enabled);
subtitle_tab_->set_sidecar_enabled(
oakengine_encoding_params_subtitles_are_sidecar(e));
if (subs_enabled) {
subtitle_tab_->set_subtitle_codec(
oakengine_encoding_params_subtitles_codec(e));
if (oakengine_encoding_params_subtitles_are_sidecar(e)) {
subtitle_tab_->set_sidecar_format(
oakengine_encoding_params_subtitles_sidecar_format(e));
}
}
}
@@ -997,7 +1098,10 @@ void ExportDialog::done(int r)
preview_viewer_->connect_viewer_node(nullptr);
if (!stills_only_mode_) {
viewer_node_->set_last_used_encoding_params(generate_params());
OakEngineEncodingParams *p = generate_params();
oakengine_encoding_params_set_last_used(
reinterpret_cast<OakEngineSequence *>(viewer_node_), p);
oakengine_encoding_params_destroy(p);
}
super::done(r);
@@ -1024,14 +1128,16 @@ void ExportDialog::update_viewer_dimensions()
static_cast<int>(video_tab_->width_slider()->get_value()),
static_cast<int>(video_tab_->height_slider()->get_value()));
VideoParams vp = viewer_node_->get_video_params();
VideoParams vp = viewer_output_video_params(viewer_node_);
QMatrix4x4 transform = EncodingParams::generate_matrix(
static_cast<EncodingParams::VideoScalingMethod>(
video_tab_->scaling_method_combobox()->currentData().toInt()),
float mat16[16];
oakengine_encoding_generate_matrix(
video_tab_->scaling_method_combobox()->currentData().toInt(),
vp.width(), vp.height(),
static_cast<int>(video_tab_->width_slider()->get_value()),
static_cast<int>(video_tab_->height_slider()->get_value()));
static_cast<int>(video_tab_->height_slider()->get_value()),
mat16);
QMatrix4x4 transform(mat16);
preview_viewer_->set_matrix(transform);
}
+10 -8
View File
@@ -24,17 +24,17 @@
#include <QComboBox>
#include <QDialog>
#include <cstdint>
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QProgressBar>
#include "codec/encoder.h"
#include "codec/exportcodec.h"
#include "codec/exportformat.h"
#include "dialog/export/exportformatcombobox.h"
#include "exportaudiotab.h"
#include "exportsubtitlestab.h"
#include "exportvideotab.h"
#include "oakengine/encoding.h"
#include "widget/nodeparamview/nodeparamviewwidgetbridge.h"
#include "widget/viewer/viewer.h"
@@ -54,8 +54,8 @@ public:
Rational get_selected_timebase() const;
void set_selected_timebase(const Rational &r);
EncodingParams generate_params() const;
void set_params(const EncodingParams &e);
OakEngineEncodingParams *generate_params() const;
void set_params(const OakEngineEncodingParams *e);
virtual bool eventFilter(QObject *o, QEvent *e) override;
@@ -77,7 +77,9 @@ private:
ViewerOutput *viewer_node_;
ExportFormat::Format previously_selected_format_;
int64_t viewer_sub_ = 0;
int previously_selected_format_;
Rational get_export_length() const;
int64_t get_export_length_in_timebase_units() const;
@@ -93,7 +95,7 @@ private:
QComboBox *preset_combobox_;
QComboBox *range_combobox_;
std::vector<EncodingParams> presets_;
std::vector<OakEngineEncodingParams *> presets_;
QCheckBox *video_enabled_;
QCheckBox *audio_enabled_;
@@ -109,7 +111,7 @@ private:
double video_aspect_ratio_;
ColorManager *color_manager_;
OakEngineColorManager *color_manager_;
QWidget *preferences_area_;
QCheckBox *export_bkg_box_;
@@ -122,7 +124,7 @@ private:
private slots:
void browse_filename();
void format_changed(ExportFormat::Format current_format);
void format_changed(int current_format);
void resolution_changed();
@@ -54,13 +54,13 @@ public:
pixel_format_combobox_->setCurrentText(s);
}
VideoParams::ColorRange yuv_range() const
int yuv_range() const
{
return static_cast<VideoParams::ColorRange>(
return static_cast<int>(
yuv_color_range_combobox_->currentIndex());
}
void set_yuv_range(VideoParams::ColorRange i)
void set_yuv_range(int i)
{
yuv_color_range_combobox_->setCurrentIndex(i);
}
+21 -8
View File
@@ -24,6 +24,9 @@
#include <QGridLayout>
#include <QLabel>
#include <olive/core/core.h>
#include "oakengine/encoding.h"
namespace olive
{
@@ -87,14 +90,17 @@ ExportAudioTab::ExportAudioTab(QWidget *parent)
outer_layout->addStretch();
}
int ExportAudioTab::set_format(ExportFormat::Format format)
int ExportAudioTab::set_format(int format)
{
QList<ExportCodec::Codec> acodecs = ExportFormat::get_audio_codecs(format);
setEnabled(!acodecs.isEmpty());
const int acodec_count = oakengine_encoding_format_audio_codec_count(format);
setEnabled(acodec_count > 0);
codec_combobox_->blockSignals(true);
codec_combobox_->clear();
foreach (ExportCodec::Codec acodec, acodecs) {
codec_combobox_->addItem(ExportCodec::get_codec_name(acodec), acodec);
for (int i = 0; i < acodec_count; i++) {
int codec = oakengine_encoding_format_audio_codec_at(format, i);
char buf[256];
oakengine_encoding_codec_name(codec, buf, sizeof(buf));
codec_combobox_->addItem(QString::fromUtf8(buf), codec);
}
codec_combobox_->blockSignals(false);
fmt_ = format;
@@ -102,18 +108,25 @@ int ExportAudioTab::set_format(ExportFormat::Format format)
update_sample_formats();
update_bit_rate_enabled();
return acodecs.size();
return acodec_count;
}
void ExportAudioTab::update_sample_formats()
{
auto fmts = ExportFormat::get_sample_formats_for_codec(fmt_, get_codec());
// Use oakengine to get sample format values and build the vector
const int count = oakengine_encoding_sample_format_count(fmt_, get_codec());
std::vector<olive::core::SampleFormat> fmts;
fmts.reserve(count);
for (int i = 0; i < count; i++) {
int val = oakengine_encoding_sample_format_at(fmt_, get_codec(), i);
fmts.push_back(olive::core::SampleFormat(static_cast<olive::core::SampleFormat::Format>(val)));
}
sample_format_combobox_->set_available_formats(fmts);
}
void ExportAudioTab::update_bit_rate_enabled()
{
bool uses_bitrate = !ExportCodec::is_codec_lossless(get_codec());
bool uses_bitrate = !oakengine_encoding_codec_is_lossless(get_codec());
bit_rate_slider_->setEnabled(uses_bitrate);
if (!uses_bitrate) {
+5 -7
View File
@@ -26,7 +26,6 @@
#include <QWidget>
#include "common/define.h"
#include "codec/exportformat.h"
#include "widget/slider/integerslider.h"
#include "widget/standardcombos/standardcombos.h"
@@ -38,13 +37,12 @@ class ExportAudioTab : public QWidget {
public:
ExportAudioTab(QWidget *parent = nullptr);
ExportCodec::Codec get_codec() const
int get_codec() const
{
return static_cast<ExportCodec::Codec>(
codec_combobox_->currentData().toInt());
return codec_combobox_->currentData().toInt();
}
void set_codec(ExportCodec::Codec c)
void set_codec(int c)
{
for (int i = 0; i < codec_combobox_->count(); i++) {
if (codec_combobox_->itemData(i) == c) {
@@ -75,10 +73,10 @@ public:
}
public slots:
int set_format(ExportFormat::Format format);
int set_format(int format);
private:
ExportFormat::Format fmt_;
int fmt_;
QComboBox *codec_combobox_;
SampleRateComboBox *sample_rate_combobox_;
ChannelLayoutComboBox *channel_layout_combobox_;
+23 -16
View File
@@ -24,6 +24,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include "oakengine/encoding.h"
#include "ui/icons/icons.h"
namespace olive
@@ -32,6 +33,10 @@ namespace olive
ExportFormatComboBox::ExportFormatComboBox(Mode mode, QWidget *parent)
: QComboBox(parent)
{
// The invalid placeholder format is the format count itself
// (ExportFormat::k_format_count), not -1.
current_ = oakengine_encoding_format_count();
custom_menu_ = new Menu(this);
// Populate combobox formats
@@ -69,43 +74,45 @@ void ExportFormatComboBox::showPopup()
custom_menu_->exec(mapToGlobal(QPoint(0, 0)));
}
void ExportFormatComboBox::set_format(ExportFormat::Format fmt)
void ExportFormatComboBox::set_format(int fmt)
{
current_ = fmt;
clear();
addItem(ExportFormat::get_name(current_));
char buf[256];
oakengine_encoding_format_name(fmt, buf, sizeof(buf));
addItem(QString::fromUtf8(buf));
}
void ExportFormatComboBox::handle_index_change(QAction *a)
{
ExportFormat::Format f =
static_cast<ExportFormat::Format>(a->data().toInt());
int f = a->data().toInt();
set_format(f);
emit format_changed(f);
}
void ExportFormatComboBox::populate_type(Track::Type type)
{
for (int i = 0; i < ExportFormat::k_format_count; i++) {
ExportFormat::Format f = static_cast<ExportFormat::Format>(i);
const int fmt_count = oakengine_encoding_format_count();
for (int i = 0; i < fmt_count; i++) {
int f = i;
char buf[256];
if (type == Track::k_video &&
!ExportFormat::get_video_codecs(f).isEmpty()) {
bool has_video = oakengine_encoding_format_video_codec_count(f) > 0;
bool has_audio = oakengine_encoding_format_audio_codec_count(f) > 0;
bool has_sub = oakengine_encoding_format_subtitle_codec_count(f) > 0;
if (type == Track::k_video && has_video) {
// Do nothing
} else if (type == Track::k_audio &&
ExportFormat::get_video_codecs(f).isEmpty() &&
!ExportFormat::get_audio_codecs(f).isEmpty()) {
} else if (type == Track::k_audio && !has_video && has_audio) {
// Do nothing
} else if (type == Track::k_subtitle &&
ExportFormat::get_video_codecs(f).isEmpty() &&
ExportFormat::get_audio_codecs(f).isEmpty() &&
!ExportFormat::get_subtitle_codecs(f).isEmpty()) {
} else if (type == Track::k_subtitle && !has_video && !has_audio && has_sub) {
// Do nothing
} else {
continue;
}
QString format_name = ExportFormat::get_name(f);
oakengine_encoding_format_name(f, buf, sizeof(buf));
QString format_name = QString::fromUtf8(buf);
QAction *a = custom_menu_->addAction(format_name);
a->setData(i);
+4 -5
View File
@@ -25,7 +25,6 @@
#include <QComboBox>
#include <QWidgetAction>
#include "codec/exportformat.h"
#include "node/output/track/track.h"
#include "widget/menu/menu.h"
@@ -48,7 +47,7 @@ public:
{
}
ExportFormat::Format get_format() const
int get_format() const
{
return current_;
}
@@ -56,10 +55,10 @@ public:
void showPopup();
signals:
void format_changed(ExportFormat::Format fmt);
void format_changed(int fmt);
public slots:
void set_format(ExportFormat::Format fmt);
void set_format(int fmt);
private slots:
void handle_index_change(QAction *a);
@@ -71,7 +70,7 @@ private:
Menu *custom_menu_;
ExportFormat::Format current_ = ExportFormat::k_format_count;
int current_ = -1; // was ExportFormat::k_format_count
};
}
+27 -11
View File
@@ -22,6 +22,7 @@
#include "exportsavepresetdialog.h"
#include <QDialogButtonBox>
#include <QDir>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
@@ -29,7 +30,7 @@
namespace olive
{
ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p,
ExportSavePresetDialog::ExportSavePresetDialog(const OakEngineEncodingParams *p,
QWidget *parent)
: QDialog(parent)
, params_(p)
@@ -39,7 +40,17 @@ ExportSavePresetDialog::ExportSavePresetDialog(const EncodingParams &p,
name_edit_ = new QLineEdit();
// Populate existing list
QStringList l = EncodingParams::get_list_of_presets();
QStringList l;
{
const int n = oakengine_encoding_preset_count();
for (int i = 0; i < n; i++) {
char name_buf[256];
if (oakengine_encoding_preset_name(
i, name_buf, static_cast<int>(sizeof(name_buf))) > 0) {
l.append(QString::fromUtf8(name_buf));
}
}
}
if (!l.empty()) {
auto list_widget = new QListWidget();
for (const QString &f : l) {
@@ -78,13 +89,17 @@ void ExportSavePresetDialog::accept()
return;
}
QDir d(EncodingParams::get_preset_path());
char preset_path_buf[1024];
preset_path_buf[0] = '\0';
oakengine_encoding_preset_path(
preset_path_buf, static_cast<int>(sizeof(preset_path_buf)));
QDir d(QString::fromUtf8(preset_path_buf));
if (!d.exists()) {
d.mkpath(QStringLiteral("."));
}
QFile f(d.filePath(name_edit_->text()));
if (f.exists()) {
if (d.exists(name_edit_->text())) {
if (QMessageBox::question(
this, tr("Overwrite Preset"),
tr("A preset with the name \"%1\" already exists. Do you wish to overwrite it?")
@@ -94,17 +109,18 @@ void ExportSavePresetDialog::accept()
}
}
if (!f.open(QFile::WriteOnly)) {
const QByteArray full_path =
d.filePath(name_edit_->text()).toUtf8();
const int rc = oakengine_encoding_params_save_file(
params_, full_path.constData());
if (rc != OAKENGINE_OK) {
QMessageBox::critical(
this, tr("Write Error"),
tr("Failed to open file \"%1\" for writing.").arg(f.fileName()));
tr("Failed to save preset to \"%1\".").arg(
QString::fromUtf8(full_path)));
return;
}
params_.save(&f);
f.close();
QDialog::accept();
}
+3 -3
View File
@@ -26,7 +26,7 @@
#include <QLineEdit>
#include <QListWidget>
#include "codec/encoder.h"
#include "oakengine/encoding.h"
namespace olive
{
@@ -34,7 +34,7 @@ namespace olive
class ExportSavePresetDialog : public QDialog {
Q_OBJECT
public:
ExportSavePresetDialog(const EncodingParams &p, QWidget *parent = nullptr);
ExportSavePresetDialog(const OakEngineEncodingParams *p, QWidget *parent = nullptr);
QString get_selected_preset_name() const
{
@@ -47,7 +47,7 @@ public slots:
private:
QLineEdit *name_edit_;
EncodingParams params_;
const OakEngineEncodingParams *params_;
};
}
+18 -31
View File
@@ -1,25 +1,9 @@
/*
* 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 "exportsubtitlestab.h"
#include <QGridLayout>
#include "oakengine/encoding.h"
namespace olive
{
@@ -62,32 +46,35 @@ ExportSubtitlesTab::ExportSubtitlesTab(QWidget *parent)
&QWidget::setVisible);
}
int ExportSubtitlesTab::set_format(ExportFormat::Format format)
int ExportSubtitlesTab::set_format(int format)
{
auto vcodecs = ExportFormat::get_video_codecs(format);
auto acodecs = ExportFormat::get_audio_codecs(format);
const bool has_video = oakengine_encoding_format_video_codec_count(format) > 0;
const bool has_audio = oakengine_encoding_format_audio_codec_count(format) > 0;
int scodec_count = oakengine_encoding_format_subtitle_codec_count(format);
auto scodecs = ExportFormat::get_subtitle_codecs(format);
if (!scodecs.empty() && vcodecs.empty() && acodecs.empty()) {
if (scodec_count > 0 && !has_video && !has_audio) {
// If format supports ONLY scodecs, default this to off and disable it
sidecar_checkbox_->setChecked(false);
sidecar_checkbox_->setEnabled(false);
} else {
// If format does not support scodecs, default this to checked and disable it
sidecar_checkbox_->setChecked(scodecs.empty());
sidecar_checkbox_->setEnabled(!scodecs.empty());
sidecar_checkbox_->setChecked(scodec_count == 0);
sidecar_checkbox_->setEnabled(scodec_count > 0);
}
scodecs =
ExportFormat::get_subtitle_codecs(sidecar_format_combobox_->get_format());
// Refresh for sidecar format
int sidecar_fmt = sidecar_format_combobox_->get_format();
scodec_count = oakengine_encoding_format_subtitle_codec_count(sidecar_fmt);
codec_combobox_->clear();
foreach (ExportCodec::Codec scodec, scodecs) {
codec_combobox_->addItem(ExportCodec::get_codec_name(scodec), scodec);
for (int i = 0; i < scodec_count; i++) {
int scodec = oakengine_encoding_format_subtitle_codec_at(sidecar_fmt, i);
char buf[256];
oakengine_encoding_codec_name(scodec, buf, sizeof(buf));
codec_combobox_->addItem(QString::fromUtf8(buf), scodec);
}
return scodecs.size();
return scodec_count;
}
}
+6 -8
View File
@@ -26,7 +26,6 @@
#include <QComboBox>
#include <QLabel>
#include "codec/exportformat.h"
#include "common/qtutils.h"
#include "dialog/export/exportformatcombobox.h"
@@ -47,24 +46,23 @@ public:
sidecar_checkbox_->setChecked(e);
}
ExportFormat::Format get_sidecar_format() const
int get_sidecar_format() const
{
return sidecar_format_combobox_->get_format();
}
void set_sidecar_format(ExportFormat::Format f)
void set_sidecar_format(int f)
{
sidecar_format_combobox_->set_format(f);
}
int set_format(ExportFormat::Format format);
int set_format(int format);
ExportCodec::Codec get_subtitle_codec()
int get_subtitle_codec()
{
return static_cast<ExportCodec::Codec>(
codec_combobox_->currentData().toInt());
return codec_combobox_->currentData().toInt();
}
void set_subtitle_codec(ExportCodec::Codec c)
void set_subtitle_codec(int c)
{
QtUtils::set_combo_box_data(codec_combobox_, c);
}
+37 -23
View File
@@ -29,15 +29,16 @@
#include "exportadvancedvideodialog.h"
#include "node/color/colormanager/colormanager.h"
#include "oakengine/encoding.h"
namespace olive
{
ExportVideoTab::ExportVideoTab(ColorManager *color_manager, QWidget *parent)
ExportVideoTab::ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent)
: QWidget(parent)
, color_manager_(color_manager)
, threads_(0)
, color_range_(VideoParams::k_color_range_default)
, color_range_(0) // k_color_range_default
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
@@ -50,17 +51,20 @@ ExportVideoTab::ExportVideoTab(ColorManager *color_manager, QWidget *parent)
outer_layout->addStretch();
}
int ExportVideoTab::set_format(ExportFormat::Format format)
int ExportVideoTab::set_format(int format)
{
format_ = format;
QList<ExportCodec::Codec> vcodecs = ExportFormat::get_video_codecs(format);
setEnabled(!vcodecs.isEmpty());
const int vcodec_count = oakengine_encoding_format_video_codec_count(format);
setEnabled(vcodec_count > 0);
codec_combobox()->clear();
foreach (ExportCodec::Codec vcodec, vcodecs) {
codec_combobox()->addItem(ExportCodec::get_codec_name(vcodec), vcodec);
for (int i = 0; i < vcodec_count; i++) {
int vcodec = oakengine_encoding_format_video_codec_at(format, i);
char buf[256];
oakengine_encoding_codec_name(vcodec, buf, sizeof(buf));
codec_combobox()->addItem(QString::fromUtf8(buf), vcodec);
}
return vcodecs.size();
return vcodec_count;
}
bool ExportVideoTab::is_image_sequence_set() const
@@ -116,9 +120,9 @@ QWidget *ExportVideoTab::setup_resolution_section()
scaling_method_combobox_ = new QComboBox();
scaling_method_combobox_->setEnabled(false);
scaling_method_combobox_->addItem(tr("Fit"), EncodingParams::k_fit);
scaling_method_combobox_->addItem(tr("Stretch"), EncodingParams::k_stretch);
scaling_method_combobox_->addItem(tr("Crop"), EncodingParams::k_crop);
scaling_method_combobox_->addItem(tr("Fit"), OAKENGINE_ENCODING_SCALING_FIT);
scaling_method_combobox_->addItem(tr("Stretch"), OAKENGINE_ENCODING_SCALING_STRETCH);
scaling_method_combobox_->addItem(tr("Crop"), OAKENGINE_ENCODING_SCALING_CROP);
layout->addWidget(scaling_method_combobox_, row, 1);
// Automatically enable/disable the scaling method depending on maintain aspect ratio
@@ -223,9 +227,14 @@ void ExportVideoTab::maintain_aspect_ratio_changed(bool val)
void ExportVideoTab::open_advanced_dialog()
{
// Find export formats compatible with this encoder
QStringList pixel_formats =
ExportFormat::get_pixel_formats_for_codec(format_, get_selected_codec());
// Find pixel formats compatible with this encoder
QStringList pixel_formats;
const int pix_count = oakengine_encoding_pix_fmt_count(format_, get_selected_codec());
for (int i = 0; i < pix_count; i++) {
char buf[64];
oakengine_encoding_pix_fmt_at(format_, get_selected_codec(), i, buf, sizeof(buf));
pixel_formats.append(QString::fromUtf8(buf));
}
ExportAdvancedVideoDialog d(pixel_formats, this);
@@ -256,30 +265,35 @@ void ExportVideoTab::update_frame_rate(Rational r)
void ExportVideoTab::video_codec_changed()
{
ExportCodec::Codec codec = get_selected_codec();
int codec = get_selected_codec();
switch (codec) {
case ExportCodec::k_codec_h264:
case ExportCodec::k_codec_h264rgb:
case OAKENGINE_ENCODING_CODEC_H264:
case OAKENGINE_ENCODING_CODEC_H264RGB:
set_codec_section(h264_section_);
break;
case ExportCodec::k_codec_h265:
case OAKENGINE_ENCODING_CODEC_H265:
set_codec_section(h265_section_);
break;
case ExportCodec::k_codec_a_v1:
case OAKENGINE_ENCODING_CODEC_AV1:
set_codec_section(av1_section_);
break;
case ExportCodec::k_codec_cineform:
case OAKENGINE_ENCODING_CODEC_CINEFORM:
set_codec_section(cineform_section_);
break;
default:
set_codec_section(
ExportCodec::is_codec_a_still_image(codec) ? image_section_ : nullptr);
oakengine_encoding_codec_is_still_image(codec) ? image_section_ : nullptr);
}
// Set default pixel format
QStringList pix_fmts =
ExportFormat::get_pixel_formats_for_codec(format_, codec);
QStringList pix_fmts;
const int pix_count = oakengine_encoding_pix_fmt_count(format_, codec);
for (int i = 0; i < pix_count; i++) {
char buf[64];
oakengine_encoding_pix_fmt_at(format_, codec, i, buf, sizeof(buf));
pix_fmts.append(QString::fromUtf8(buf));
}
if (!pix_fmts.isEmpty()) {
pix_fmt_ = pix_fmts.first();
} else {
+12 -12
View File
@@ -32,8 +32,9 @@
#include "dialog/export/codec/codecstack.h"
#include "dialog/export/codec/h264section.h"
#include "dialog/export/codec/imagesection.h"
#include "node/color/colormanager/colormanager.h"
#include "oakengine/color.h"
#include "widget/colorwheel/colorspacechooser.h"
#include "widget/manageddisplay/colorprocessorhandle.h"
#include "widget/slider/integerslider.h"
#include "widget/standardcombos/standardcombos.h"
@@ -43,9 +44,9 @@ namespace olive
class ExportVideoTab : public QWidget {
Q_OBJECT
public:
ExportVideoTab(ColorManager *color_manager, QWidget *parent = nullptr);
ExportVideoTab(OakEngineColorManager *color_manager, QWidget *parent = nullptr);
int set_format(ExportFormat::Format format);
int set_format(int format);
bool is_image_sequence_set() const;
void set_image_sequence(bool e) const;
@@ -55,13 +56,12 @@ public:
return image_section_->get_time();
}
ExportCodec::Codec get_selected_codec() const
int get_selected_codec() const
{
return static_cast<ExportCodec::Codec>(
codec_combobox()->currentData().toInt());
return codec_combobox()->currentData().toInt();
}
void set_selected_codec(ExportCodec::Codec c)
void set_selected_codec(int c)
{
QtUtils::set_combo_box_data(codec_combobox(), c);
}
@@ -161,11 +161,11 @@ public:
pix_fmt_ = s;
}
VideoParams::ColorRange color_range() const
int color_range() const
{
return color_range_;
}
void set_color_range(VideoParams::ColorRange c)
void set_color_range(int c)
{
color_range_ = c;
}
@@ -204,7 +204,7 @@ private:
IntegerSlider *width_slider_;
IntegerSlider *height_slider_;
ColorManager *color_manager_;
OakEngineColorManager *color_manager_;
InterlacedComboBox *interlaced_combobox_;
PixelAspectRatioComboBox *pixel_aspect_combobox_;
@@ -213,9 +213,9 @@ private:
int threads_;
QString pix_fmt_;
VideoParams::ColorRange color_range_;
int color_range_;
ExportFormat::Format format_;
int format_;
private slots:
void maintain_aspect_ratio_changed(bool val);
@@ -34,11 +34,13 @@
#include <QSpinBox>
#include "core.h"
#include "node/nodeundo.h"
#include "oakengine/footage.h"
#include "oakengine/node.h"
#include "oakengine/timeline.h"
#include "oakengine/undo.h"
#include "streamproperties/audiostreamproperties.h"
#include "streamproperties/videostreamproperties.h"
#include "widget/viewer/vieweroutpututils.h"
namespace olive
{
@@ -131,28 +133,43 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent,
QString description;
bool is_enabled = false;
OakEngineFootage *facade_handle = oakengine_footage_borrow(
reinterpret_cast<OakEngineNode *>(footage_));
switch (reference.type()) {
case Track::k_video: {
stacked_widget_->addWidget(
new VideoStreamProperties(footage_, reference.index()));
VideoParams vp = footage_->get_video_params(reference.index());
VideoParams vp = viewer_output_video_params(footage_, reference.index());
is_enabled = vp.enabled();
description = Footage::describe_video_stream(vp);
{
char desc_buf[256];
oakengine_footage_describe_video_stream(
facade_handle, reference.index(), desc_buf,
sizeof(desc_buf));
description = QString::fromUtf8(desc_buf);
}
break;
}
case Track::k_audio: {
stacked_widget_->addWidget(
new AudioStreamProperties(footage_, reference.index()));
AudioParams ap = footage_->get_audio_params(reference.index());
AudioParams ap = viewer_output_audio_params(footage_, reference.index());
is_enabled = ap.enabled();
description = Footage::describe_audio_stream(ap);
{
char desc_buf[256];
oakengine_footage_describe_audio_stream(
facade_handle, reference.index(), desc_buf,
sizeof(desc_buf));
description = QString::fromUtf8(desc_buf);
}
break;
}
case Track::k_subtitle: {
SubtitleParams sp = footage_->get_subtitle_params(reference.index());
is_enabled = sp.enabled();
is_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference.index());
// FIXME: Language?
description = tr("Subtitles");
@@ -164,6 +181,8 @@ FootagePropertiesDialog::FootagePropertiesDialog(QWidget *parent,
break;
}
oakengine_footage_free(facade_handle);
QListWidgetItem *item = new QListWidgetItem(description, track_list_);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(is_enabled ? Qt::Checked : Qt::Unchecked);
@@ -244,16 +263,16 @@ void FootagePropertiesDialog::accept()
switch (reference.type()) {
case Track::k_video:
old_stream_enabled =
footage_->get_video_params(reference.index()).enabled();
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_VIDEO, reference.index());
break;
case Track::k_audio:
old_stream_enabled =
footage_->get_audio_params(reference.index()).enabled();
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_AUDIO, reference.index());
break;
case Track::k_subtitle:
old_stream_enabled =
footage_->get_subtitle_params(reference.index()).enabled();
old_stream_enabled = oakengine_footage_get_stream_enabled(
facade_handle, OAKENGINE_TRACK_TYPE_SUBTITLE, reference.index());
break;
case Track::k_none:
case Track::k_count:
@@ -269,12 +288,12 @@ void FootagePropertiesDialog::accept()
oakengine_footage_free(facade_handle);
MultiUndoCommand *command = new MultiUndoCommand();
void *command = oakengine_undo_command_create_multi();
for (int i = 0; i < stacked_widget_->count(); i++) {
static_cast<StreamProperties *>(stacked_widget_->widget(i))
->accept(command);
}
delete command; // stream pages write through the facade directly
oakengine_undo_command_free(command); // stream pages write through the facade directly
QDialog::accept();
}
@@ -31,7 +31,6 @@
#include <QStackedWidget>
#include "node/project/footage/footage.h"
#include "undo/undocommand.h"
namespace olive
{
@@ -30,7 +30,7 @@ AudioStreamProperties::AudioStreamProperties(Footage *footage, int audio_index)
{
}
void AudioStreamProperties::accept(MultiUndoCommand *)
void AudioStreamProperties::accept(void *)
{
Q_UNUSED(footage_)
Q_UNUSED(audio_index_)
@@ -32,7 +32,7 @@ class AudioStreamProperties : public StreamProperties {
public:
AudioStreamProperties(Footage *footage, int audio_index);
virtual void accept(MultiUndoCommand *parent) override;
virtual void accept(void *parent) override;
private:
Footage *footage_;
@@ -25,7 +25,6 @@
#include <QWidget>
#include "common/define.h"
#include "undo/undocommand.h"
namespace olive
{
@@ -34,7 +33,7 @@ class StreamProperties : public QWidget {
public:
StreamProperties(QWidget *parent = nullptr);
virtual void accept(MultiUndoCommand *)
virtual void accept(void *)
{
}
@@ -28,8 +28,12 @@
#include <QMessageBox>
#include "node/project.h"
#include "oakengine/color.h"
#include "widget/manageddisplay/colorprocessorhandle.h"
#include "oakengine/footage.h"
#include "oakengine/node.h"
#include "oakengine/viewer.h"
#include "oakengine/videoparams.h"
namespace olive
{
@@ -46,7 +50,10 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index)
video_layout->addWidget(new QLabel(tr("Pixel Aspect:")), row, 0);
VideoParams vp = footage_->get_video_params(video_index_);
oak_video_params vpod;
oakengine_viewer_get_video_params(
reinterpret_cast<const OakEngineNode *>(footage_), video_index_,
&vpod);
// Stream override values come through the liboakengine C ABI facade;
// layout-only conditions (channel count, video type) stay direct reads.
@@ -85,10 +92,13 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index)
// The dropdown's color space list comes through the facade (same list
// the engine's color config reports).
OakEngineColorManager *cm = oakengine_color_manager_from_project(
reinterpret_cast<OakEngineProject *>(footage_->project()));
video_color_space_->addItem(tr("Default (%1)")
.arg(footage_->project()
->color_manager()
->get_default_input_color_space()));
.arg(oak_query_string([cm](char *buf, int size) {
return oakengine_color_manager_default_input_color_space(
cm, buf, size);
})));
const int colorspace_count =
oakengine_footage_colorspace_count(facade_handle);
@@ -110,14 +120,14 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index)
color_range_combo_ = new QComboBox();
color_range_combo_->addItem(tr("Limited (16-235)"),
VideoParams::k_color_range_limited);
0);
color_range_combo_->addItem(tr("Full (0-255)"),
VideoParams::k_color_range_full);
1);
color_range_combo_->setCurrentIndex(color_range);
video_layout->addWidget(color_range_combo_, row, 1);
if (vp.channel_count() == VideoParams::k_rgba_channel_count) {
if (oakengine_video_params_internal_channel_count() == 4) {
row++;
video_premultiply_alpha_ = new QCheckBox(tr("Premultiplied Alpha"));
@@ -127,7 +137,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index)
row++;
if (vp.video_type() == VideoParams::k_video_type_image_sequence) {
if (vpod.video_type == 2) {
QGroupBox *imgseq_group = new QGroupBox(tr("Image Sequence"));
QGridLayout *imgseq_layout = new QGridLayout(imgseq_group);
@@ -169,7 +179,7 @@ VideoStreamProperties::VideoStreamProperties(Footage *footage, int video_index)
oakengine_footage_free(facade_handle);
}
void VideoStreamProperties::accept(MultiUndoCommand *parent)
void VideoStreamProperties::accept(void *parent)
{
Q_UNUSED(parent)
@@ -182,17 +192,40 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent)
set_colorspace = video_color_space_->currentText();
}
VideoParams vp = footage_->get_video_params(video_index_);
// Fetch current values through the facade (avoids the inline
// ViewerOutput::get_video_params() which references k_video_params_input).
char vp_colorspace[256];
vp_colorspace[0] = '\0';
int vp_color_range = 0, vp_interlacing = 0, vp_premultiplied = 0;
oakengine_footage_get_video_stream_overrides(
facade_handle, video_index_, vp_colorspace, sizeof(vp_colorspace),
&vp_color_range, &vp_interlacing, &vp_premultiplied);
int vp_par_num = 1, vp_par_den = 1;
oakengine_footage_get_pixel_aspect(facade_handle, video_index_,
&vp_par_num, &vp_par_den);
oak_video_params vpod;
oakengine_viewer_get_video_params(
reinterpret_cast<const OakEngineNode *>(footage_), video_index_,
&vpod);
int64_t vp_start_time = 0, vp_duration = 0;
int vp_fr_num = 0, vp_fr_den = 1;
oakengine_footage_get_image_sequence_params(
facade_handle, video_index_, &vp_start_time, &vp_duration,
&vp_fr_num, &vp_fr_den);
// Write every override through the facade (each call is one undoable
// command on the shared undo stack, replacing this dialog's own undo
// command classes with identical semantics).
if ((video_premultiply_alpha_ &&
video_premultiply_alpha_->isChecked() != vp.premultiplied_alpha()) ||
set_colorspace != vp.colorspace() ||
video_premultiply_alpha_->isChecked() != (vp_premultiplied != 0)) ||
set_colorspace != QString::fromUtf8(vp_colorspace) ||
static_cast<VideoParams::Interlacing>(
video_interlace_combo_->currentIndex()) != vp.interlacing() ||
color_range_combo_->currentData().toInt() != vp.color_range()) {
video_interlace_combo_->currentIndex()) !=
static_cast<VideoParams::Interlacing>(vp_interlacing) ||
color_range_combo_->currentData().toInt() != vp_color_range) {
oakengine_footage_set_video_stream_overrides(
facade_handle, video_index_,
set_colorspace.toUtf8().constData(),
@@ -204,19 +237,19 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent)
}
const Rational new_par = pixel_aspect_combo_->get_pixel_aspect_ratio();
if (new_par != vp.pixel_aspect_ratio()) {
if (new_par != Rational(vp_par_num, vp_par_den)) {
oakengine_footage_set_pixel_aspect(facade_handle, video_index_,
new_par.numerator(),
new_par.denominator());
}
if (vp.video_type() == VideoParams::k_video_type_image_sequence) {
if (vpod.video_type == 2) {
int64_t new_dur =
imgseq_end_time_->get_value() - imgseq_start_time_->get_value() + 1;
if (vp.start_time() != imgseq_start_time_->get_value() ||
vp.duration() != new_dur ||
vp.frame_rate() != imgseq_frame_rate_->get_frame_rate()) {
if (vp_start_time != imgseq_start_time_->get_value() ||
vp_duration != new_dur ||
Rational(vp_fr_num, vp_fr_den) != imgseq_frame_rate_->get_frame_rate()) {
const Rational fr = imgseq_frame_rate_->get_frame_rate();
oakengine_footage_set_image_sequence_params(
facade_handle, video_index_,
@@ -230,8 +263,11 @@ void VideoStreamProperties::accept(MultiUndoCommand *parent)
bool VideoStreamProperties::sanity_check()
{
if (footage_->get_video_params(video_index_).video_type() ==
VideoParams::k_video_type_image_sequence) {
oak_video_params vpod;
oakengine_viewer_get_video_params(
reinterpret_cast<const OakEngineNode *>(footage_), video_index_,
&vpod);
if (vpod.video_type == 2) {
if (imgseq_start_time_->get_value() >= imgseq_end_time_->get_value()) {
QMessageBox::critical(
this, tr("Invalid Configuration"),
@@ -38,7 +38,7 @@ class VideoStreamProperties : public StreamProperties {
public:
VideoStreamProperties(Footage *footage, int video_index);
virtual void accept(MultiUndoCommand *parent) override;
virtual void accept(void *parent) override;
virtual bool sanity_check() override;
@@ -183,7 +183,9 @@ void FootageRelinkDialog::browse_for_footage()
new_dir.filePath(relative_to_original);
if (QFileInfo::exists(absolute_to_new)) {
other_footage->set_filename(absolute_to_new);
oakengine_footage_relink(
reinterpret_cast<OakEngineFootage *>(other_footage),
absolute_to_new.toUtf8().constData());
}
}
}
@@ -29,6 +29,8 @@
#include "core.h"
#include "oakengine/timeline.h"
namespace olive
{
@@ -138,29 +140,29 @@ void MarkerPropertiesDialog::accept()
return;
}
MultiUndoCommand *command = new MultiUndoCommand();
int color = color_menu_->get_selected_color();
foreach (TimelineMarker *m, markers_) {
if (color != -1) {
command->add_child(new MarkerChangeColorCommand(m, color));
// Batch-set properties via facade (one undoable command)
{
QVector<OakEngineMarker *> oak_markers;
foreach (TimelineMarker *m, markers_) {
oak_markers.append(reinterpret_cast<OakEngineMarker *>(m));
}
int color = color_menu_->get_selected_color();
QByteArray name_ba;
const char *name = nullptr;
if (label_edit_->placeholderText().isEmpty()) {
command->add_child(
new MarkerChangeNameCommand(m, label_edit_->text()));
name_ba = label_edit_->text().toUtf8();
name = name_ba.constData();
}
oakengine_marker_set_properties(
oak_markers.data(), oak_markers.size(), color, name,
(markers_.size() == 1) ? 1 : 0,
in_slider_->get_value().numerator(),
in_slider_->get_value().denominator(),
out_slider_->get_value().numerator(),
out_slider_->get_value().denominator(),
nullptr);
}
if (markers_.size() == 1) {
command->add_child(new MarkerChangeTimeCommand(
markers_.front(),
TimeRange(in_slider_->get_value(), out_slider_->get_value())));
}
Core::instance()->undo_stack()->push(command, tr("Set Marker Properties"));
super::accept();
}
+2 -2
View File
@@ -26,7 +26,7 @@
#include <QSplitter>
#include <QVBoxLayout>
#include "config/config.h"
#include "oakengine/config.h"
#include "tabs/preferencesgeneraltab.h"
#include "tabs/preferencesbehaviortab.h"
#include "tabs/preferencesappearancetab.h"
@@ -69,7 +69,7 @@ PreferencesDialog::PreferencesDialog(MainWindow *main_window, int start_tab)
void PreferencesDialog::AcceptEvent()
{
Config::save();
oakengine_config_save();
}
}
@@ -28,6 +28,7 @@
#include <QLabel>
#include "node/node.h"
#include "oakengine/node.h"
namespace olive
{
@@ -69,8 +70,9 @@ PreferencesAppearanceTab::PreferencesAppearanceTab()
QGridLayout *color_layout = new QGridLayout(color_group);
for (int i = 0; i < Node::k_category_count; i++) {
QString cat_name =
Node::get_category_name(static_cast<Node::CategoryID>(i));
char cat_buf[256];
oakengine_node_category_name(i, cat_buf, sizeof(cat_buf));
QString cat_name = QString::fromUtf8(cat_buf);
color_layout->addWidget(new QLabel(cat_name), i, 0);
ColorCodingComboBox *ccc = new ColorCodingComboBox();
@@ -102,7 +104,7 @@ PreferencesAppearanceTab::PreferencesAppearanceTab()
layout->addStretch();
}
void PreferencesAppearanceTab::accept(MultiUndoCommand *command)
void PreferencesAppearanceTab::accept(void *command)
{
Q_UNUSED(command)
@@ -38,7 +38,7 @@ class PreferencesAppearanceTab : public ConfigDialogBaseTab {
public:
PreferencesAppearanceTab();
virtual void accept(MultiUndoCommand *command) override;
virtual void accept(void *command) override;
private:
/**
@@ -25,8 +25,9 @@
#include <QGroupBox>
#include <QLabel>
#include "audio/audiomanager.h"
#include "config/config.h"
#include "oakengine/audio.h"
#include <portaudio.h>
#include "common/configwrapper.h"
namespace olive
{
@@ -171,13 +172,13 @@ PreferencesAudioTab::PreferencesAudioTab()
new ExportFormatComboBox(ExportFormatComboBox::k_show_audio_only);
record_format_combo_->setSizePolicy(QSizePolicy::Expanding,
QSizePolicy::Expanding);
record_format_combo_->set_format(static_cast<ExportFormat::Format>(
record_format_combo_->set_format(static_cast<int>(
OAK_CONFIG("AudioRecordingFormat").toInt()));
fmt_layout->addWidget(record_format_combo_);
record_options_ = new ExportAudioTab();
record_options_->set_format(record_format_combo_->get_format());
record_options_->set_codec(static_cast<ExportCodec::Codec>(
record_options_->set_codec(static_cast<int>(
OAK_CONFIG("AudioRecordingCodec").toInt()));
record_options_->sample_rate_combobox()->set_sample_rate(
OAK_CONFIG("AudioRecordingSampleRate").toInt());
@@ -213,7 +214,7 @@ PreferencesAudioTab::PreferencesAudioTab()
refresh_backends();
}
void PreferencesAudioTab::accept(MultiUndoCommand *command)
void PreferencesAudioTab::accept(void *command)
{
Q_UNUSED(command)
@@ -228,8 +229,8 @@ void PreferencesAudioTab::accept(MultiUndoCommand *command)
OAK_CONFIG("AudioInput") = audio_input_devices_->currentText();
// Set devices to be used from now on
AudioManager::instance()->set_output_device(output_device);
AudioManager::instance()->set_input_device(input_device);
oakengine_audio_set_output_device(output_device);
oakengine_audio_set_input_device(input_device);
OAK_CONFIG("AudioOutputSampleRate") = output_rate_combo_->get_sample_rate();
OAK_CONFIG("AudioOutputChannelLayout") =
@@ -251,7 +252,8 @@ void PreferencesAudioTab::accept(MultiUndoCommand *command)
->get_sample_format()
.to_string());
emit AudioManager::instance() -> output_params_changed();
// AudioManager output params changed is handled internally by the facade
// when oakengine_audio_set_output_device() is called.
OAK_CONFIG("AudioScrubbing") = audio_scrubbing_->isChecked();
}
@@ -299,7 +301,7 @@ void PreferencesAudioTab::refresh_devices()
void PreferencesAudioTab::hard_refresh_backends()
{
AudioManager::instance()->hard_reset();
oakengine_audio_hard_reset();
refresh_backends();
}
@@ -307,9 +309,9 @@ void PreferencesAudioTab::attempt_to_set_devices_from_config()
{
// Load with currently active devices
PaDeviceIndex current_output_index =
AudioManager::instance()->get_output_device();
static_cast<PaDeviceIndex>(oakengine_audio_get_output_device());
PaDeviceIndex current_input_index =
AudioManager::instance()->get_input_device();
static_cast<PaDeviceIndex>(oakengine_audio_get_input_device());
const PaDeviceInfo *current_output = nullptr, *current_input = nullptr;
if (current_output_index != paNoDevice) {
@@ -40,7 +40,7 @@ class PreferencesAudioTab : public ConfigDialogBaseTab {
public:
PreferencesAudioTab();
virtual void accept(MultiUndoCommand *command) override;
virtual void accept(void *command) override;
private:
QComboBox *audio_backend_combobox_;
@@ -23,7 +23,7 @@
#include <QLabel>
#include "config/config.h"
#include "common/configwrapper.h"
namespace olive
{
@@ -114,7 +114,7 @@ PreferencesBehaviorTab::PreferencesBehaviorTab(Category category)
}
}
void PreferencesBehaviorTab::accept(MultiUndoCommand *command)
void PreferencesBehaviorTab::accept(void *command)
{
Q_UNUSED(command)

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