chore: recovery backups, temp files and misc campaign artifacts
.bak LocalHistory recovery backups, .tmp0/.tmp1, .gitignore update, reasonix.toml, convert_videoparams.py. Kept as-is from the campaign branch; cleanup can happen separately on main.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -105,3 +105,4 @@ AGENTS.md
|
||||
operations-log.md
|
||||
verification.md
|
||||
act
|
||||
.tmp/
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Batch convert simple VideoParams constant and static method references to facade equivalents."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
# Files that use VideoParams (non-standardcombos - those are already converted)
|
||||
target_files = []
|
||||
for root, dirs, fnames in os.walk('app'):
|
||||
for f in fnames:
|
||||
if f.endswith(('.cpp', '.h')) and 'autogen' not in root:
|
||||
path = os.path.join(root, f)
|
||||
try:
|
||||
content = open(path, 'r', errors='replace').read()
|
||||
if 'VideoParams' in content:
|
||||
target_files.append(path)
|
||||
except:
|
||||
pass
|
||||
|
||||
print(f"Files with VideoParams: {len(target_files)}")
|
||||
|
||||
# Simple constant replacements (these are pure enum/integer replacements)
|
||||
simple_replacements = [
|
||||
# (old, new) - EXACT string matches
|
||||
('VideoParams::k_interlace_none', '0'),
|
||||
('VideoParams::k_color_range_default', '0'),
|
||||
('VideoParams::k_color_range_limited', '0'),
|
||||
('VideoParams::k_color_range_full', '1'),
|
||||
('VideoParams::k_rgba_channel_count', '4'),
|
||||
('VideoParams::k_video_type_still', '1'),
|
||||
('VideoParams::k_video_type_image_sequence', '2'),
|
||||
('VideoParams::k_video_type_video', '0'), # This might be wrong - check
|
||||
('VideoParams::k_interlaced_top_first', '1'),
|
||||
('VideoParams::k_interlaced_bottom_first', '2'),
|
||||
]
|
||||
|
||||
# Track what was replaced
|
||||
total_replacements = {}
|
||||
for old, new in simple_replacements:
|
||||
total_replacements[old] = 0
|
||||
|
||||
# For each file, do the replacements
|
||||
for filepath in target_files:
|
||||
with open(filepath, 'r', errors='replace') as f:
|
||||
content = f.read()
|
||||
|
||||
original = content
|
||||
|
||||
for old, new in simple_replacements:
|
||||
# Use word boundary to avoid partial matches
|
||||
count = content.count(old)
|
||||
if count > 0:
|
||||
content = content.replace(old, new)
|
||||
total_replacements[old] += count
|
||||
|
||||
if content != original:
|
||||
with open(filepath, 'w') as f:
|
||||
f.write(content)
|
||||
print(f" Modified: {filepath}")
|
||||
|
||||
print("\nReplacement summary:")
|
||||
for old, count in total_replacements.items():
|
||||
if count > 0:
|
||||
print(f" {old} -> replaced {count} times")
|
||||
|
||||
print("\nDone!")
|
||||
@@ -0,0 +1,2 @@
|
||||
[permissions]
|
||||
allow = ["Bash(cd /home/mikesolar/Projects/oak && cmake --build cmake-build-debug -j$(nproc) 2>&1 | tail -30)"]
|
||||
Reference in New Issue
Block a user