Merge branch 'master' into pr/1717

This commit is contained in:
itsmattkc
2022-05-01 10:58:44 -07:00
1583 changed files with 89018 additions and 321451 deletions
@@ -41,7 +41,6 @@ ColorLabelMenuItem::ColorLabelMenuItem(QWidget* parent) :
layout->addWidget(box_);
label_ = new QLabel();
StyleManager::UseOSNativeStyling(label_);
layout->addWidget(label_);
}
-3
View File
@@ -106,9 +106,6 @@ void Menu::Init()
QPalette p = palette();
p.setColor(QPalette::Disabled, QPalette::Light, QColor(0, 0, 0, 0));
setPalette(p);
// If a native palette is available, it should override the one above
StyleManager::UseOSNativeStyling(this);
}
}
+7
View File
@@ -137,6 +137,13 @@ void MenuShared::AddItemsForEditMenu(Menu *m, bool for_clips)
m->addAction(edit_ripple_delete_item_);
m->addAction(edit_split_item_);
m->addAction(edit_speedduration_item_);
m->addSeparator();
m->addAction(clip_add_default_transition_item_);
m->addAction(clip_link_unlink_item_);
m->addAction(clip_enable_disable_item_);
m->addAction(clip_nest_item_);
}
}
+179 -76
View File
@@ -28,6 +28,7 @@
#include "common/functiontimer.h"
#include "common/timecodefunctions.h"
#include "node/output/viewer/viewer.h"
#include "widget/nodeview/nodeviewundo.h"
namespace olive {
@@ -37,9 +38,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) :
super(true, false, parent),
last_scroll_val_(0),
focused_node_(nullptr),
create_checkboxes_(kNoCheckBoxes),
time_target_(nullptr),
ignore_flags_(false)
time_target_(nullptr)
{
// Create horizontal layout to place scroll area in (and keyframe editing eventually)
QHBoxLayout* layout = new QHBoxLayout(this);
@@ -76,6 +75,7 @@ NodeParamView::NodeParamView(bool create_keyframe_view, QWidget *parent) :
for (int i=0; i<context_items_.size(); i++) {
NodeParamViewContext *c = new NodeParamViewContext;
c->setVisible(false);
connect(c, &NodeParamViewContext::AboutToDeleteItem, this, &NodeParamView::ItemAboutToBeRemoved, Qt::DirectConnection);
NodeParamViewItemTitleBar *title_bar = static_cast<NodeParamViewItemTitleBar*>(c->titleBarWidget());
@@ -236,62 +236,75 @@ void NodeParamView::DeselectNodes(const QVector<Node *> &nodes)
}
}*/
void NodeParamView::SetInputChecked(const NodeInput &input, bool e)
void NodeParamView::UpdateContexts()
{
input_checked_.insert(input, e);
foreach (NodeParamViewContext *ctx, context_items_) {
ctx->SetInputChecked(input, e);
//TIME_THIS_FUNCTION;
bool changes_made = false;
foreach (Node *ctx, current_contexts_) {
if (!contexts_.contains(ctx)) {
// Context is being removed
RemoveContext(ctx);
changes_made = true;
}
}
foreach (Node *ctx, contexts_) {
if (!current_contexts_.contains(ctx)) {
// Context is being added
AddContext(ctx);
changes_made = true;
}
}
if (changes_made) {
current_contexts_ = contexts_;
if (IsGroupMode()) {
// Check inputs that have been passed through
NodeGroup *group = static_cast<NodeGroup*>(contexts_.first());
for (auto it=group->GetInputPassthroughs().cbegin(); it!=group->GetInputPassthroughs().cend(); it++) {
GroupInputPassthroughAdded(group, it->second);
}
connect(group, &NodeGroup::InputPassthroughAdded, this, &NodeParamView::GroupInputPassthroughAdded);
connect(group, &NodeGroup::InputPassthroughRemoved, this, &NodeParamView::GroupInputPassthroughRemoved);
}
foreach (NodeParamViewContext *ctx, context_items_) {
SortItemsInContext(ctx);
}
if (keyframe_view_) {
QueueKeyframePositionUpdate();
}
}
}
void NodeParamView::ItemAboutToBeRemoved(NodeParamViewItem *item)
{
if (keyframe_view_) {
for (auto it=item->GetKeyframeConnections().begin(); it!=item->GetKeyframeConnections().end(); it++) {
for (auto jt=it->begin(); jt!=it->end(); jt++) {
for (auto kt=jt->begin(); kt!=jt->end(); kt++) {
keyframe_view_->RemoveKeyframesOfTrack(*kt);
}
}
}
}
if (focused_node_ == item) {
focused_node_ = nullptr;
emit FocusedNodeChanged(nullptr);
}
}
void NodeParamView::SetContexts(const QVector<Node *> &contexts)
{
//TIME_THIS_FUNCTION;
foreach (NodeParamViewContext *ctx, context_items_) {
ctx->Clear();
ctx->setVisible(false);
}
foreach (Node *ctx, contexts_) {
disconnect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext);
}
// Setting contexts is expensive, so we queue it here to prevent multiple calls in a short timespan
contexts_ = contexts;
foreach (Node *ctx, contexts_) {
// Queued so that if any further work is done in connecting this node to the context, it'll be
// done before our sorting function is called
connect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext, Qt::QueuedConnection);
}
if (keyframe_view_) {
keyframe_view_->Clear();
}
if (focused_node_) {
focused_node_ = nullptr;
emit FocusedNodeChanged(nullptr);
}
foreach (Node *ctx, contexts) {
NodeParamViewContext *item = GetContextItemFromContext(ctx);
item->AddContext(ctx);
item->setVisible(true);
for (auto it=ctx->GetContextPositions().cbegin(); it!=ctx->GetContextPositions().cend(); it++) {
AddNode(it.key(), item);
}
}
foreach (NodeParamViewContext *ctx, context_items_) {
SortItemsInContext(ctx);
}
if (keyframe_view_) {
QueueKeyframePositionUpdate();
}
UpdateContexts();
}
void NodeParamView::resizeEvent(QResizeEvent *event)
@@ -357,8 +370,28 @@ Node *NodeParamView::GetTimeTarget() const
void NodeParamView::DeleteSelected()
{
if (keyframe_view_) {
if (keyframe_view_ && keyframe_view_->hasFocus()) {
keyframe_view_->DeleteSelected();
} else if (focused_node_) {
MultiUndoCommand *c = new MultiUndoCommand();
Node *n = focused_node_->GetNode();
// Create command to delete node from context and/or graph
NodeViewDeleteCommand *dc = new NodeViewDeleteCommand();
dc->AddNode(n, focused_node_->GetContext());
c->add_child(dc);
// Copy any outputs that were connected
if (n->GetEffectInput().IsValid()) {
if (Node *out = n->GetEffectInput().GetConnectedOutput()) {
for (auto it=n->output_connections().cbegin(); it!=n->output_connections().cend(); it++) {
c->add_child(new NodeEdgeAddCommand(out, it->second));
}
}
}
Core::instance()->undo_stack()->push(c);
}
}
@@ -384,27 +417,52 @@ void NodeParamView::QueueKeyframePositionUpdate()
QMetaObject::invokeMethod(this, &NodeParamView::UpdateElementY, Qt::QueuedConnection);
}
void NodeParamView::AddNode(Node *n, NodeParamViewContext *context)
void NodeParamView::AddContext(Node *ctx)
{
if ((n->GetFlags() & Node::kDontShowInParamView) && !ignore_flags_) {
// Queued so that if any further work is done in connecting this node to the context, it'll be
// done before our sorting function is called
connect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext, Qt::QueuedConnection);
connect(ctx, &Node::NodeRemovedFromContext, this, &NodeParamView::NodeRemovedFromContext, Qt::QueuedConnection);
NodeParamViewContext *item = GetContextItemFromContext(ctx);
item->AddContext(ctx);
item->setVisible(true);
for (auto it=ctx->GetContextPositions().cbegin(); it!=ctx->GetContextPositions().cend(); it++) {
AddNode(it.key(), ctx, item);
}
}
void NodeParamView::RemoveContext(Node *ctx)
{
disconnect(ctx, &Node::NodeAddedToContext, this, &NodeParamView::NodeAddedToContext);
disconnect(ctx, &Node::NodeRemovedFromContext, this, &NodeParamView::NodeRemovedFromContext);
foreach (NodeParamViewContext *item, context_items_) {
item->RemoveContext(ctx);
item->RemoveNodesWithContext(ctx);
if (item->GetContexts().isEmpty()) {
item->setVisible(false);
}
}
}
void NodeParamView::AddNode(Node *n, Node *ctx, NodeParamViewContext *context)
{
if ((n->GetFlags() & Node::kDontShowInParamView) && !IsGroupMode()) {
return;
}
NodeParamViewItem* item = new NodeParamViewItem(n, create_checkboxes_, context);
NodeParamViewItem* item = new NodeParamViewItem(n, IsGroupMode() ? kCheckBoxesOnNonConnected : kNoCheckBoxes, context);
connect(item, &NodeParamViewItem::RequestSetTime, this, &NodeParamView::SetTimeAndSignal);
connect(item, &NodeParamViewItem::RequestSelectNode, this, &NodeParamView::RequestSelectNode);
connect(item, &NodeParamViewItem::PinToggled, this, &NodeParamView::PinNode);
connect(item, &NodeParamViewItem::InputCheckedChanged, this, &NodeParamView::SetInputChecked);
if (create_checkboxes_) {
for (auto it=input_checked_.cbegin(); it!=input_checked_.cend(); it++) {
if (it.key().node() == n) {
item->SetInputChecked(it.key(), it.value());
}
}
}
connect(item, &NodeParamViewItem::InputCheckedChanged, this, &NodeParamView::InputCheckBoxChanged);
item->SetContext(ctx);
item->SetTimeTarget(GetTimeTarget());
item->SetTimebase(timebase());
item->SetTime(GetTime());
@@ -449,9 +507,11 @@ void NodeParamView::SortItemsInContext(NodeParamViewContext *context_item)
QVector<QPair<NodeParamViewItem*, int> > distances;
for (auto it=context_item->GetItems().cbegin(); it!=context_item->GetItems().cend(); it++) {
NodeParamViewItem *item = *it;
int distance = -1;
foreach (Node *ctx, context_item->GetContexts()) {
distance = qMax(distance, GetDistanceBetweenNodes(ctx, it.key()));
distance = qMax(distance, GetDistanceBetweenNodes(ctx, item->GetNode()));
}
if (distance == -1) {
@@ -459,7 +519,7 @@ void NodeParamView::SortItemsInContext(NodeParamViewContext *context_item)
}
bool inserted = false;
QPair<NodeParamViewItem*, int> dist(it.value(), distance);
QPair<NodeParamViewItem*, int> dist(item, distance);
for (int i=0; i<distances.size(); i++) {
if (distances.at(i).second < distance) {
@@ -567,25 +627,26 @@ void NodeParamView::KeyframeViewDragged(int x, int y)
{
Q_UNUSED(y)
QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection,
Q_ARG(int, x));
QMetaObject::invokeMethod(this, "CatchUpScrollToPoint", Qt::QueuedConnection, Q_ARG(int, x));
}
void NodeParamView::UpdateElementY()
{
foreach (NodeParamViewContext *ctx, context_items_) {
for (auto it=ctx->GetItems().cbegin(); it!=ctx->GetItems().cend(); it++) {
const KeyframeView::NodeConnections &connections = it.value()->GetKeyframeConnections();
NodeParamViewItem *item = *it;
Node *node = item->GetNode();
const KeyframeView::NodeConnections &connections = item->GetKeyframeConnections();
if (!connections.isEmpty()) {
foreach (const QString& input, it.key()->inputs()) {
if (!(it.key()->GetInputFlags(input) & kInputFlagHidden)) {
int arr_sz = NodeGroup::ResolveInput(NodeInput(it.key(), input)).GetArraySize();
foreach (const QString& input, node->inputs()) {
if (!(node->GetInputFlags(input) & kInputFlagHidden)) {
int arr_sz = NodeGroup::ResolveInput(NodeInput(node, input)).GetArraySize();
for (int i=-1; i<arr_sz; i++) {
NodeInput ic = {it.key(), input, i};
NodeInput ic = {node, input, i};
int y = it.value()->GetElementY(ic);
int y = item->GetElementY(ic);
// For some reason Qt's mapToGlobal doesn't seem to handle this, so we offset here
y += vertical_scrollbar_->value();
@@ -611,9 +672,51 @@ void NodeParamView::NodeAddedToContext(Node *n)
Node *ctx = static_cast<Node*>(sender());
NodeParamViewContext *item = GetContextItemFromContext(ctx);
AddNode(n, item);
AddNode(n, ctx, item);
SortItemsInContext(item);
if (keyframe_view_) {
QueueKeyframePositionUpdate();
}
}
void NodeParamView::NodeRemovedFromContext(Node *n)
{
Node *ctx = static_cast<Node*>(sender());
foreach (NodeParamViewContext *ctx_item, context_items_) {
ctx_item->RemoveNode(n, ctx);
}
if (keyframe_view_) {
QueueKeyframePositionUpdate();
}
}
void NodeParamView::InputCheckBoxChanged(const NodeInput &input, bool e)
{
NodeGroup *group = static_cast<NodeGroup*>(contexts_.first());
if (e) {
group->AddInputPassthrough(input);
} else {
group->RemoveInputPassthrough(input);
}
}
void NodeParamView::GroupInputPassthroughAdded(NodeGroup *group, const NodeInput &input)
{
foreach (NodeParamViewContext *pvctx, context_items_) {
pvctx->SetInputChecked(input, true);
}
}
void NodeParamView::GroupInputPassthroughRemoved(NodeGroup *group, const NodeInput &input)
{
foreach (NodeParamViewContext *pvctx, context_items_) {
pvctx->SetInputChecked(input, false);
}
}
}
+23 -24
View File
@@ -45,16 +45,6 @@ public:
virtual ~NodeParamView() override;
void SetCreateCheckBoxes(NodeParamViewCheckBoxBehavior e)
{
create_checkboxes_ = e;
}
bool IsInputChecked(const NodeInput &input) const
{
return input_checked_.value(input);
}
void CloseContextsBelongingToProject(Project *p);
Node* GetTimeTarget() const;
@@ -71,11 +61,6 @@ public:
keyframe_view_->DeselectAll();
}
void SetIgnoreNodeFlags(bool e)
{
ignore_flags_ = e;
}
void SelectNodes(const QVector<Node*> &nodes);
void DeselectNodes(const QVector<Node*> &nodes);
@@ -85,8 +70,6 @@ public:
}
public slots:
void SetInputChecked(const NodeInput &input, bool e);
void SetContexts(const QVector<Node*> &contexts);
void UpdateElementY();
@@ -110,12 +93,21 @@ private:
void QueueKeyframePositionUpdate();
void AddNode(Node* n, NodeParamViewContext *context);
void AddContext(Node *context);
void RemoveContext(Node *context);
void AddNode(Node* n, Node *ctx, NodeParamViewContext *context);
void SortItemsInContext(NodeParamViewContext *context);
NodeParamViewContext *GetContextItemFromContext(Node *context);
bool IsGroupMode() const
{
return contexts_.size() == 1 && dynamic_cast<NodeGroup*>(contexts_.first());
}
KeyframeView* keyframe_view_;
QVector<NodeParamViewContext*> context_items_;
@@ -136,15 +128,10 @@ private:
NodeParamViewItem* focused_node_;
NodeParamViewCheckBoxBehavior create_checkboxes_;
Node *time_target_;
QHash<NodeInput, bool> input_checked_;
bool ignore_flags_;
QVector<Node*> contexts_;
QVector<Node*> current_contexts_;
private slots:
void UpdateGlobalScrollBar();
@@ -157,6 +144,18 @@ private slots:
void NodeAddedToContext(Node *n);
void NodeRemovedFromContext(Node *n);
void InputCheckBoxChanged(const NodeInput &input, bool e);
void GroupInputPassthroughAdded(olive::NodeGroup *group, const olive::NodeInput &input);
void GroupInputPassthroughRemoved(olive::NodeGroup *group, const olive::NodeInput &input);
void UpdateContexts();
void ItemAboutToBeRemoved(NodeParamViewItem *item);
};
}
@@ -23,6 +23,8 @@
#include <QMessageBox>
#include "node/block/clip/clip.h"
#include "node/factory.h"
#include "widget/nodeview/nodeviewundo.h"
namespace olive {
@@ -45,28 +47,61 @@ NodeParamViewContext::NodeParamViewContext(QWidget *parent) :
connect(title_bar(), &NodeParamViewItemTitleBar::AddEffectButtonClicked, this, &NodeParamViewContext::AddEffectButtonClicked);
}
NodeParamViewItem *NodeParamViewContext::GetItem(Node *node, Node *ctx)
{
for (auto it=items_.begin(); it!=items_.end(); it++) {
NodeParamViewItem *item = *it;
if (item->GetNode() == node && item->GetContext() == ctx) {
return item;
}
}
return nullptr;
}
void NodeParamViewContext::AddNode(NodeParamViewItem *item)
{
items_.insert(item->GetNode(), item);
items_.append(item);
dock_area_->AddItem(item);
}
void NodeParamViewContext::RemoveNode(Node *node)
void NodeParamViewContext::RemoveNode(Node *node, Node *ctx)
{
for (auto it=items_.begin(); it!=items_.end(); ) {
NodeParamViewItem *item = *it;
if (item->GetNode() == node && item->GetContext() == ctx) {
emit AboutToDeleteItem(item);
delete item;
it = items_.erase(it);
} else {
it++;
}
}
}
void NodeParamViewContext::Clear()
void NodeParamViewContext::RemoveNodesWithContext(Node *ctx)
{
qDeleteAll(items_);
items_.clear();
for (auto it=items_.begin(); it!=items_.end(); ) {
NodeParamViewItem *item = *it;
contexts_.clear();
if (item->GetContext() == ctx) {
emit AboutToDeleteItem(item);
delete item;
it = items_.erase(it);
} else {
it++;
}
}
}
void NodeParamViewContext::SetInputChecked(const NodeInput &input, bool e)
{
if (NodeParamViewItem *item = items_.value(input.node())) {
item->SetInputChecked(input, e);
foreach (NodeParamViewItem *item, items_) {
if (item->GetNode() == input.node()) {
item->SetInputChecked(input, e);
}
}
}
@@ -97,7 +132,45 @@ void NodeParamViewContext::Retranslate()
void NodeParamViewContext::AddEffectButtonClicked()
{
QMessageBox::information(this, tr("STUB"), tr("This feature is coming soon. Thanks for testing development builds of Olive :)"));
Menu *m = NodeFactory::CreateMenu(this, false, Node::kCategoryUnknown, Node::kVideoEffect);
connect(m, &Menu::triggered, this, &NodeParamViewContext::AddEffectMenuItemTriggered);
m->exec(QCursor::pos());
delete m;
}
void NodeParamViewContext::AddEffectMenuItemTriggered(QAction *a)
{
Node *n = NodeFactory::CreateFromMenuAction(a);
if (n) {
NodeInput new_node_input = n->GetEffectInput();
MultiUndoCommand *command = new MultiUndoCommand();
QVector<NodeGraph*> graphs_added_to;
foreach (Node *ctx, contexts_) {
NodeInput ctx_input = ctx->GetEffectInput();
if (!graphs_added_to.contains(ctx->parent())) {
command->add_child(new NodeAddCommand(ctx->parent(), n));
graphs_added_to.append(ctx->parent());
}
command->add_child(new NodeSetPositionCommand(n, ctx, ctx->GetNodePositionInContext(ctx)));
command->add_child(new NodeSetPositionCommand(ctx, ctx, ctx->GetNodePositionInContext(ctx) + QPointF(1, 0)));
if (ctx_input.IsConnected()) {
Node *prev_output = ctx_input.GetConnectedOutput();
command->add_child(new NodeEdgeRemoveCommand(prev_output, ctx_input));
command->add_child(new NodeEdgeAddCommand(prev_output, new_node_input));
}
command->add_child(new NodeEdgeAddCommand(n, ctx_input));
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
}
}
}
@@ -43,16 +43,18 @@ public:
return contexts_;
}
const QMap<Node*, NodeParamViewItem*> &GetItems() const
const QVector<NodeParamViewItem*> &GetItems() const
{
return items_;
}
NodeParamViewItem *GetItem(Node *node, Node *ctx);
void AddNode(NodeParamViewItem *item);
void RemoveNode(Node *node);
void RemoveNode(Node *node, Node *ctx);
void Clear();
void RemoveNodesWithContext(Node *ctx);
void SetInputChecked(const NodeInput &input, bool e);
@@ -62,6 +64,9 @@ public:
void SetTime(const rational &time);
signals:
void AboutToDeleteItem(NodeParamViewItem *item);
public slots:
void AddContext(Node *node)
{
@@ -81,11 +86,13 @@ private:
QVector<Node*> contexts_;
QMap<Node*, NodeParamViewItem*> items_;
QVector<NodeParamViewItem*> items_;
private slots:
void AddEffectButtonClicked();
void AddEffectMenuItemTriggered(QAction *a);
};
}
+19 -9
View File
@@ -40,12 +40,14 @@ const int NodeParamViewItemBody::kOptionalCheckBox = 0;
const int NodeParamViewItemBody::kArrayCollapseBtnColumn = 1;
const int NodeParamViewItemBody::kLabelColumn = 2;
const int NodeParamViewItemBody::kWidgetStartColumn = 3;
const int NodeParamViewItemBody::kMaxWidgetColumn = kKeyControlColumn;
#define super NodeParamViewItemBase
NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior create_checkboxes, QWidget *parent) :
super(parent),
node_(node)
node_(node),
ctx_(nullptr)
{
node_->Retranslate();
@@ -61,6 +63,11 @@ NodeParamViewItem::NodeParamViewItem(Node *node, NodeParamViewCheckBoxBehavior c
setBackgroundRole(QPalette::Window);
// Connect title bar enabled checkbox
//title_bar()->SetEnabledCheckBoxVisible(true);
//title_bar()->SetEnabledCheckBoxChecked(node_->IsEnabled());
//connect(title_bar(), &NodeParamViewItemTitleBar::EnabledCheckBoxClicked, node_, &Node::SetEnabled);
Retranslate();
}
@@ -216,13 +223,6 @@ void NodeParamViewItemBody::CreateWidgets(QGridLayout* layout, Node *node, const
// Place widgets into layout
PlaceWidgetsFromBridge(layout, ui_objects.widget_bridge, row);
// Add widgets for this parameter to the layout
for (int i=0; i<ui_objects.widget_bridge->widgets().size(); i++) {
QWidget* w = ui_objects.widget_bridge->widgets().at(i);
layout->addWidget(w, row, i+kWidgetStartColumn);
}
// In case this input is a group, resolve that actual input to use for connected labels
NodeInput resolved = NodeGroup::ResolveInput(input_ref);
@@ -355,7 +355,17 @@ void NodeParamViewItemBody::PlaceWidgetsFromBridge(QGridLayout* layout, NodePara
for (int i=0; i<bridge->widgets().size(); i++) {
QWidget* w = bridge->widgets().at(i);
layout->addWidget(w, row, i+kWidgetStartColumn);
int col = i+kWidgetStartColumn;
int colspan;
if (i == bridge->widgets().size()-1) {
// Span this widget among remaining columns
colspan = kMaxWidgetColumn - col;
} else {
colspan = 1;
}
layout->addWidget(w, row, col, 1, colspan);
}
}
@@ -134,6 +134,7 @@ private:
static const int kArrayCollapseBtnColumn;
static const int kLabelColumn;
static const int kWidgetStartColumn;
static const int kMaxWidgetColumn;
private slots:
void EdgeChanged(Node *output, const NodeInput &input);
@@ -181,6 +182,16 @@ public:
body_->SetTimebase(timebase);
}
Node *GetContext() const
{
return ctx_;
}
void SetContext(Node *ctx)
{
ctx_ = ctx;
}
Node* GetNode() const
{
return node_;
@@ -217,6 +228,8 @@ private:
Node* node_;
Node *ctx_;
rational time_;
KeyframeView::NodeConnections keyframe_connections_;
@@ -56,6 +56,11 @@ NodeParamViewItemTitleBar::NodeParamViewItemTitleBar(QWidget *parent) :
pin_btn_->setVisible(false);
layout->addWidget(pin_btn_);
connect(pin_btn_, &QPushButton::clicked, this, &NodeParamViewItemTitleBar::PinToggled);
enabled_checkbox_ = new QCheckBox();
enabled_checkbox_->setVisible(false);
layout->addWidget(enabled_checkbox_);
connect(enabled_checkbox_, &QCheckBox::clicked, this, &NodeParamViewItemTitleBar::EnabledCheckBoxClicked);
}
void NodeParamViewItemTitleBar::SetExpanded(bool e)
@@ -21,6 +21,7 @@
#ifndef NODEPARAMVIEWITEMTITLEBAR_H
#define NODEPARAMVIEWITEMTITLEBAR_H
#include <QCheckBox>
#include <QLabel>
#include <QWidget>
@@ -59,6 +60,16 @@ public slots:
add_fx_btn_->setVisible(e);
}
void SetEnabledCheckBoxVisible(bool e)
{
enabled_checkbox_->setVisible(e);
}
void SetEnabledCheckBoxChecked(bool e)
{
enabled_checkbox_->setChecked(e);
}
signals:
void ExpandedStateChanged(bool e);
@@ -66,6 +77,8 @@ signals:
void AddEffectButtonClicked();
void EnabledCheckBoxClicked(bool e);
protected:
virtual void paintEvent(QPaintEvent *event) override;
@@ -82,6 +95,8 @@ private:
QPushButton *add_fx_btn_;
QCheckBox *enabled_checkbox_;
};
}
@@ -92,6 +92,7 @@ void NodeParamViewWidgetBridge::CreateWidgets()
case NodeValue::kGenerateJob:
case NodeValue::kVideoParams:
case NodeValue::kAudioParams:
case NodeValue::kDataTypeCount:
break;
case NodeValue::kInt:
{
@@ -198,40 +199,7 @@ void NodeParamViewWidgetBridge::SetInputValue(const QVariant &value, int track)
void NodeParamViewWidgetBridge::SetInputValueInternal(const QVariant &value, int track, MultiUndoCommand *command, bool insert_on_all_tracks_if_no_key)
{
if (GetInnerInput().IsKeyframing()) {
rational node_time = GetCurrentTimeAsNodeTime();
NodeKeyframe* existing_key = GetInnerInput().GetKeyframeAtTimeOnTrack(node_time, track);
if (existing_key) {
command->add_child(new NodeParamSetKeyframeValueCommand(existing_key, value));
} else {
// No existing key, create a new one
int nb_tracks = NodeValue::get_number_of_keyframe_tracks(GetInnerInput().node()->GetInputDataType(GetInnerInput().input()));
for (int i=0; i<nb_tracks; i++) {
QVariant track_value;
if (i == track) {
track_value = value;
} else if (!insert_on_all_tracks_if_no_key) {
continue;
} else {
track_value = GetInnerInput().node()->GetSplitValueAtTimeOnTrack(GetInnerInput().input(), node_time, i, GetInnerInput().element());
}
NodeKeyframe* new_key = new NodeKeyframe(node_time,
track_value,
GetInnerInput().node()->GetBestKeyframeTypeForTimeOnTrack(NodeKeyframeTrackReference(GetInnerInput(), i), node_time),
i,
GetInnerInput().element(),
GetInnerInput().input());
command->add_child(new NodeParamInsertKeyframeCommand(GetInnerInput().node(), new_key));
}
}
} else {
command->add_child(new NodeParamSetStandardValueCommand(NodeKeyframeTrackReference(GetInnerInput(), track), value));
}
Node::SetValueAtTime(GetInnerInput(), GetCurrentTimeAsNodeTime(), value, track, command, insert_on_all_tracks_if_no_key);
}
void NodeParamViewWidgetBridge::ProcessSlider(NumericSliderBase *slider, int slider_track, const QVariant &value)
@@ -278,6 +246,7 @@ void NodeParamViewWidgetBridge::WidgetCallback()
case NodeValue::kGenerateJob:
case NodeValue::kVideoParams:
case NodeValue::kAudioParams:
case NodeValue::kDataTypeCount:
break;
case NodeValue::kInt:
{
@@ -454,6 +423,7 @@ void NodeParamViewWidgetBridge::UpdateWidgetValues()
case NodeValue::kGenerateJob:
case NodeValue::kVideoParams:
case NodeValue::kAudioParams:
case NodeValue::kDataTypeCount:
break;
case NodeValue::kInt:
{
+188 -45
View File
@@ -23,6 +23,7 @@
#include <QInputDialog>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPushButton>
#include <QScrollBar>
#include <QToolTip>
@@ -32,6 +33,7 @@
#include "node/factory.h"
#include "node/group/group.h"
#include "node/traverser.h"
#include "ui/icons/icons.h"
#include "widget/menu/menushared.h"
#include "widget/timebased/timebasedview.h"
@@ -43,10 +45,12 @@ const double NodeView::kMinimumScale = 0.1;
NodeView::NodeView(QWidget *parent) :
HandMovableView(parent),
NodeCopyPasteService(QStringLiteral("nodes")),
drop_edge_(nullptr),
create_edge_(nullptr),
create_edge_output_item_(nullptr),
create_edge_input_item_(nullptr),
overlay_view_(nullptr),
scale_(1.0)
{
setScene(&scene_);
@@ -83,6 +87,10 @@ NodeView::~NodeView()
void NodeView::SetContexts(const QVector<Node*> &nodes)
{
if (overlay_view_) {
CloseOverlay();
}
// Remove contexts that are no longer in the list
foreach (Node *n, contexts_) {
if (!nodes.contains(n)) {
@@ -213,20 +221,67 @@ void NodeView::Paste()
void NodeView::Duplicate()
{
if (!selected_nodes_.isEmpty()) {
Node::PositionMap map;
QVector<Node*> selected = selected_nodes_;
QVector<Node*> new_nodes;
new_nodes.resize(selected_nodes_.size());
Node::PositionMap map;
for (int i=0; i<new_nodes.size(); i++) {
Node *og = selected_nodes_.at(i);
Node *copy = og->copy();
Node::CopyInputs(og, copy, false);
map.insert(copy, GetAssumedPositionForSelectedNode(og));
new_nodes[i] = copy;
new_nodes.resize(selected.size());
// Create copies of each selected node, checking for groups and adding children if necessary
for (int i=0; i<selected.size(); i++) {
new_nodes[i] = selected.at(i)->copy();
if (NodeGroup *g = dynamic_cast<NodeGroup*>(selected.at(i))) {
for (auto it=g->GetContextPositions().cbegin(); it!=g->GetContextPositions().cend(); it++) {
if (!selected.contains(it.key())) {
// This should automatically recurse if this is a group inside a group
selected.append(it.key());
}
}
new_nodes.resize(selected.size());
}
}
Node::CopyDependencyGraph(selected_nodes_, new_nodes, nullptr);
// Get positions in contexts, add input passthroughs, and copy input values/keyframes
for (int i=0; i<new_nodes.size(); i++) {
Node *og = selected.at(i);
Node *copy = new_nodes.at(i);
Node::Position pos;
if (GetAssumedPositionForSelectedNode(og, &pos)) {
map.insert(copy, pos);
}
for (auto it=og->GetContextPositions().cbegin(); it!=og->GetContextPositions().cend(); it++) {
Node *child_og = it.key();
int child_index = selected.indexOf(child_og);
if (child_index != -1) {
Node *child_copy = new_nodes.at(child_index);
copy->SetNodePositionInContext(child_copy, it.value());
}
}
if (NodeGroup *src_group = dynamic_cast<NodeGroup*>(og)) {
NodeGroup *dst_group = static_cast<NodeGroup*>(copy);
for (auto it=src_group->GetInputPassthroughs().cbegin(); it!=src_group->GetInputPassthroughs().cend(); it++) {
NodeInput input = it->second;
input.set_node(new_nodes.at(selected.indexOf(input.node())));
dst_group->AddInputPassthrough(input, it->first);
}
dst_group->SetOutputPassthrough(new_nodes.at(selected.indexOf(src_group->GetOutputPassthrough())));
}
Node::CopyInputs(selected.at(i), new_nodes.at(i), false);
}
// Copy connections
Node::CopyDependencyGraph(selected, new_nodes, nullptr);
// Set root level context positions and attach to
PostPaste(new_nodes, map);
}
}
@@ -303,6 +358,8 @@ void NodeView::keyPressEvent(QKeyEvent *event)
break;
}
emit EscPressed();
/* fall through */
default:
super::keyPressEvent(event);
@@ -394,8 +451,10 @@ void NodeView::mousePressEvent(QMouseEvent *event)
}
}
// Default QGraphicsView functionality (selecting, dragging, etc.)
super::mousePressEvent(event);
if (attached_items_.isEmpty()) {
// Default QGraphicsView functionality (selecting, dragging, etc.)
super::mousePressEvent(event);
}
// For any selected item, store its position in case the user is dragging it somewhere else
auto selected_items = scene_.GetSelectedItems();
@@ -416,7 +475,9 @@ void NodeView::mouseMoveEvent(QMouseEvent *event)
return;
}
super::mouseMoveEvent(event);
if (attached_items_.isEmpty()) {
super::mouseMoveEvent(event);
}
// See if there are any items attached
if (!attached_items_.isEmpty()) {
@@ -443,26 +504,26 @@ void NodeView::mouseMoveEvent(QMouseEvent *event)
if (new_drop_edge) {
drop_input_.Reset();
NodeValue::Type drop_edge_data_type = NodeValue::kNone;
NodeValue::Type drop_edge_data_type = new_drop_edge->input().GetDataType();
// Run the Node and determine what type is being used
NodeTraverser traverser;
NodeValue drop_edge_value = traverser.GenerateRow(new_drop_edge->output(), TimeRange(0, 0))[new_drop_edge->input().input()];
drop_edge_data_type = drop_edge_value.type();
// Determine best input to connect to our new node
if (attached_node->GetEffectInput().IsValid()) {
// If node specifies an effect input, use that immediately
drop_input_ = attached_node->GetEffectInput();
} else {
// Otherwise, we may have to iterate to find a valid one
for (const QString& input : attached_node->inputs()) {
NodeInput i(attached_node, input);
// Iterate through the inputs of our dragging node and see if our node has any acceptable
// inputs to connect to for this type
for (const QString& input : attached_node->inputs()) {
NodeInput i(attached_node, input);
if (attached_node->IsInputConnectable(input)) {
if (attached_node->GetInputDataType(input) == drop_edge_data_type) {
// Found exactly the type we're looking for, set and break this loop
drop_input_ = i;
break;
} else if (!drop_input_.IsValid()) {
// Default to first connectable input
drop_input_ = i;
if (attached_node->IsInputConnectable(input)) {
if (attached_node->GetInputDataType(input) == drop_edge_data_type) {
// Found exactly the type we're looking for, set and break this loop
drop_input_ = i;
break;
} else if (!drop_input_.IsValid()) {
// Default to first connectable input
drop_input_ = i;
}
}
}
}
@@ -503,6 +564,8 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event)
Node *select_context = nullptr;
QVector<Node*> select_nodes;
bool had_attached_items = !attached_items_.isEmpty();
if (!attached_items_.isEmpty()) {
select_context = nullptr;
@@ -586,7 +649,9 @@ void NodeView::mouseReleaseEvent(QMouseEvent *event)
Core::instance()->undo_stack()->pushIfHasChildren(command);
super::mouseReleaseEvent(event);
if (!had_attached_items) {
super::mouseReleaseEvent(event);
}
if (select_context) {
scene_.context_map().value(select_context)->Select(select_nodes);
@@ -610,6 +675,10 @@ void NodeView::resizeEvent(QResizeEvent *event)
super::resizeEvent(event);
RepositionMiniMap();
if (overlay_view_) {
ResizeOverlay();
}
}
void NodeView::UpdateSelectionCache()
@@ -1020,12 +1089,13 @@ NodeViewItem *NodeView::GetAssumedItemForSelectedNode(Node *node)
return nullptr;
}
Node::Position NodeView::GetAssumedPositionForSelectedNode(Node *node)
bool NodeView::GetAssumedPositionForSelectedNode(Node *node, Node::Position *pos)
{
if (NodeViewItem *item = GetAssumedItemForSelectedNode(node)) {
return item->GetNodePositionData();
*pos = item->GetNodePositionData();
return true;
} else {
return Node::Position();
return false;
}
}
@@ -1174,6 +1244,7 @@ void NodeView::GroupNodes()
if (!output_passthrough) {
// Default to the first node we find that doesn't output to a node inside the group
output_passthrough = nodes_to_group.first();
foreach (Node *potential_in, nodes_to_group) {
if (potential_in != n && !n->OutputsTo(potential_in, false)) {
output_passthrough = n;
@@ -1236,7 +1307,37 @@ void NodeView::ShowNodeProperties()
Node *first_node = selected_nodes_.first();
if (NodeGroup *group = dynamic_cast<NodeGroup*>(first_node)) {
emit NodeGroupOpenRequested(group);
if (!overlay_view_) {
overlay_view_ = new NodeView(this);
overlay_view_->show();
QPushButton *overlay_close_btn = new QPushButton(overlay_view_);
overlay_close_btn->setIcon(icon::Error);
int offset = overlay_close_btn->sizeHint().width()/2;
overlay_close_btn->move(offset, offset);
overlay_close_btn->show();
connect(overlay_view_, &NodeView::NodesSelected, this, &NodeView::NodesSelected);
connect(overlay_view_, &NodeView::NodesDeselected, this, &NodeView::NodesDeselected);
connect(overlay_view_, &NodeView::NodeGroupOpened, this, &NodeView::NodeGroupOpened);
connect(overlay_view_, &NodeView::NodeGroupClosed, this, &NodeView::NodeGroupClosed);
connect(overlay_view_, &NodeView::EscPressed, this, &NodeView::CloseOverlay);
connect(overlay_close_btn, &QPushButton::clicked, this, &NodeView::CloseOverlay);
const QColor &bgcol = overlay_view_->palette().base().color();
overlay_view_->setStyleSheet(QStringLiteral("QGraphicsView { background: rgba(%1, %2, %3, 0.8); }").arg(QString::number(bgcol.red()), QString::number(bgcol.green()), QString::number(bgcol.blue())));
overlay_close_btn->setStyleSheet(QStringLiteral("background: transparent; border: none;"));
}
overlay_view_->SetContexts({group});
ResizeOverlay();
QMetaObject::invokeMethod(overlay_view_, &NodeView::CenterOnItemsBoundingRect, Qt::QueuedConnection);
overlay_view_->setFocus();
emit NodesDeselected(selected_nodes_);
overlay_view_->SelectAll();
emit NodeGroupOpened(group);
} else {
LabelSelectedNodes();
}
@@ -1272,6 +1373,17 @@ void NodeView::ItemAboutToBeDeleted(NodeViewItem *item)
}
}
void NodeView::CloseOverlay()
{
if (overlay_view_->overlay_view_) {
overlay_view_->CloseOverlay();
}
overlay_view_->deleteLater();
overlay_view_ = nullptr;
emit NodeGroupClosed();
}
void NodeView::AddContext(Node *n)
{
NodeViewContext *ctx = scene_.AddContext(n);
@@ -1341,8 +1453,8 @@ void NodeView::EndEdgeDrag(bool cancel)
create_edge_input_item_->SetHighlighted(false);
}
NodeInput &creating_input = create_edge_input_;
if (create_edge_output_item_ && create_edge_input_item_ && !cancel) {
NodeInput &creating_input = create_edge_input_;
if (creating_input.IsValid()) {
// Make connection
if (!reconnected_to_itself) {
@@ -1353,28 +1465,43 @@ void NodeView::EndEdgeDrag(bool cancel)
}
while (NodeGroup *input_group = dynamic_cast<NodeGroup*>(creating_input.node())) {
creating_input = input_group->GetInputPassthroughs().value(creating_input.input());
creating_input = input_group->GetInputFromID(creating_input.input());
}
if (creating_input.IsConnected()) {
Node::OutputConnection existing_edge_to_remove = {creating_input.GetConnectedOutput(), creating_input};
command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second));
Node *already_connected_output = creating_input.GetConnectedOutput();
NodeViewContext *ctx = GetContextItemFromNodeItem(create_edge_input_item_);
if (ctx && !ctx->GetItemFromMap(already_connected_output)) {
if (QMessageBox::warning(this, QString(), tr("Input \"%1\" is currently connected to node \"%2\", which is not visible in this context. "
"By connecting this, that connection will be removed. Do you wish to continue?").arg(creating_input.name(), already_connected_output->GetLabelAndName()),
QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
cancel = true;
}
}
if (!cancel) {
command->add_child(new NodeEdgeRemoveCommand(existing_edge_to_remove.first, existing_edge_to_remove.second));
}
}
command->add_child(new NodeEdgeAddCommand(creating_output, creating_input));
if (!cancel) {
command->add_child(new NodeEdgeAddCommand(creating_output, creating_input));
// If the output is not in the input's context, add it now. We check the item rather than
// the node itself, because sometimes a node may not be in the context but another node
// representing it will be (e.g. groups)
if (!scene_.context_map().value(create_edge_input_item_->GetContext())->GetItemFromMap(creating_output)) {
command->add_child(new NodeSetPositionCommand(creating_output, create_edge_input_item_->GetContext(), scene_.context_map().value(create_edge_input_item_->GetContext())->MapScenePosToNodePosInContext(create_edge_output_item_->scenePos())));
// If the output is not in the input's context, add it now. We check the item rather than
// the node itself, because sometimes a node may not be in the context but another node
// representing it will be (e.g. groups)
if (!scene_.context_map().value(create_edge_input_item_->GetContext())->GetItemFromMap(creating_output)) {
command->add_child(new NodeSetPositionCommand(creating_output, create_edge_input_item_->GetContext(), scene_.context_map().value(create_edge_input_item_->GetContext())->MapScenePosToNodePosInContext(create_edge_output_item_->scenePos())));
}
}
}
creating_input.Reset();
}
}
creating_input.Reset();
create_edge_output_item_ = nullptr;
create_edge_input_item_ = nullptr;
@@ -1429,6 +1556,22 @@ void NodeView::PostPaste(const QVector<Node *> &new_nodes, const Node::PositionM
SetAttachedItems(new_attached);
}
void NodeView::ResizeOverlay()
{
overlay_view_->resize(this->size());
}
NodeViewContext *NodeView::GetContextItemFromNodeItem(NodeViewItem *item)
{
QGraphicsItem *i = item;
while ((i = i->parentItem())) {
if (NodeViewContext *nvc = dynamic_cast<NodeViewContext*>(i)) {
return nvc;
}
}
return nullptr;
}
void NodeView::SetAttachedItems(const QVector<AttachedItem> &items)
{
// Detach anything currently attached
+23 -3
View File
@@ -54,7 +54,16 @@ public:
const QVector<Node*> &GetContexts() const
{
return contexts_;
if (overlay_view_) {
return overlay_view_->GetContexts();
} else {
return contexts_;
}
}
bool IsGroupOverlay() const
{
return overlay_view_;
}
void CloseContextsBelongingToProject(Project *project);
@@ -109,7 +118,10 @@ signals:
void NodesDeselected(const QVector<Node*>& nodes);
void NodeGroupOpenRequested(NodeGroup *group);
void NodeGroupOpened(NodeGroup *group);
void NodeGroupClosed();
void EscPressed();
protected:
virtual void keyPressEvent(QKeyEvent *event) override;
@@ -149,7 +161,7 @@ private:
QPointF GetEstimatedPositionForContext(NodeViewItem *item, Node *context) const;
NodeViewItem *GetAssumedItemForSelectedNode(Node *node);
Node::Position GetAssumedPositionForSelectedNode(Node *node);
bool GetAssumedPositionForSelectedNode(Node *node, Node::Position *pos);
Menu *CreateAddMenu(Menu *parent);
@@ -169,8 +181,12 @@ private:
void PostPaste(const QVector<Node*> &new_nodes, const Node::PositionMap &map);
void ResizeOverlay();
NodeViewMiniMap *minimap_;
NodeViewContext *GetContextItemFromNodeItem(NodeViewItem *item);
struct AttachedItem {
NodeViewItem* item;
Node *node;
@@ -202,6 +218,8 @@ private:
QMap<NodeViewItem*, QPointF> dragging_items_;
NodeView* overlay_view_;
double scale_;
static const double kMinimumScale;
@@ -252,6 +270,8 @@ private slots:
void ItemAboutToBeDeleted(NodeViewItem *item);
void CloseOverlay();
};
}
+10 -4
View File
@@ -464,10 +464,16 @@ void NodeViewItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
QVariant NodeViewItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{
if (change == ItemPositionHasChanged && node_) {
ReadjustAllEdges();
if (node_) {
if (change == ItemPositionHasChanged) {
ReadjustAllEdges();
UpdateContextRect();
UpdateContextRect();
} else if (change == ItemSelectedHasChanged) {
if (value.toBool()) {
qDebug() << "Selected node:" << node_;
}
}
}
return QGraphicsItem::itemChange(change, value);
@@ -786,7 +792,7 @@ NodeViewItem *NodeViewItem::GetItemForInput(NodeInput input)
if (NodeGroup *group = dynamic_cast<NodeGroup*>(node_)) {
if (input.node() != group) {
// Translate input to group input
QString id = NodeGroup::GetGroupInputIDFromInput(input);
QString id = group->GetIDOfPassthrough(input);
input.set_node(group);
input.set_input(id);
}
@@ -55,6 +55,7 @@ PlaybackControls::PlaybackControls(QWidget *parent) :
cur_tc_lbl_ = new RationalSlider();
cur_tc_lbl_->SetDisplayType(RationalSlider::kTime);
cur_tc_lbl_->SetMinimum(0);
connect(cur_tc_lbl_, &RationalSlider::ValueChanged, this, &PlaybackControls::TimeChanged);
lower_left_layout->addWidget(cur_tc_lbl_);
lower_left_layout->addStretch();
+12 -1
View File
@@ -57,6 +57,7 @@ SliderBase::SliderBase(QWidget *parent) :
void SliderBase::SetAlignment(Qt::Alignment alignment)
{
label_->setAlignment(alignment);
editor_->setAlignment(alignment);
}
bool SliderBase::IsTristate() const
@@ -104,7 +105,17 @@ void SliderBase::changeEvent(QEvent *e)
void SliderBase::UpdateLabel()
{
label_->setText(tristate_ ? tr("---") : GetFormattedValueToString());
QString s;
if (tristate_) {
s = tr("---");
} else if (label_substitutions_.contains(GetValueInternal())) {
s = label_substitutions_.value(GetValueInternal());
} else {
s = GetFormattedValueToString();
}
label_->setText(s);
}
QVariant SliderBase::AdjustValue(const QVariant &value) const
+8
View File
@@ -49,6 +49,12 @@ public:
QString GetFormattedValueToString(const QVariant& v) const;
void InsertLabelSubstitution(const QVariant &value, const QString &label)
{
label_substitutions_.insert(value, label);
UpdateLabel();
}
public slots:
void ShowEditor();
@@ -92,6 +98,8 @@ private:
bool format_plural_;
QMap<QVariant, QString> label_substitutions_;
private slots:
void LineEditConfirmed();
+25 -11
View File
@@ -41,6 +41,7 @@
#include "tool/rolling.h"
#include "tool/slide.h"
#include "tool/slip.h"
#include "tool/trackselect.h"
#include "tool/transition.h"
#include "tool/zoom.h"
#include "tool/tool.h"
@@ -59,6 +60,7 @@ namespace olive {
TimelineWidget::TimelineWidget(QWidget *parent) :
super(true, true, parent),
NodeCopyPasteService(QStringLiteral("timeline")),
rubberband_(QRubberBand::Rectangle, this),
active_tool_(nullptr),
use_audio_time_units_(false),
@@ -75,6 +77,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
timecode_label_->SetAlignment(Qt::AlignCenter);
timecode_label_->SetDisplayType(RationalSlider::kTime);
timecode_label_->setVisible(false);
timecode_label_->SetMinimum(0);
connect(timecode_label_, &RationalSlider::ValueChanged, this, &TimelineWidget::SetTimeAndSignal);
ruler_and_time_layout->addWidget(timecode_label_);
@@ -100,6 +103,7 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
tools_.fill(nullptr);
tools_.replace(olive::Tool::kPointer, new PointerTool(this));
tools_.replace(olive::Tool::kTrackSelect, new TrackSelectTool(this));
tools_.replace(olive::Tool::kEdit, new EditTool(this));
tools_.replace(olive::Tool::kRipple, new RippleTool(this));
tools_.replace(olive::Tool::kRolling, new RollingTool(this));
@@ -183,6 +187,14 @@ TimelineWidget::TimelineWidget(QWidget *parent) :
connect(Core::instance(), &Core::ToolChanged, this, &TimelineWidget::ToolChanged);
connect(Core::instance(), &Core::AddableObjectChanged, this, &TimelineWidget::AddableObjectChanged);
signal_block_change_timer_ = new QTimer(this);
signal_block_change_timer_->setInterval(1);
signal_block_change_timer_->setSingleShot(true);
connect(signal_block_change_timer_, &QTimer::timeout, this, [this]{
signal_block_change_timer_->stop();
emit BlockSelectionChanged(selected_blocks_);
});
}
TimelineWidget::~TimelineWidget()
@@ -471,7 +483,8 @@ void TimelineWidget::SplitAtPlayhead()
void TimelineWidget::ReplaceBlocksWithGaps(const QVector<Block *> &blocks,
bool remove_from_graph,
MultiUndoCommand *command,
bool handle_transitions)
bool handle_transitions,
bool handle_invalidations)
{
foreach (Block* b, blocks) {
if (dynamic_cast<GapBlock*>(b)) {
@@ -482,7 +495,7 @@ void TimelineWidget::ReplaceBlocksWithGaps(const QVector<Block *> &blocks,
Track* original_track = b->track();
command->add_child(new TrackReplaceBlockWithGapCommand(original_track, b, handle_transitions));
command->add_child(new TrackReplaceBlockWithGapCommand(original_track, b, handle_transitions, handle_invalidations));
if (remove_from_graph) {
command->add_child(new NodeRemoveWithExclusiveDependenciesAndDisconnect(b));
@@ -521,6 +534,9 @@ void TimelineWidget::DeleteSelected(bool ripple)
MultiUndoCommand* command = new MultiUndoCommand();
// Remove all selections
command->add_child(new SetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections()));
// For transitions, remove them but extend their attached blocks to fill their place
foreach (TransitionBlock* transition, transitions_to_delete) {
TransitionRemoveCommand *trc = new TransitionRemoveCommand(transition, true);
@@ -532,10 +548,7 @@ void TimelineWidget::DeleteSelected(bool ripple)
}
// Replace clips with gaps (effectively deleting them)
ReplaceBlocksWithGaps(clips_to_delete, true, command);
// Remove all selections
command->add_child(new SetSelectionsCommand(this, TimelineWidgetSelections(), GetSelections(), false));
ReplaceBlocksWithGaps(clips_to_delete, true, command, false, !ripple);
// Insert ripple command now that it's all cleaned up gaps
TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
@@ -1184,7 +1197,8 @@ void TimelineWidget::SetScrollZoomsByDefaultOnAllViews(bool e)
void TimelineWidget::SignalBlockSelectionChange()
{
emit BlockSelectionChanged(selected_blocks_);
signal_block_change_timer_->stop();
signal_block_change_timer_->start();
}
void TimelineWidget::AddGhost(TimelineViewGhostItem *ghost)
@@ -1231,7 +1245,7 @@ void TimelineWidget::NudgeInternal(rational amount)
// Nudge selections
TimelineWidgetSelections new_sel = GetSelections();
new_sel.ShiftTime(amount);
command->add_child(new TimelineWidget::SetSelectionsCommand(this, new_sel, GetSelections(), true));
command->add_child(new TimelineWidget::SetSelectionsCommand(this, new_sel, GetSelections()));
Core::instance()->undo_stack()->push(command);
}
@@ -1283,7 +1297,7 @@ void TimelineWidget::MoveToPlayheadInternal(bool out)
it.value().shift(track_adj);
}
}
command->add_child(new SetSelectionsCommand(this, new_sel, GetSelections(), true));
command->add_child(new SetSelectionsCommand(this, new_sel, GetSelections()));
Core::instance()->undo_stack()->push(command);
}
@@ -1357,7 +1371,7 @@ void TimelineWidget::SignalSelectedBlocks(QVector<Block *> input, bool filter)
selected_blocks_.append(input);
emit SignalBlockSelectionChange();
SignalBlockSelectionChange();
}
void TimelineWidget::SignalDeselectedBlocks(const QVector<Block *> &deselected_blocks)
@@ -1370,7 +1384,7 @@ void TimelineWidget::SignalDeselectedBlocks(const QVector<Block *> &deselected_b
selected_blocks_.removeOne(b);
}
emit SignalBlockSelectionChange();
SignalBlockSelectionChange();
}
void TimelineWidget::SignalDeselectedAllBlocks()
+4 -2
View File
@@ -124,7 +124,7 @@ public:
void RestoreSplitterState(const QByteArray& state);
static void ReplaceBlocksWithGaps(const QVector<Block *> &blocks, bool remove_from_graph, MultiUndoCommand *command, bool handle_transitions = true);
static void ReplaceBlocksWithGaps(const QVector<Block *> &blocks, bool remove_from_graph, MultiUndoCommand *command, bool handle_transitions = true, bool handle_invalidations = true);
/**
* @brief Retrieve the QGraphicsItem at a particular scene position
@@ -237,7 +237,7 @@ public:
class SetSelectionsCommand : public UndoCommand {
public:
SetSelectionsCommand(TimelineWidget* timeline, const TimelineWidgetSelections& now, const TimelineWidgetSelections& old, bool process_block_changes) :
SetSelectionsCommand(TimelineWidget* timeline, const TimelineWidgetSelections& now, const TimelineWidgetSelections& old, bool process_block_changes = true) :
timeline_(timeline),
old_(old),
now_(now),
@@ -329,6 +329,8 @@ private:
MultiUndoCommand *subtitle_show_command_;
QTimer *signal_block_change_timer_;
class SetSplitterSizesCommand : public UndoCommand
{
public:
@@ -38,6 +38,8 @@ set(OLIVE_SOURCES
widget/timelinewidget/tool/slide.h
widget/timelinewidget/tool/slip.cpp
widget/timelinewidget/tool/slip.h
widget/timelinewidget/tool/trackselect.cpp
widget/timelinewidget/tool/trackselect.h
widget/timelinewidget/tool/transition.cpp
widget/timelinewidget/tool/transition.h
widget/timelinewidget/tool/tool.cpp
+2 -2
View File
@@ -24,7 +24,7 @@
#include "node/factory.h"
#include "node/generator/shape/shapenode.h"
#include "node/generator/solid/solid.h"
#include "node/generator/text/text.h"
#include "node/generator/text/textv3.h"
#include "widget/timelinewidget/timelinewidget.h"
#include "widget/timelinewidget/undo/timelineundopointer.h"
@@ -138,7 +138,7 @@ void AddTool::MouseRelease(TimelineViewMouseEvent *event)
break;
case olive::Tool::kAddableTitle:
{
node_to_add = new TextGenerator();
node_to_add = new TextGeneratorV3();
break;
}
case olive::Tool::kAddableBars:
+36 -20
View File
@@ -44,6 +44,7 @@ PointerTool::PointerTool(TimelineWidget *parent) :
trimming_allowed_(true),
track_movement_allowed_(true),
gap_trimming_allowed_(false),
can_rubberband_select_(false),
rubberband_selecting_(false)
{
}
@@ -173,7 +174,7 @@ void PointerTool::MouseMove(TimelineViewMouseEvent *event)
// If we're performing an action, we can initiate ghosts
if (drag_movement_mode_ != Timeline::kNone) {
InitiateDrag(clicked_item_, drag_movement_mode_);
InitiateDrag(clicked_item_, drag_movement_mode_, event->GetModifiers());
}
// Set dragging to true here so no matter what, the drag isn't re-initiated until it's completed
@@ -246,6 +247,7 @@ void SetGhostToSlideMode(TimelineViewGhostItem* g)
void PointerTool::InitiateDragInternal(Block *clicked_item,
Timeline::MovementMode trim_mode,
Qt::KeyboardModifiers modifiers,
bool dont_roll_trims,
bool allow_nongap_rolling,
bool slide_instead_of_moving)
@@ -409,30 +411,41 @@ void PointerTool::InitiateDragInternal(Block *clicked_item,
}
Timeline::MovementMode flipped_mode = FlipTrimMode(trim_mode);
TimelineViewGhostItem* adjacent_ghost;
QVector<TimelineViewGhostItem*> adjacent_ghosts;
if (adjacent) {
adjacent_ghost = AddGhostFromBlock(adjacent, flipped_mode);
adjacent_ghosts.append(AddGhostFromBlock(adjacent, flipped_mode));
// Select adjacent's links if applicable
// FIXME: The check for `clips.size() == 1` may not be necessary, but I don't know yet.
// I'm only including it to prevent any potentially unintended behavior.
if (clips.size() == 1 && !(modifiers & Qt::AltModifier)) {
if (ClipBlock *adjacent_clip = dynamic_cast<ClipBlock*>(adjacent)) {
foreach (Block *adjacent_link, adjacent_clip->block_links()) {
adjacent_ghosts.append(AddGhostFromBlock(adjacent_link, flipped_mode));
}
}
}
} else if (trim_mode == Timeline::kTrimIn || block->next()) {
rational null_ghost_pos = (trim_mode == Timeline::kTrimIn) ? block->in() : block->out();
adjacent_ghost = AddGhostFromNull(null_ghost_pos, null_ghost_pos, clip_item->track()->ToReference(), flipped_mode);
} else {
adjacent_ghost = nullptr;
adjacent_ghosts.append(AddGhostFromNull(null_ghost_pos, null_ghost_pos, clip_item->track()->ToReference(), flipped_mode));
}
// If we have an adjacent block (for any reason), this is a roll edit and the adjacent is
// expected to fill the remaining space (no gap needs to be created)
ghost->SetData(TimelineViewGhostItem::kTrimIsARollEdit, static_cast<bool>(adjacent));
if (adjacent_ghost) {
if (treat_trim_as_slide) {
// We're sliding a transition rather than a pure trim/roll
SetGhostToSlideMode(adjacent_ghost);
} else if (dynamic_cast<GapBlock*>(block)) {
ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true);
} else {
adjacent_ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true);
foreach (TimelineViewGhostItem *adjacent_ghost, adjacent_ghosts) {
if (adjacent_ghost) {
if (treat_trim_as_slide) {
// We're sliding a transition rather than a pure trim/roll
SetGhostToSlideMode(adjacent_ghost);
} else if (dynamic_cast<GapBlock*>(block)) {
ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true);
} else {
adjacent_ghost->SetData(TimelineViewGhostItem::kTrimShouldBeIgnored, true);
}
}
}
}
@@ -560,6 +573,10 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event)
ghost->GetAdjustedLength(),
ghost->GetMode());
if (event->GetModifiers() & Qt::ControlModifier) {
}
c->SetTrimIsARollEdit(ghost->GetData(TimelineViewGhostItem::kTrimIsARollEdit).toBool());
command->add_child(c);
@@ -575,7 +592,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event)
} else {
new_sel.TrimOut(reference_ghost->GetOutAdjustment());
}
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), true));
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections()));
}
}
@@ -624,7 +641,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event)
TimelineWidgetSelections new_sel = parent()->GetSelections();
new_sel.ShiftTime(blocks_moving.first().ghost->GetInAdjustment());
new_sel.ShiftTracks(drag_track_type_, blocks_moving.first().ghost->GetTrackAdjustment());
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), true));
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections()));
}
if (!blocks_sliding.isEmpty()) {
@@ -685,7 +702,7 @@ void PointerTool::FinishDrag(TimelineViewMouseEvent *event)
// Adjust selections
TimelineWidgetSelections new_sel = parent()->GetSelections();
new_sel.ShiftTime(movement);
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections(), true));
command->add_child(new TimelineWidget::SetSelectionsCommand(parent(), new_sel, parent()->GetSelections()));
}
}
@@ -714,10 +731,9 @@ Timeline::MovementMode PointerTool::IsCursorInTrimHandle(Block *block, qreal cur
}
}
void PointerTool::InitiateDrag(Block *clicked_item,
Timeline::MovementMode trim_mode)
void PointerTool::InitiateDrag(Block *clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers)
{
InitiateDragInternal(clicked_item, trim_mode, false, false, false);
InitiateDragInternal(clicked_item, trim_mode, modifiers, false, false, false);
}
//#define HIDE_GAP_GHOSTS
+9 -6
View File
@@ -39,8 +39,7 @@ public:
protected:
virtual void FinishDrag(TimelineViewMouseEvent *event);
virtual void InitiateDrag(Block* clicked_item,
Timeline::MovementMode trim_mode);
virtual void InitiateDrag(Block* clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers);
TimelineViewGhostItem* AddGhostFromBlock(Block *block, Timeline::MovementMode mode, bool check_if_exists = false);
@@ -66,13 +65,12 @@ protected:
void InitiateDragInternal(Block* clicked_item,
Timeline::MovementMode trim_mode,
Qt::KeyboardModifiers modifiers,
bool dont_roll_trims,
bool allow_nongap_rolling, bool slide_instead_of_moving);
const Timeline::MovementMode& drag_movement_mode() const
{
return drag_movement_mode_;
}
const Timeline::MovementMode& drag_movement_mode() const { return drag_movement_mode_; }
void set_drag_movement_mode(const Timeline::MovementMode &d) { drag_movement_mode_ = d; }
void SetMovementAllowed(bool e)
{
@@ -94,6 +92,11 @@ protected:
gap_trimming_allowed_ = e;
}
void SetClickedItem(Block *b)
{
clicked_item_ = b;
}
private:
Timeline::MovementMode IsCursorInTrimHandle(Block* block, qreal cursor_x);
+2 -3
View File
@@ -34,10 +34,9 @@ RippleTool::RippleTool(TimelineWidget* parent) :
SetGapTrimmingAllowed(true);
}
void RippleTool::InitiateDrag(Block *clicked_item,
Timeline::MovementMode trim_mode)
void RippleTool::InitiateDrag(Block *clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers)
{
InitiateDragInternal(clicked_item, trim_mode, true, true, false);
InitiateDragInternal(clicked_item, trim_mode, modifiers, true, true, false);
if (!parent()->HasGhosts()) {
return;
+1 -2
View File
@@ -32,8 +32,7 @@ public:
protected:
virtual void FinishDrag(TimelineViewMouseEvent *event) override;
virtual void InitiateDrag(Block* clicked_item,
Timeline::MovementMode trim_mode) override;
virtual void InitiateDrag(Block* clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers) override;
};
}
+2 -3
View File
@@ -33,10 +33,9 @@ RollingTool::RollingTool(TimelineWidget* parent) :
SetGapTrimmingAllowed(true);
}
void RollingTool::InitiateDrag(Block *clicked_item,
Timeline::MovementMode trim_mode)
void RollingTool::InitiateDrag(Block *clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers)
{
InitiateDragInternal(clicked_item, trim_mode, false, true, false);
InitiateDragInternal(clicked_item, trim_mode, modifiers, false, true, false);
}
}
+1 -2
View File
@@ -31,8 +31,7 @@ public:
RollingTool(TimelineWidget* parent);
protected:
virtual void InitiateDrag(Block* clicked_item,
Timeline::MovementMode trim_mode) override;
virtual void InitiateDrag(Block* clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers) override;
};
}
+2 -2
View File
@@ -34,9 +34,9 @@ SlideTool::SlideTool(TimelineWidget* parent) :
SetGapTrimmingAllowed(true);
}
void SlideTool::InitiateDrag(Block *clicked_item, Timeline::MovementMode trim_mode)
void SlideTool::InitiateDrag(Block *clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers)
{
InitiateDragInternal(clicked_item, trim_mode, false, true, true);
InitiateDragInternal(clicked_item, trim_mode, modifiers, false, true, true);
}
}
+1 -2
View File
@@ -31,8 +31,7 @@ public:
SlideTool(TimelineWidget* parent);
protected:
virtual void InitiateDrag(Block* clicked_item,
Timeline::MovementMode trim_mode) override;
virtual void InitiateDrag(Block* clicked_item, Timeline::MovementMode trim_mode, Qt::KeyboardModifiers modifiers) override;
};
@@ -0,0 +1,96 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2021 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "trackselect.h"
#include "node/block/gap/gap.h"
#include "node/output/track/track.h"
#include "widget/timelinewidget/timelinewidget.h"
namespace olive {
TrackSelectTool::TrackSelectTool(TimelineWidget *parent) :
PointerTool(parent)
{
}
void TrackSelectTool::MousePress(TimelineViewMouseEvent *event)
{
QVector<Block*> blocks;
bool forward = !(event->GetModifiers() & Qt::ControlModifier);
parent()->DeselectAll();
if (event->GetModifiers() & Qt::ShiftModifier) {
// Track only
Track *track = parent()->GetTrackFromReference(event->GetTrack());
if (track) {
SelectBlocksOnTrack(track, event, &blocks, forward);
}
} else {
// All tracks
foreach (Track *track, parent()->sequence()->GetTracks()) {
SelectBlocksOnTrack(track, event, &blocks, forward);
}
}
if (!blocks.isEmpty()) {
parent()->SignalSelectedBlocks(blocks);
set_drag_movement_mode(Timeline::kMove);
SetClickedItem(blocks.first());
drag_start_ = event->GetCoordinates();
} else {
set_drag_movement_mode(Timeline::kNone);
}
}
void TrackSelectTool::SelectBlocksOnTrack(Track *track, TimelineViewMouseEvent *event, QVector<Block*> *blocks, bool forward)
{
Block *b = track->NearestBlockBeforeOrAt(event->GetFrame());
if (!b && !track->Blocks().isEmpty() && !forward) {
// Fallback to first or last block in track
b = track->Blocks().last();
}
while (b) {
if (!dynamic_cast<GapBlock*>(b)) {
if (!blocks->contains(b)) {
parent()->AddSelection(b);
blocks->append(b);
}
if (!(event->GetModifiers() & Qt::AltModifier)) {
if (ClipBlock *clip = dynamic_cast<ClipBlock*>(b)) {
foreach (Block *link, clip->block_links()) {
if (!blocks->contains(link)) {
parent()->AddSelection(link);
blocks->append(link);
}
}
}
}
}
b = forward ? b->next() : b->previous();
}
}
}
@@ -0,0 +1,42 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2021 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef TRACKSELECTTOOL_H
#define TRACKSELECTTOOL_H
#include "pointer.h"
namespace olive {
class TrackSelectTool : public PointerTool
{
public:
TrackSelectTool(TimelineWidget *parent);
virtual void MousePress(TimelineViewMouseEvent *event) override;
private:
void SelectBlocksOnTrack(Track *track, TimelineViewMouseEvent *event, QVector<Block *> *blocks, bool forward);
};
}
#endif // TRACKSELECTTOOL_H
@@ -28,6 +28,7 @@
#include <QtMath>
#include "core.h"
#include "ui/icons/icons.h"
#include "widget/menu/menu.h"
#include "widget/timelinewidget/undo/timelineundogeneral.h"
@@ -56,15 +57,21 @@ TrackViewItem::TrackViewItem(Track* track, QWidget *parent) :
connect(line_edit_, &FocusableLineEdit::Cancelled, this, &TrackViewItem::LineEditCancelled);
stack_->addWidget(line_edit_);
mute_button_ = CreateMSLButton(tr("M"), Qt::red);
mute_button_ = CreateMSLButton(Qt::red);
mute_button_->setChecked(track->IsMuted());
UpdateMuteButton(track->IsMuted());
connect(mute_button_, &QPushButton::toggled, track_, &Track::SetMuted);
connect(mute_button_, &QPushButton::toggled, this, &TrackViewItem::UpdateMuteButton);
layout->addWidget(mute_button_);
/*solo_button_ = CreateMSLButton(tr("S"), Qt::yellow);
layout->addWidget(solo_button_);*/
lock_button_ = CreateMSLButton(tr("L"), Qt::gray);
lock_button_ = CreateMSLButton(Qt::gray);
lock_button_->setChecked(track->IsLocked());
UpdateLockButton(track->IsLocked());
connect(lock_button_, &QPushButton::toggled, track_, &Track::SetLocked);
connect(lock_button_, &QPushButton::toggled, this, &TrackViewItem::UpdateLockButton);
layout->addWidget(lock_button_);
setMinimumHeight(mute_button_->height());
@@ -74,9 +81,9 @@ TrackViewItem::TrackViewItem(Track* track, QWidget *parent) :
connect(this, &QWidget::customContextMenuRequested, this, &TrackViewItem::ShowContextMenu);
}
QPushButton *TrackViewItem::CreateMSLButton(const QString& text, const QColor& checked_color) const
QPushButton *TrackViewItem::CreateMSLButton(const QColor& checked_color) const
{
QPushButton* button = new QPushButton(text);
QPushButton* button = new QPushButton();
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
button->setCheckable(true);
button->setStyleSheet(QStringLiteral("QPushButton::checked { background: %1; }").arg(checked_color.name()));
@@ -169,4 +176,14 @@ void TrackViewItem::DeleteAllEmptyTracks()
}
}
void TrackViewItem::UpdateMuteButton(bool e)
{
mute_button_->setIcon(e ? icon::EyeClosed : icon::EyeOpened);
}
void TrackViewItem::UpdateLockButton(bool e)
{
lock_button_->setIcon(e ? icon::LockClosed : icon::LockOpened);
}
}
@@ -39,7 +39,7 @@ public:
QWidget* parent = nullptr);
private:
QPushButton* CreateMSLButton(const QString &text, const QColor &checked_color) const;
QPushButton* CreateMSLButton(const QColor &checked_color) const;
QStackedWidget* stack_;
@@ -67,6 +67,10 @@ private slots:
void DeleteAllEmptyTracks();
void UpdateMuteButton(bool e);
void UpdateLockButton(bool e);
};
}
@@ -490,7 +490,9 @@ void TrackReplaceBlockWithGapCommand::redo()
track_->EndOperation();
track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput);
if (handle_invalidations_) {
track_->Node::InvalidateCache(invalidate_range, Track::kBlockInput);
}
} else {
// Block is at the end of the track, simply remove it
@@ -547,7 +549,9 @@ void TrackReplaceBlockWithGapCommand::undo()
track_->EndOperation();
track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput);
if (handle_invalidations_) {
track_->Node::InvalidateCache(TimeRange(block_->in(), block_->out()), Track::kBlockInput);
}
} else {
// Our gap and existing gap were both null, our block must have been at the end and thus
@@ -233,13 +233,14 @@ private:
class TrackReplaceBlockWithGapCommand : public UndoCommand {
public:
TrackReplaceBlockWithGapCommand(Track* track, Block* block, bool handle_transitions = true) :
TrackReplaceBlockWithGapCommand(Track* track, Block* block, bool handle_transitions = true, bool handle_invalidations = true) :
track_(track),
block_(block),
existing_gap_(nullptr),
existing_merged_gap_(nullptr),
our_gap_(nullptr),
handle_transitions_(handle_transitions)
handle_transitions_(handle_transitions),
handle_invalidations_(handle_invalidations)
{
}
@@ -265,6 +266,7 @@ private:
GapBlock* our_gap_;
bool handle_transitions_;
bool handle_invalidations_;
QObject memory_manager_;
@@ -360,6 +362,62 @@ private:
};
class NodeBeginOperationCommand : public UndoCommand
{
public:
NodeBeginOperationCommand(Node *node) :
node_(node)
{}
virtual Project* GetRelevantProject() const override
{
return node_->project();
}
protected:
virtual void redo() override
{
node_->BeginOperation();
}
virtual void undo() override
{
node_->EndOperation();
}
private:
Node *node_;
};
class NodeEndOperationCommand : public UndoCommand
{
public:
NodeEndOperationCommand(Node *node) :
node_(node)
{}
virtual Project* GetRelevantProject() const override
{
return node_->project();
}
protected:
virtual void redo() override
{
node_->EndOperation();
}
virtual void undo() override
{
node_->BeginOperation();
}
private:
Node *node_;
};
}
#endif // TIMELINEUNDOGENERAL_H
@@ -488,26 +488,36 @@ void TimelineRippleDeleteGapsAtRegionsCommand::prepare()
}
max_gaps = qMax(max_gaps, gaps_on_track.size());
} else {
qWarning() << "Failed to find corresponding gap to region";
}
}
// For each gap on each track, find a corresponding gap on every other track (which may include
// a requested gap) to ripple in order to keep everything synchronized
QHash<GapBlock*, rational> gap_lengths;
for (int gap_index=0; gap_index<max_gaps; gap_index++) {
rational earliest_point = RATIONAL_MAX;
rational ripple_length = RATIONAL_MAX;
rational latest_point = RATIONAL_MIN;
foreach (const QVector<RemovalRequest> &gaps_on_track, requested_gaps) {
if (gap_index < gaps_on_track.size()) {
const RemovalRequest &gap = gaps_on_track.at(gap_index);
earliest_point = qMin(earliest_point, gap.range.in());
ripple_length = qMin(ripple_length, gap.range.length());
latest_point = qMax(latest_point, gap.range.out());
}
}
// Determine which gaps will be involved in this operation
QVector<GapBlock*> gaps;
bool all_tracks_unlocked = true;
foreach (Track* track, timeline_->GetTracks()) {
if (track->IsLocked()) {
all_tracks_unlocked = false;
continue;
}
@@ -548,7 +558,12 @@ void TimelineRippleDeleteGapsAtRegionsCommand::prepare()
if (gap) {
gaps.append(gap);
ripple_length = qMin(ripple_length, gap->length());
if (!gap_lengths.contains(gap)) {
gap_lengths.insert(gap, gap->length());
}
ripple_length = qMin(ripple_length, gap_lengths.value(gap));
}
if (ripple_length == 0) {
@@ -558,11 +573,24 @@ void TimelineRippleDeleteGapsAtRegionsCommand::prepare()
if (ripple_length > 0) {
foreach (GapBlock *gap, gaps) {
if (gap->length() == ripple_length) {
if (all_tracks_unlocked) {
commands_.append(new NodeBeginOperationCommand(gap->track()));
}
if (gap_lengths.value(gap) == ripple_length) {
commands_.append(new TrackRippleRemoveBlockCommand(gap->track(), gap));
} else {
commands_.append(new BlockResizeCommand(gap, gap->length() - ripple_length));
gap_lengths[gap] -= ripple_length;
commands_.append(new BlockResizeCommand(gap, gap_lengths.value(gap)));
}
if (all_tracks_unlocked) {
commands_.append(new NodeEndOperationCommand(gap->track()));
}
}
if (all_tracks_unlocked) {
commands_.append(new TimelineShiftCacheCommand(timeline_, latest_point, latest_point - ripple_length));
}
}
}
@@ -582,4 +610,14 @@ void TimelineRippleDeleteGapsAtRegionsCommand::undo()
}
}
void TimelineShiftCacheCommand::redo()
{
timeline_->ShiftCache(from_, to_);
}
void TimelineShiftCacheCommand::undo()
{
timeline_->ShiftCache(to_, from_);
}
}
@@ -38,7 +38,8 @@ namespace olive {
* By default, nothing takes this area meaning all subsequent clips are pushed backward, however you can specify
* a block to insert at the `in` point. No checking is done to ensure `insert` is the same length as `in` to `out`.
*/
class TrackRippleRemoveAreaCommand : public UndoCommand {
class TrackRippleRemoveAreaCommand : public UndoCommand
{
public:
TrackRippleRemoveAreaCommand(Track* track, const TimeRange& range);
@@ -98,7 +99,8 @@ private:
};
class TrackListRippleRemoveAreaCommand : public UndoCommand {
class TrackListRippleRemoveAreaCommand : public UndoCommand
{
public:
TrackListRippleRemoveAreaCommand(TrackList* list, rational in, rational out) :
list_(list),
@@ -134,7 +136,8 @@ private:
};
class TimelineRippleRemoveAreaCommand : public MultiUndoCommand {
class TimelineRippleRemoveAreaCommand : public MultiUndoCommand
{
public:
TimelineRippleRemoveAreaCommand(Sequence* timeline, rational in, rational out);
@@ -148,7 +151,8 @@ private:
};
class TrackListRippleToolCommand : public UndoCommand {
class TrackListRippleToolCommand : public UndoCommand
{
public:
struct RippleInfo {
Block* block;
@@ -200,7 +204,8 @@ private:
};
class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand {
class TimelineRippleDeleteGapsAtRegionsCommand : public UndoCommand
{
public:
TimelineRippleDeleteGapsAtRegionsCommand(Sequence* vo, const QVector<QPair<Track*, TimeRange> >& regions) :
timeline_(vo),
@@ -243,6 +248,34 @@ private:
};
class TimelineShiftCacheCommand : public UndoCommand
{
public:
TimelineShiftCacheCommand(Sequence* timeline, const rational &from, const rational &to) :
timeline_(timeline),
from_(from),
to_(to)
{}
virtual Project* GetRelevantProject() const override
{
return timeline_->project();
}
protected:
virtual void redo() override;
virtual void undo() override;
private:
Sequence* timeline_;
rational from_;
rational to_;
};
}
#endif // TIMELINEUNDORIPPLE_H
@@ -38,14 +38,15 @@ void BlockSplitCommand::redo()
if (!reconnect_tree_command_) {
reconnect_tree_command_ = new MultiUndoCommand();
new_block_ = static_cast<Block*>(Node::CopyNodeInGraph(block_, reconnect_tree_command_));
if (ClipBlock *new_clip = dynamic_cast<ClipBlock*>(new_block_)) {
ClipBlock *old_clip = static_cast<ClipBlock*>(block_);
new_clip->waveform() = old_clip->waveform();
}
}
reconnect_tree_command_->redo_now();
if (ClipBlock *new_clip = dynamic_cast<ClipBlock*>(new_block_)) {
ClipBlock *old_clip = static_cast<ClipBlock*>(block_);
new_clip->waveform() = old_clip->waveform();
}
// Determine our new lengths
rational new_length = point_ - block_->in();
rational new_part_length = block_->out() - point_;
@@ -360,6 +360,9 @@ void TimelineView::ToolChangedEvent(Tool::Item tool)
case Tool::kZoom:
setCursor(Qt::CrossCursor);
break;
case Tool::kTrackSelect:
setCursor(Qt::SizeHorCursor); // FIXME: Not the ideal cursor
break;
default:
unsetCursor();
}
+3
View File
@@ -42,6 +42,7 @@ Toolbar::Toolbar(QWidget *parent) :
// Create standard tool buttons
btn_pointer_tool_ = CreateToolButton(Tool::kPointer);
btn_trackselect_tool_ = CreateToolButton(Tool::kTrackSelect);
btn_edit_tool_ = CreateToolButton(Tool::kEdit);
btn_ripple_tool_ = CreateToolButton(Tool::kRipple);
btn_rolling_tool_ = CreateToolButton(Tool::kRolling);
@@ -107,6 +108,7 @@ void Toolbar::resizeEvent(QResizeEvent *e)
void Toolbar::Retranslate()
{
btn_pointer_tool_->setToolTip(tr("Pointer Tool"));
btn_trackselect_tool_->setToolTip(tr("Track Select Tool"));
btn_edit_tool_->setToolTip(tr("Edit Tool"));
btn_ripple_tool_->setToolTip(tr("Ripple Tool"));
btn_rolling_tool_->setToolTip(tr("Rolling Tool"));
@@ -124,6 +126,7 @@ void Toolbar::Retranslate()
void Toolbar::UpdateIcons()
{
btn_pointer_tool_->setIcon(icon::ToolPointer);
btn_trackselect_tool_->setIcon(icon::ToolTrackSelect);
btn_edit_tool_->setIcon(icon::ToolEdit);
btn_ripple_tool_->setIcon(icon::ToolRipple);
btn_rolling_tool_->setIcon(icon::ToolRolling);
+1
View File
@@ -173,6 +173,7 @@ private:
QList<ToolbarButton*> toolbar_btns_;
ToolbarButton* btn_pointer_tool_;
ToolbarButton* btn_trackselect_tool_;
ToolbarButton* btn_edit_tool_;
ToolbarButton* btn_ripple_tool_;
ToolbarButton* btn_rolling_tool_;
+2
View File
@@ -32,6 +32,8 @@ set(OLIVE_SOURCES
widget/viewer/viewersafemargininfo.h
widget/viewer/viewersizer.cpp
widget/viewer/viewersizer.h
widget/viewer/viewertexteditor.cpp
widget/viewer/viewertexteditor.h
widget/viewer/viewerwindow.cpp
widget/viewer/viewerwindow.h
PARENT_SCOPE
+4 -5
View File
@@ -256,6 +256,8 @@ void ViewerWidget::DisconnectNodeEvent(ViewerOutput *n)
// Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected
QMetaObject::invokeMethod(this, &ViewerWidget::UpdateStack, Qt::QueuedConnection);
SetGizmos(nullptr);
}
void ViewerWidget::ConnectedNodeChangeEvent(ViewerOutput *n)
@@ -466,11 +468,8 @@ void ViewerWidget::ReceivedAudioBufferForPlayback()
// If the tempo must be adjusted, adjust now
if (tempo_processor_.IsOpen()) {
tempo_processor_.Push(pack.data(), pack.size());
int actual = tempo_processor_.Pull(pack.data(), pack.size());
if (actual != pack.size()) {
pack.resize(actual);
}
tempo_processor_.Push(pack);
pack = tempo_processor_.Pull();
}
// TempoProcessor may have emptied the array
+163 -18
View File
@@ -21,6 +21,8 @@
#include "viewerdisplay.h"
#include <OpenImageIO/imagebuf.h>
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QFileInfo>
#include <QMessageBox>
#include <QMouseEvent>
@@ -28,12 +30,23 @@
#include <QOpenGLFunctions>
#include <QOpenGLTexture>
#include <QPainter>
#include <QPushButton>
#include <QScreen>
#include <QTextEdit>
#include "common/define.h"
#include "common/functiontimer.h"
#include "common/html.h"
#include "config/config.h"
#include "core.h"
#include "node/gizmo/path.h"
#include "node/gizmo/point.h"
#include "node/gizmo/polygon.h"
#include "node/gizmo/screen.h"
#include "node/gizmo/text.h"
#include "node/traverser.h"
#include "viewertexteditor.h"
#include "window/mainwindow/mainwindow.h"
namespace olive {
@@ -44,7 +57,8 @@ ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) :
deinterlace_texture_(nullptr),
signal_cursor_color_(false),
gizmos_(nullptr),
gizmo_click_(false),
current_gizmo_(nullptr),
gizmo_drag_started_(false),
hand_dragging_(false),
deinterlace_(false),
show_fps_(false),
@@ -175,7 +189,7 @@ void ViewerDisplayWidget::SetTime(const rational &time)
}
}
QPoint ViewerDisplayWidget::TransformViewerSpaceToBufferSpace(QPoint pos)
QPointF ViewerDisplayWidget::TransformViewerSpaceToBufferSpace(const QPointF &pos)
{
/*
* Inversion will only fail if the viewer has been scaled by 0 in any direction
@@ -204,12 +218,12 @@ void ViewerDisplayWidget::IncrementSkippedFrames()
void ViewerDisplayWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && gizmos_
&& gizmos_->GizmoPress(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime()), TransformViewerSpaceToBufferSpace(event->pos()))) {
&& (current_gizmo_ = TryGizmoPress(gizmo_db_, TransformViewerSpaceToBufferSpace(event->pos())))) {
// Handle gizmo click
gizmo_click_ = true;
gizmo_drag_time_ = GetGizmoTime();
gizmo_start_drag_ = event->pos();
gizmo_last_drag_ = gizmo_start_drag_;
current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime()));
} else if (IsHandDrag(event)) {
@@ -242,14 +256,38 @@ void ViewerDisplayWidget::mouseMoveEvent(QMouseEvent *event)
hand_last_drag_pos_ = event->pos();
} else if (gizmo_click_) {
} else if (current_gizmo_) {
// Signal movement
gizmos_->GizmoMove(TransformViewerSpaceToBufferSpace(event->pos()),
gizmo_drag_time_,
event->modifiers());
gizmo_start_drag_ = event->pos();
update();
if (DraggableGizmo *draggable = dynamic_cast<DraggableGizmo*>(current_gizmo_)) {
if (!gizmo_drag_started_) {
QPointF start = TransformViewerSpaceToBufferSpace(gizmo_start_drag_);
rational gizmo_time = GetGizmoTime();
NodeTraverser t;
t.SetCacheVideoParams(gizmo_params_);
NodeValueRow row = t.GenerateRow(gizmos_, TimeRange(gizmo_time, gizmo_time + gizmo_params_.frame_rate_as_time_base()));
draggable->DragStart(row, start.x(), start.y(), gizmo_time);
gizmo_drag_started_ = true;
}
QPointF v = TransformViewerSpaceToBufferSpace(event->pos());
switch (draggable->GetDragValueBehavior()) {
case DraggableGizmo::kAbsolute:
// Above value is correct
break;
case DraggableGizmo::kDeltaFromPrevious:
v -= TransformViewerSpaceToBufferSpace(gizmo_last_drag_);
gizmo_last_drag_ = event->pos();
break;
case DraggableGizmo::kDeltaFromStart:
v -= TransformViewerSpaceToBufferSpace(gizmo_start_drag_);
break;
}
draggable->DragMove(v.x(), v.y(), event->modifiers());
}
} else {
@@ -268,13 +306,18 @@ void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event)
hand_dragging_ = false;
UpdateCursor();
} else if (gizmo_click_) {
} else if (current_gizmo_) {
// Handle gizmo
MultiUndoCommand *command = new MultiUndoCommand();
gizmos_->GizmoRelease(command);
Core::instance()->undo_stack()->pushIfHasChildren(command);
gizmo_click_ = false;
if (gizmo_drag_started_) {
MultiUndoCommand *command = new MultiUndoCommand();
if (DraggableGizmo *draggable = dynamic_cast<DraggableGizmo*>(current_gizmo_)) {
draggable->DragEnd(command);
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
gizmo_drag_started_ = false;
}
current_gizmo_ = nullptr;
} else {
@@ -284,6 +327,69 @@ void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event)
}
}
void ViewerDisplayWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && gizmos_) {
QPointF ptr = TransformViewerSpaceToBufferSpace(event->pos());
foreach (NodeGizmo *g, gizmos_->GetGizmos()) {
if (TextGizmo *text = dynamic_cast<TextGizmo*>(g)) {
if (text->GetRect().contains(ptr)) {
QTransform gizmo_transform = GenerateGizmoTransform();
ViewerTextEditor *text_edit = new ViewerTextEditor(gizmo_transform.m11(), this);
Html::HtmlToDoc(text_edit->document(), text->GetHtml());
text_edit->setProperty("gizmo", reinterpret_cast<quintptr>(text));
QRectF transformed_geom = gizmo_transform.map(text->GetRect()).boundingRect();
text_edit->setGeometry(transformed_geom.toRect());
ViewerTextEditorToolBar *toolbar = new ViewerTextEditorToolBar(this);
QPoint pos = mapToGlobal(QPoint(transformed_geom.x(), transformed_geom.y() - toolbar->height()));
for (QScreen *screen : qApp->screens()) {
if (screen->geometry().contains(pos)) {
if (pos.x() + toolbar->width() > screen->geometry().right()) {
pos.setX(screen->geometry().right() - toolbar->width());
}
break;
}
}
toolbar->move(pos);
toolbar->show();
text_edit->show();
connect(text_edit, &ViewerTextEditor::textChanged, this, &ViewerDisplayWidget::TextEditChanged);
text_edit->ConnectToolBar(toolbar);
QPoint text_edit_pos = text_edit->mapFrom(this, event->pos());
// Ensure text edit is actually focused rather than the toolbar
connect(toolbar, &ViewerTextEditorToolBar::FirstPaint, this, [this, text_edit, text_edit_pos]{
// Grab focus back from the toolbar
this->raise();
this->activateWindow();
text_edit->setFocus();
// Start text cursor where the user clicked
text_edit->setTextCursor(text_edit->cursorForPosition(text_edit_pos));
// HACK: On macOS, for some reason the QDockWidget receives focus before the
// ViewerTextEditor, causing the editor to close prematurely. However this only
// happens the first time the editor receives focus and not subsequent times, so
// if we get it to only listen after the first one, this solves the problem.
text_edit->SetListenToFocusEvents(true);
});
break;
}
}
}
}
super::mouseDoubleClickEvent(event);
}
void ViewerDisplayWidget::dragEnterEvent(QDragEnterEvent *event)
{
emit DragEntered(event);
@@ -396,7 +502,11 @@ void ViewerDisplayWidget::OnPaint()
QPainter p(inner_widget());
p.setWorldTransform(GenerateGizmoTransform());
gizmos_->DrawGizmos(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, range), &p);
gizmos_->UpdateGizmoPositions(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, range));
foreach (NodeGizmo *gizmo, gizmos_->GetGizmos()) {
gizmo->Draw(&p);
}
}
// Draw action/title safe areas
@@ -404,7 +514,7 @@ void ViewerDisplayWidget::OnPaint()
QPainter p(inner_widget());
p.setWorldTransform(GenerateWorldTransform());
p.setPen(Qt::lightGray);
p.setPen(QPen(Qt::lightGray, 0));
p.setBrush(Qt::NoBrush);
int x = 0, y = 0, w = width(), h = height();
@@ -563,6 +673,31 @@ QTransform ViewerDisplayWidget::GenerateGizmoTransform()
return gizmo_transform;
}
NodeGizmo *ViewerDisplayWidget::TryGizmoPress(const NodeValueRow &row, const QPointF &p)
{
for (auto it=gizmos_->GetGizmos().crbegin(); it!=gizmos_->GetGizmos().crend(); it++) {
NodeGizmo *gizmo = *it;
if (PointGizmo *point = dynamic_cast<PointGizmo*>(gizmo)) {
if (point->GetClickingRect(GenerateGizmoTransform()).contains(p)) {
return point;
}
} else if (PolygonGizmo *poly = dynamic_cast<PolygonGizmo*>(gizmo)) {
if (poly->GetPolygon().containsPoint(p, Qt::OddEvenFill)) {
return poly;
}
} else if (PathGizmo *path = dynamic_cast<PathGizmo*>(gizmo)) {
if (path->GetPath().contains(p)) {
return path;
}
} else if (ScreenGizmo *screen = dynamic_cast<ScreenGizmo*>(gizmo)) {
// NOTE: Perhaps this should limit to the actual visible screen space? We'll see.
return screen;
}
}
return nullptr;
}
void ViewerDisplayWidget::EmitColorAtCursor(QMouseEvent *e)
{
// Do this no matter what, emits signal to any pixel samplers
@@ -657,4 +792,14 @@ void ViewerDisplayWidget::UpdateFromQueue()
update();
}
void ViewerDisplayWidget::TextEditChanged()
{
ViewerTextEditor *editor = static_cast<ViewerTextEditor *>(sender());
TextGizmo *gizmo = reinterpret_cast<TextGizmo*>(editor->property("gizmo").value<quintptr>());
QString html = Html::DocToHtml(editor->document());
gizmo->UpdateInputHtml(html, GetGizmoTime());
}
}
+10 -3
View File
@@ -83,7 +83,7 @@ public:
* @brief Transform a point from viewer space to the buffer space.
* Multiplies by the inverted transform matrix to undo the scaling and translation.
*/
QPoint TransformViewerSpaceToBufferSpace(QPoint pos);
QPointF TransformViewerSpaceToBufferSpace(const QPointF &pos);
bool IsDeinterlacing() const
{
@@ -216,6 +216,8 @@ protected:
*/
virtual void mouseReleaseEvent(QMouseEvent* event) override;
virtual void mouseDoubleClickEvent(QMouseEvent *event) override;
virtual void dragEnterEvent(QDragEnterEvent* event) override;
virtual void dragLeaveEvent(QDragLeaveEvent* event) override;
@@ -255,6 +257,8 @@ private:
return TimeRange(node_time, node_time + gizmo_params_.frame_rate_as_time_base());
}
NodeGizmo *TryGizmoPress(const NodeValueRow &row, const QPointF &p);
/**
* @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL().
*/
@@ -302,10 +306,11 @@ private:
Node* gizmos_;
NodeValueRow gizmo_db_;
rational gizmo_drag_time_;
VideoParams gizmo_params_;
QPoint gizmo_start_drag_;
bool gizmo_click_;
QPoint gizmo_last_drag_;
NodeGizmo *current_gizmo_;
bool gizmo_drag_started_;
rational time_;
@@ -358,6 +363,8 @@ private slots:
void UpdateFromQueue();
void TextEditChanged();
};
}
+529
View File
@@ -0,0 +1,529 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2021 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "viewertexteditor.h"
#include <QAbstractTextDocumentLayout>
#include <QColorDialog>
#include <QKeyEvent>
#include <QHBoxLayout>
#include <QScrollBar>
#include "common/qtutils.h"
#include "ui/icons/icons.h"
#include "widget/colorbutton/colorbutton.h"
namespace olive {
#define super QTextEdit
ViewerTextEditor::ViewerTextEditor(double scale, QWidget *parent) :
super(parent),
transparent_clone_(nullptr),
block_update_toolbar_signal_(false),
listen_to_focus_events_(false)
{
// Ensure default text color is white
QPalette p = palette();
p.setColor(QPalette::Text, Qt::white);
setPalette(p);
document()->setDefaultStyleSheet(QStringLiteral("body { color: white; }"));
viewport()->setAutoFillBackground(false);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
connect(horizontalScrollBar(), &QScrollBar::rangeChanged, this, &ViewerTextEditor::LockScrollBarMaximumToZero);
connect(verticalScrollBar(), &QScrollBar::rangeChanged, this, &ViewerTextEditor::LockScrollBarMaximumToZero);
// Force DPI to the same one that we're using in the actual render
dpi_force_ = QImage(1, 1, QImage::Format_RGBA8888_Premultiplied);
const int dpm = 3780 * scale;
dpi_force_.setDotsPerMeterX(dpm);
dpi_force_.setDotsPerMeterY(dpm);
document()->documentLayout()->setPaintDevice(&dpi_force_);
connect(qApp, &QApplication::focusChanged, this, &ViewerTextEditor::FocusChanged);
connect(this, &QTextEdit::currentCharFormatChanged, this, &ViewerTextEditor::FormatChanged);
connect(document(), &QTextDocument::contentsChanged, this, &ViewerTextEditor::DocumentChanged, Qt::QueuedConnection);
}
void ViewerTextEditor::ConnectToolBar(ViewerTextEditorToolBar *toolbar)
{
connect(this, &ViewerTextEditor::destroyed, toolbar, &ViewerTextEditorToolBar::deleteLater);
connect(toolbar, &ViewerTextEditorToolBar::FamilyChanged, this, &ViewerTextEditor::SetFamily);
connect(toolbar, &ViewerTextEditorToolBar::SizeChanged, this, &ViewerTextEditor::setFontPointSize);
connect(toolbar, &ViewerTextEditorToolBar::StyleChanged, this, &ViewerTextEditor::SetStyle);
connect(toolbar, &ViewerTextEditorToolBar::UnderlineChanged, this, &ViewerTextEditor::setFontUnderline);
connect(toolbar, &ViewerTextEditorToolBar::StrikethroughChanged, this, &ViewerTextEditor::SetFontStrikethrough);
connect(toolbar, &ViewerTextEditorToolBar::ColorChanged, this, &ViewerTextEditor::setTextColor);
connect(toolbar, &ViewerTextEditorToolBar::SmallCapsChanged, this, &ViewerTextEditor::SetSmallCaps);
connect(toolbar, &ViewerTextEditorToolBar::StretchChanged, this, &ViewerTextEditor::SetFontStretch);
connect(toolbar, &ViewerTextEditorToolBar::KerningChanged, this, &ViewerTextEditor::SetFontKerning);
connect(toolbar, &ViewerTextEditorToolBar::LineHeightChanged, this, &ViewerTextEditor::SetLineHeight);
connect(toolbar, &ViewerTextEditorToolBar::AlignmentChanged, this, [this](Qt::Alignment a){
this->setAlignment(a);
// Ensure no buttons are checked that shouldn't be
static_cast<ViewerTextEditorToolBar*>(sender())->SetAlignment(a);
});
UpdateToolBar(toolbar, this->currentCharFormat(), this->textCursor().blockFormat(), this->alignment());
toolbars_.append(toolbar);
}
void ViewerTextEditor::keyPressEvent(QKeyEvent *event)
{
super::keyPressEvent(event);
if (event->key() == Qt::Key_Escape) {
deleteLater();
}
}
void ViewerTextEditor::paintEvent(QPaintEvent *e)
{
QPainter p(this->viewport());
QAbstractTextDocumentLayout::PaintContext ctx;
QRect r = e->rect();
if (r.isValid())
p.setClipRect(r, Qt::IntersectClip);
ctx.clip = r;
ctx.cursorPosition = this->textCursor().position();
if (this->textCursor().hasSelection()) {
QAbstractTextDocumentLayout::Selection selection;
selection.cursor = this->textCursor();
QPalette::ColorGroup cg = this->hasFocus() ? QPalette::Active : QPalette::Inactive;
QBrush b = ctx.palette.brush(cg, QPalette::Highlight);
QColor bc = b.color();
bc.setAlpha(128);
b.setColor(bc);
selection.format.setBackground(b);
QStyleOption opt;
opt.initFrom(this);
if (this->style()->styleHint(QStyle::SH_RichText_FullWidthSelection, &opt, this)) {
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
}
ctx.selections.append(selection);
}
transparent_clone_->documentLayout()->draw(&p, ctx);
}
void ViewerTextEditor::UpdateToolBar(ViewerTextEditorToolBar *toolbar, const QTextCharFormat &f, const QTextBlockFormat &b, Qt::Alignment alignment)
{
QFontDatabase fd;
QString family = f.fontFamily();
if (family.isEmpty()) {
family = qApp->font().family();
}
QString style = f.fontStyleName().toString();
QStringList styles = fd.styles(family);
if (!styles.isEmpty() && (style.isEmpty() || !styles.contains(style))) {
// There seems to be no better way to find the "regular" style outside of this heuristic.
// Feel free to add more if a font isn't working right.
style = QStringLiteral("Regular");
if (!styles.contains(style)) {
style = QStringLiteral("Normal");
}
if (!styles.contains(style)) {
style = QStringLiteral("R");
}
if (!styles.contains(style)) {
style = styles.first();
}
}
toolbar->SetFontFamily(family);
toolbar->SetFontSize(f.fontPointSize());
toolbar->SetStyle(style);
toolbar->SetUnderline(f.fontUnderline());
toolbar->SetStrikethrough(f.fontStrikeOut());
toolbar->SetAlignment(alignment);
toolbar->SetColor(f.foreground().color());
toolbar->SetSmallCaps(f.fontCapitalization() == QFont::SmallCaps);
toolbar->SetStretch(f.fontStretch() == 0 ? 100 : f.fontStretch());
toolbar->SetKerning(f.fontLetterSpacing() == 0.0 ? 100 : f.fontLetterSpacing());
toolbar->SetLineHeight(b.lineHeight() == 0.0 ? 100 : b.lineHeight());
}
void ViewerTextEditor::FocusChanged(QWidget *old, QWidget *now)
{
if (!listen_to_focus_events_) {
return;
}
QWidget *test = now;
if (!test) {
// Ignore null focuses because that could be one of the toolbar widgets simply losing focus
// and that would be undesirable to close the text editor from
return;
}
while (test) {
if (test == this
|| dynamic_cast<ViewerTextEditorToolBar*>(test)
|| dynamic_cast<SliderLadder*>(test)) {
return;
}
test = test->parentWidget();
}
// If we didn't return in the loop, the user must have focused on something else
deleteLater();
}
void ViewerTextEditor::FormatChanged(const QTextCharFormat &f)
{
if (!block_update_toolbar_signal_) {
foreach (ViewerTextEditorToolBar *toolbar, toolbars_) {
UpdateToolBar(toolbar, f, textCursor().blockFormat(), this->alignment());
}
}
}
void ViewerTextEditor::SetFamily(const QString &s)
{
ViewerTextEditorToolBar *toolbar = static_cast<ViewerTextEditorToolBar *>(sender());
QTextCharFormat f;
f.setFontFamily(s);
#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0)
f.setFontFamilies({s});
#endif
ApplyStyle(&f, s, toolbar->GetFontStyleName());
MergeCharFormat(f);
}
void ViewerTextEditor::SetStyle(const QString &s)
{
ViewerTextEditorToolBar *toolbar = static_cast<ViewerTextEditorToolBar *>(sender());
QTextCharFormat f;
ApplyStyle(&f, toolbar->GetFontFamily(), s);
MergeCharFormat(f);
}
void ViewerTextEditor::SetFontStrikethrough(bool e)
{
QTextCharFormat f;
f.setFontStrikeOut(e);
MergeCharFormat(f);
}
void ViewerTextEditor::SetSmallCaps(bool e)
{
QTextCharFormat f;
f.setFontCapitalization(e ? QFont::SmallCaps : QFont::MixedCase);
MergeCharFormat(f);
}
void ViewerTextEditor::SetFontStretch(int i)
{
QTextCharFormat f;
f.setFontStretch(i);
MergeCharFormat(f);
}
void ViewerTextEditor::SetFontKerning(qreal i)
{
QTextCharFormat f;
f.setFontLetterSpacing(i);
MergeCharFormat(f);
}
void ViewerTextEditor::MergeCharFormat(const QTextCharFormat &fmt)
{
// mergeCurrentCharFormat throws a currentCharFormatChanged signal that updates the toolbar,
// this can be undesirable if the user is currently typing a font
block_update_toolbar_signal_ = true;
mergeCurrentCharFormat(fmt);
block_update_toolbar_signal_ = false;
}
void ViewerTextEditor::ApplyStyle(QTextCharFormat *format, const QString &family, const QString &style)
{
// NOTE: Windows appears to require setting weight and italic manually, while macOS and Linux are
// perfectly fine with just the style name
QFontDatabase fd;
format->setFontWeight(fd.weight(family, style));
format->setFontItalic(fd.italic(family, style));
format->setFontStyleName(style);
}
void ViewerTextEditor::SetLineHeight(qreal i)
{
QTextBlockFormat f = this->textCursor().blockFormat();
f.setLineHeight(i, QTextBlockFormat::ProportionalHeight);
this->textCursor().setBlockFormat(f);
}
void ViewerTextEditor::LockScrollBarMaximumToZero()
{
static_cast<QScrollBar*>(sender())->setMaximum(0);
}
void ViewerTextEditor::DocumentChanged()
{
// HACK: We want to show the text cursor and selections without necessarily rendering the text,
// because the text is already being rendered underneath the gizmo (and rendering twice will
// alter the overall look of the text while editing). This is something that Qt does not
// support by default, and while it could be solved by subclassing QAbstractTextDocumentLayout,
// this seems like overkill since 99% of QTextDocumentLayout's (the subclass used by default)
// functionality and would need to be fully implemented ourselves. So instead, we clone the
// document, set all of its colors to rgba(0,0,0,0) to make them transparent, and draw that
// document instead. The result is cursor and selections being rendered without the text.
// While this isn't the fastest code, nor is it the cleanest solution, it definitely works.
delete transparent_clone_;
transparent_clone_ = document()->clone(this);
transparent_clone_->documentLayout()->setPaintDevice(&dpi_force_);
QTextCursor cursor(transparent_clone_);
cursor.select(QTextCursor::Document);
QTextCharFormat fmt;
fmt.setForeground(QColor(0, 0, 0, 0));
cursor.mergeCharFormat(fmt);
}
ViewerTextEditorToolBar::ViewerTextEditorToolBar(QWidget *parent) :
QWidget(parent, Qt::Tool | Qt::FramelessWindowHint),
painted_(false)
{
QVBoxLayout *outer_layout = new QVBoxLayout(this);
outer_layout->setSpacing(0);
QHBoxLayout *basic_layout = new QHBoxLayout();
outer_layout->addLayout(basic_layout);
int advanced_slider_width = QtUtils::QFontMetricsWidth(fontMetrics(), QStringLiteral("9999.9%"));
font_combo_ = new QFontComboBox();
connect(font_combo_, &QFontComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::UpdateFontStyleListAndEmitFamilyChanged);
basic_layout->addWidget(font_combo_);
font_sz_slider_ = new FloatSlider();
font_sz_slider_->SetMinimum(0.1);
font_sz_slider_->SetMaximum(9999.9);
font_sz_slider_->SetDecimalPlaces(1);
font_sz_slider_->SetAlignment(Qt::AlignCenter);
font_sz_slider_->setFixedWidth(advanced_slider_width);
connect(font_sz_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::SizeChanged);
font_sz_slider_->SetLadderElementCount(2);
basic_layout->addWidget(font_sz_slider_);
style_combo_ = new QComboBox();
connect(style_combo_, &QComboBox::currentTextChanged, this, &ViewerTextEditorToolBar::StyleChanged);
basic_layout->addWidget(style_combo_);
underline_btn_ = new QPushButton();
connect(underline_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::UnderlineChanged);
underline_btn_->setCheckable(true);
underline_btn_->setIcon(icon::TextUnderline);
basic_layout->addWidget(underline_btn_);
strikethrough_btn_ = new QPushButton();
connect(strikethrough_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::StrikethroughChanged);
strikethrough_btn_->setCheckable(true);
strikethrough_btn_->setIcon(icon::TextStrikethrough);
basic_layout->addWidget(strikethrough_btn_);
basic_layout->addWidget(QtUtils::CreateVerticalLine());
align_left_btn_ = new QPushButton();
align_left_btn_->setCheckable(true);
align_left_btn_->setIcon(icon::TextAlignLeft);
connect(align_left_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignLeft);});
basic_layout->addWidget(align_left_btn_);
align_center_btn_ = new QPushButton();
align_center_btn_->setCheckable(true);
align_center_btn_->setIcon(icon::TextAlignCenter);
connect(align_center_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignCenter);});
basic_layout->addWidget(align_center_btn_);
align_right_btn_ = new QPushButton();
align_right_btn_->setCheckable(true);
align_right_btn_->setIcon(icon::TextAlignRight);
connect(align_right_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignRight);});
basic_layout->addWidget(align_right_btn_);
align_justify_btn_ = new QPushButton();
align_justify_btn_->setCheckable(true);
align_justify_btn_->setIcon(icon::TextAlignJustify);
connect(align_justify_btn_, &QPushButton::clicked, this, [this]{emit AlignmentChanged(Qt::AlignJustify);});
basic_layout->addWidget(align_justify_btn_);
basic_layout->addWidget(QtUtils::CreateVerticalLine());
color_btn_ = new QPushButton();
color_btn_->setAutoFillBackground(true);
connect(color_btn_, &QPushButton::clicked, this, [this]{
QColor c = color_btn_->property("color").value<QColor>();
QColorDialog cd(c, this);
if (cd.exec() == QDialog::Accepted) {
c = cd.selectedColor();
SetColor(c);
emit ColorChanged(c);
}
});
basic_layout->addWidget(color_btn_);
basic_layout->addStretch();
QHBoxLayout *advanced_layout = new QHBoxLayout();
outer_layout->addLayout(advanced_layout);
advanced_layout->addWidget(new QLabel(tr("Stretch: "))); // FIXME: Procure icon
stretch_slider_ = new IntegerSlider();
stretch_slider_->SetMinimum(0);
stretch_slider_->SetDefaultValue(100);
stretch_slider_->setFixedWidth(advanced_slider_width);
stretch_slider_->SetFormat(tr("%1%"));
connect(stretch_slider_, &IntegerSlider::ValueChanged, this, &ViewerTextEditorToolBar::StretchChanged);
advanced_layout->addWidget(stretch_slider_);
advanced_layout->addWidget(new QLabel(tr("Kerning: "))); // FIXME: Procure icon
kerning_slider_ = new FloatSlider();
kerning_slider_->SetMinimum(0);
kerning_slider_->SetDefaultValue(100);
kerning_slider_->SetDecimalPlaces(1);
kerning_slider_->setFixedWidth(advanced_slider_width);
kerning_slider_->SetFormat(tr("%1%"));
connect(kerning_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::KerningChanged);
advanced_layout->addWidget(kerning_slider_);
advanced_layout->addWidget(new QLabel(tr("Line Height: "))); // FIXME: Procure icon
line_height_slider_ = new FloatSlider();
line_height_slider_->SetMinimum(0);
line_height_slider_->SetDefaultValue(100);
line_height_slider_->SetDecimalPlaces(1);
line_height_slider_->setFixedWidth(advanced_slider_width);
line_height_slider_->SetFormat(tr("%1%"));
connect(line_height_slider_, &FloatSlider::ValueChanged, this, &ViewerTextEditorToolBar::LineHeightChanged);
advanced_layout->addWidget(line_height_slider_);
small_caps_btn_ = new QPushButton();
small_caps_btn_->setIcon(icon::TextSmallCaps);
small_caps_btn_->setCheckable(true);
connect(small_caps_btn_, &QPushButton::clicked, this, &ViewerTextEditorToolBar::SmallCapsChanged);
advanced_layout->addWidget(small_caps_btn_);
advanced_layout->addStretch();
setAutoFillBackground(true);
resize(sizeHint());
}
void ViewerTextEditorToolBar::SetAlignment(Qt::Alignment a)
{
align_left_btn_->setChecked(a == Qt::AlignLeft);
align_center_btn_->setChecked(a == Qt::AlignCenter);
align_right_btn_->setChecked(a == Qt::AlignRight);
align_justify_btn_->setChecked(a == Qt::AlignJustify);
}
void ViewerTextEditorToolBar::SetColor(const QColor &c)
{
color_btn_->setProperty("color", c);
color_btn_->setStyleSheet(QStringLiteral("QPushButton { background: %1; }").arg(c.name()));
}
void ViewerTextEditorToolBar::closeEvent(QCloseEvent *event)
{
event->ignore();
}
void ViewerTextEditorToolBar::paintEvent(QPaintEvent *event)
{
if (!painted_) {
emit FirstPaint();
painted_ = true;
}
QWidget::paintEvent(event);
}
void ViewerTextEditorToolBar::UpdateFontStyleList(const QString &family)
{
QString temp = style_combo_->currentText();
style_combo_->blockSignals(true);
style_combo_->clear();
QStringList l = QFontDatabase().styles(family);
foreach (const QString &style, l) {
style_combo_->addItem(style);
}
style_combo_->setCurrentText(temp);
style_combo_->blockSignals(false);
}
void ViewerTextEditorToolBar::UpdateFontStyleListAndEmitFamilyChanged(const QString &family)
{
// Ensures correct ordering of commands
UpdateFontStyleList(family);
emit FamilyChanged(family);
}
void ViewerTextEditorToolBar::mousePressEvent(QMouseEvent *event)
{
QWidget::mousePressEvent(event);
if (event->button() == Qt::LeftButton) {
drag_anchor_ = event->pos();
}
}
void ViewerTextEditorToolBar::mouseMoveEvent(QMouseEvent *event)
{
QWidget::mouseMoveEvent(event);
if (event->buttons() & Qt::LeftButton) {
this->move(mapToParent(QPoint(event->pos() - drag_anchor_)));
}
}
void ViewerTextEditorToolBar::mouseReleaseEvent(QMouseEvent *event)
{
QWidget::mouseReleaseEvent(event);
}
}
+195
View File
@@ -0,0 +1,195 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2021 Olive Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef VIEWERTEXTEDITOR_H
#define VIEWERTEXTEDITOR_H
#include <QApplication>
#include <QDebug>
#include <QFontComboBox>
#include <QTextEdit>
#include <QPushButton>
#include "widget/slider/floatslider.h"
#include "widget/slider/integerslider.h"
namespace olive {
class ViewerTextEditorToolBar : public QWidget
{
Q_OBJECT
public:
ViewerTextEditorToolBar(QWidget *parent = nullptr);
QString GetFontFamily() const
{
return font_combo_->currentText();
}
QString GetFontStyleName() const
{
return style_combo_->currentText();
}
public slots:
void SetFontFamily(QString s)
{
font_combo_->blockSignals(true);
font_combo_->setCurrentFont(s);
UpdateFontStyleList(s);
font_combo_->blockSignals(false);
}
void SetStyle(QString style)
{
style_combo_->blockSignals(true);
style_combo_->setCurrentText(style);
style_combo_->blockSignals(false);
}
void SetFontSize(double d) { font_sz_slider_->SetValue(d); }
void SetUnderline(bool e) { underline_btn_->setChecked(e); }
void SetStrikethrough(bool e) { strikethrough_btn_->setChecked(e); }
void SetAlignment(Qt::Alignment a);
void SetColor(const QColor &c);
void SetSmallCaps(bool e) { small_caps_btn_->setChecked(e); }
void SetStretch(int i) { stretch_slider_->SetValue(i); }
void SetKerning(qreal i) { kerning_slider_->SetValue(i); }
void SetLineHeight(qreal i) { line_height_slider_->SetValue(i); }
signals:
void FamilyChanged(const QString &s);
void SizeChanged(double d);
void StyleChanged(const QString &s);
void UnderlineChanged(bool e);
void StrikethroughChanged(bool e);
void AlignmentChanged(Qt::Alignment alignment);
void ColorChanged(const QColor &c);
void SmallCapsChanged(bool e);
void StretchChanged(int i);
void KerningChanged(qreal i);
void LineHeightChanged(qreal i);
void FirstPaint();
protected:
virtual void mousePressEvent(QMouseEvent *event) override;
virtual void mouseMoveEvent(QMouseEvent *event) override;
virtual void mouseReleaseEvent(QMouseEvent *event) override;
virtual void closeEvent(QCloseEvent *event) override;
virtual void paintEvent(QPaintEvent *event) override;
private:
QPoint drag_anchor_;
QFontComboBox *font_combo_;
FloatSlider *font_sz_slider_;
QComboBox *style_combo_;
QPushButton *underline_btn_;
QPushButton *strikethrough_btn_;
QPushButton *align_left_btn_;
QPushButton *align_center_btn_;
QPushButton *align_right_btn_;
QPushButton *align_justify_btn_;
IntegerSlider *stretch_slider_;
FloatSlider *kerning_slider_;
FloatSlider *line_height_slider_;
QPushButton *small_caps_btn_;
QPushButton *color_btn_;
bool painted_;
private slots:
void UpdateFontStyleList(const QString &family);
void UpdateFontStyleListAndEmitFamilyChanged(const QString &family);
};
class ViewerTextEditor : public QTextEdit
{
Q_OBJECT
public:
ViewerTextEditor(double scale, QWidget *parent = nullptr);
void ConnectToolBar(ViewerTextEditorToolBar *toolbar);
void SetListenToFocusEvents(bool e) { listen_to_focus_events_ = e; }
protected:
virtual void keyPressEvent(QKeyEvent *event) override;
virtual void paintEvent(QPaintEvent *event) override;
private:
static void UpdateToolBar(ViewerTextEditorToolBar *toolbar, const QTextCharFormat &f, const QTextBlockFormat &b, Qt::Alignment alignment);
void MergeCharFormat(const QTextCharFormat &fmt);
void ApplyStyle(QTextCharFormat *format, const QString &family, const QString &style);
QVector<ViewerTextEditorToolBar *> toolbars_;
QImage dpi_force_;
QTextDocument *transparent_clone_;
bool block_update_toolbar_signal_;
bool listen_to_focus_events_;
private slots:
void FocusChanged(QWidget *old, QWidget *now);
void FormatChanged(const QTextCharFormat &f);
void SetFamily(const QString &s);
void SetStyle(const QString &s);
void SetFontStrikethrough(bool e);
void SetSmallCaps(bool e);
void SetFontStretch(int i);
void SetFontKerning(qreal i);
void SetLineHeight(qreal i);
void LockScrollBarMaximumToZero();
void DocumentChanged();
};
}
#endif // VIEWERTEXTEDITOR_H