nodes: moved matrix input on "video input" elsewhere
Rather than plugging a matrix into the video input node, the matrix is now multiplied by the video input using a math node. This is probably more sensible from a user perspective. This also means the renderer is tolerant of texture sizes that are not equal to the sequence size, however most nodes will downsample the texture to the sequence size (and if not, it will be downsampled once it is cached). Textures will still *always* be in reference space and the sequence's format. This seems like the best compromise between backend and frontend congruity.
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(block)
|
||||
add_subdirectory(distort)
|
||||
add_subdirectory(generator)
|
||||
add_subdirectory(input)
|
||||
add_subdirectory(math)
|
||||
add_subdirectory(output)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "block/clip/clip.h"
|
||||
#include "block/gap/gap.h"
|
||||
#include "block/transition/externaltransition.h"
|
||||
#include "distort/transform/transform.h"
|
||||
#include "generator/matrix/matrix.h"
|
||||
#include "input/media/video/video.h"
|
||||
#include "input/media/audio/audio.h"
|
||||
#include "input/time/timeinput.h"
|
||||
@@ -141,8 +141,8 @@ Node *NodeFactory::CreateInternal(const NodeFactory::InternalID &id)
|
||||
return new ClipBlock();
|
||||
case kGapBlock:
|
||||
return new GapBlock();
|
||||
case kTransformDistort:
|
||||
return new TransformDistort();
|
||||
case kMatrixGenerator:
|
||||
return new MatrixGenerator();
|
||||
case kVideoInput:
|
||||
return new VideoInput();
|
||||
case kAudioInput:
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public:
|
||||
kClipBlock,
|
||||
kGapBlock,
|
||||
kAudioInput,
|
||||
kTransformDistort,
|
||||
kMatrixGenerator,
|
||||
kVideoInput,
|
||||
kTrackOutput,
|
||||
kAudioVolume,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
add_subdirectory(transform)
|
||||
add_subdirectory(matrix)
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
node/distort/transform/transform.h
|
||||
node/distort/transform/transform.cpp
|
||||
node/generator/matrix/matrix.h
|
||||
node/generator/matrix/matrix.cpp
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -18,14 +18,14 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "transform.h"
|
||||
#include "matrix.h"
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector2D>
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
TransformDistort::TransformDistort()
|
||||
MatrixGenerator::MatrixGenerator()
|
||||
{
|
||||
position_input_ = new NodeInput("pos_in", NodeParam::kVec2);
|
||||
AddInput(position_input_);
|
||||
@@ -42,39 +42,39 @@ TransformDistort::TransformDistort()
|
||||
uniform_scale_input_ = new NodeInput("uniform_scale_in", NodeParam::kBoolean, true);
|
||||
uniform_scale_input_->set_is_keyframable(false);
|
||||
uniform_scale_input_->SetConnectable(false);
|
||||
connect(uniform_scale_input_, &NodeInput::ValueChanged, this, &TransformDistort::UniformScaleChanged);
|
||||
connect(uniform_scale_input_, &NodeInput::ValueChanged, this, &MatrixGenerator::UniformScaleChanged);
|
||||
AddInput(uniform_scale_input_);
|
||||
|
||||
anchor_input_ = new NodeInput("anchor_in", NodeParam::kVec2);
|
||||
AddInput(anchor_input_);
|
||||
}
|
||||
|
||||
Node *TransformDistort::copy() const
|
||||
Node *MatrixGenerator::copy() const
|
||||
{
|
||||
return new TransformDistort();
|
||||
return new MatrixGenerator();
|
||||
}
|
||||
|
||||
QString TransformDistort::Name() const
|
||||
QString MatrixGenerator::Name() const
|
||||
{
|
||||
return tr("Transform");
|
||||
return tr("Orthographic Matrix");
|
||||
}
|
||||
|
||||
QString TransformDistort::id() const
|
||||
QString MatrixGenerator::id() const
|
||||
{
|
||||
return QStringLiteral("org.olivevideoeditor.Olive.transform");
|
||||
}
|
||||
|
||||
QString TransformDistort::Category() const
|
||||
QString MatrixGenerator::Category() const
|
||||
{
|
||||
return tr("Distort");
|
||||
return tr("Generator");
|
||||
}
|
||||
|
||||
QString TransformDistort::Description() const
|
||||
QString MatrixGenerator::Description() const
|
||||
{
|
||||
return tr("Apply transformations to position, rotation, and scale.");
|
||||
return tr("Generate an orthographic matrix using position, rotation, and scale.");
|
||||
}
|
||||
|
||||
void TransformDistort::Retranslate()
|
||||
void MatrixGenerator::Retranslate()
|
||||
{
|
||||
position_input_->set_name(tr("Position"));
|
||||
rotation_input_->set_name(tr("Rotation"));
|
||||
@@ -83,7 +83,7 @@ void TransformDistort::Retranslate()
|
||||
anchor_input_->set_name(tr("Anchor Point"));
|
||||
}
|
||||
|
||||
NodeValueTable TransformDistort::Value(NodeValueDatabase &value) const
|
||||
NodeValueTable MatrixGenerator::Value(NodeValueDatabase &value) const
|
||||
{
|
||||
QMatrix4x4 mat;
|
||||
|
||||
@@ -110,7 +110,7 @@ NodeValueTable TransformDistort::Value(NodeValueDatabase &value) const
|
||||
return output;
|
||||
}
|
||||
|
||||
void TransformDistort::UniformScaleChanged()
|
||||
void MatrixGenerator::UniformScaleChanged()
|
||||
{
|
||||
scale_input_->set_property("disabley", uniform_scale_input_->get_standard_value().toBool());
|
||||
}
|
||||
@@ -18,18 +18,18 @@
|
||||
|
||||
***/
|
||||
|
||||
#ifndef TRANSFORMDISTORT_H
|
||||
#define TRANSFORMDISTORT_H
|
||||
#ifndef MATRIXGENERATOR_H
|
||||
#define MATRIXGENERATOR_H
|
||||
|
||||
#include "node/node.h"
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
class TransformDistort : public Node
|
||||
class MatrixGenerator : public Node
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
TransformDistort();
|
||||
MatrixGenerator();
|
||||
|
||||
virtual Node* copy() const override;
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
AudioInput::AudioInput()
|
||||
{
|
||||
}
|
||||
|
||||
Node *AudioInput::copy() const
|
||||
{
|
||||
return new AudioInput();
|
||||
|
||||
@@ -28,7 +28,7 @@ OLIVE_NAMESPACE_ENTER
|
||||
class AudioInput : public MediaInput
|
||||
{
|
||||
public:
|
||||
AudioInput();
|
||||
AudioInput() = default;
|
||||
|
||||
virtual Node* copy() const override;
|
||||
|
||||
@@ -36,7 +36,6 @@ public:
|
||||
virtual QString id() const override;
|
||||
virtual QString Description() const override;
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -67,7 +67,7 @@ NodeValueTable MediaInput::Value(NodeValueDatabase &value) const
|
||||
}
|
||||
|
||||
// Push buffer to the top of the stack
|
||||
NodeValue buffer = value[footage_input_].GetWithMeta(NodeParam::kSamples);
|
||||
NodeValue buffer = value[footage_input_].GetWithMeta(NodeParam::kBuffer);
|
||||
if (buffer.type() != NodeParam::kNone) {
|
||||
table.Push(buffer);
|
||||
}
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
|
||||
OLIVE_NAMESPACE_ENTER
|
||||
|
||||
VideoInput::VideoInput()
|
||||
{
|
||||
matrix_input_ = new NodeInput("matrix_in", NodeInput::kMatrix);
|
||||
AddInput(matrix_input_);
|
||||
}
|
||||
|
||||
Node *VideoInput::copy() const
|
||||
{
|
||||
return new VideoInput();
|
||||
@@ -57,31 +51,4 @@ QString VideoInput::Description() const
|
||||
return tr("Import a video footage stream.");
|
||||
}
|
||||
|
||||
NodeInput *VideoInput::matrix_input() const
|
||||
{
|
||||
return matrix_input_;
|
||||
}
|
||||
|
||||
Node::Capabilities VideoInput::GetCapabilities(const NodeValueDatabase &) const
|
||||
{
|
||||
return kShader;
|
||||
}
|
||||
|
||||
QString VideoInput::ShaderVertexCode(const NodeValueDatabase&) const
|
||||
{
|
||||
return ReadFileAsString(":/shaders/videoinput.vert");
|
||||
}
|
||||
|
||||
QString VideoInput::ShaderFragmentCode(const NodeValueDatabase&) const
|
||||
{
|
||||
return ReadFileAsString(":/shaders/videoinput.frag");
|
||||
}
|
||||
|
||||
void VideoInput::Retranslate()
|
||||
{
|
||||
MediaInput::Retranslate();
|
||||
|
||||
matrix_input_->set_name(tr("Transform"));
|
||||
}
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
@@ -31,7 +31,7 @@ OLIVE_NAMESPACE_ENTER
|
||||
class VideoInput : public MediaInput
|
||||
{
|
||||
public:
|
||||
VideoInput();
|
||||
VideoInput() = default;
|
||||
|
||||
virtual Node* copy() const override;
|
||||
|
||||
@@ -39,19 +39,6 @@ public:
|
||||
virtual QString id() const override;
|
||||
virtual QString Description() const override;
|
||||
|
||||
NodeInput* matrix_input() const;
|
||||
|
||||
virtual Capabilities GetCapabilities(const NodeValueDatabase&) const override;
|
||||
virtual QString ShaderVertexCode(const NodeValueDatabase&) const override;
|
||||
virtual QString ShaderFragmentCode(const NodeValueDatabase&) const override;
|
||||
|
||||
virtual void Retranslate() override;
|
||||
|
||||
protected:
|
||||
|
||||
private:
|
||||
NodeInput* matrix_input_;
|
||||
|
||||
};
|
||||
|
||||
OLIVE_NAMESPACE_EXIT
|
||||
|
||||
+103
-91
@@ -131,9 +131,9 @@ QString MathNode::ShaderFragmentCode(const NodeValueDatabase &input) const
|
||||
|
||||
// Override the operation for this operation since we multiply texture COORDS by the matrix rather than
|
||||
NodeParam* tex_in = (type_a == NodeParam::kTexture) ? param_a_in_ : param_b_in_;
|
||||
NodeParam* mat_in = (type_a == NodeParam::kTexture) ? param_b_in_ : param_a_in_;
|
||||
|
||||
operation = QStringLiteral("texture(%1, (vec4(ove_texcoord, 0.0, 1.0) * %2).xy)").arg(tex_in->id(), mat_in->id());
|
||||
// No-op frag shader (can we return QString() instead?)
|
||||
operation = QStringLiteral("texture(%1, ove_texcoord)").arg(tex_in->id());
|
||||
|
||||
} else {
|
||||
switch (GetOperation()) {
|
||||
@@ -176,6 +176,25 @@ QString MathNode::ShaderFragmentCode(const NodeValueDatabase &input) const
|
||||
operation);
|
||||
}
|
||||
|
||||
QString MathNode::ShaderVertexCode(const NodeValueDatabase &input) const
|
||||
{
|
||||
PairingCalculator calc(input[param_a_in_], input[param_b_in_]);
|
||||
|
||||
if (calc.GetMostLikelyPairing() == kPairTextureMatrix && GetOperation() == kOpMultiply) {
|
||||
|
||||
NodeParam::DataType type_a = calc.GetMostLikelyValueA().type();
|
||||
|
||||
// Override the operation for this operation since we multiply texture COORDS by the matrix rather than
|
||||
NodeParam* tex_in = (type_a == NodeParam::kTexture) ? param_a_in_ : param_b_in_;
|
||||
NodeParam* mat_in = (type_a == NodeParam::kTexture) ? param_b_in_ : param_a_in_;
|
||||
|
||||
return ReadFileAsString(":/shaders/matrix.vert").arg(mat_in->id(), tex_in->id());
|
||||
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
NodeValue MathNode::InputValueFromTable(NodeInput *input, NodeValueDatabase &db, bool take) const
|
||||
{
|
||||
if (input == param_a_in_ || input == param_b_in_) {
|
||||
@@ -382,6 +401,11 @@ MathNode::Operation MathNode::GetOperation() const
|
||||
return static_cast<Operation>(method_in_->get_standard_value().toInt());
|
||||
}
|
||||
|
||||
void MathNode::SetOperation(MathNode::Operation o)
|
||||
{
|
||||
method_in_->set_standard_value(o);
|
||||
}
|
||||
|
||||
QString MathNode::GetShaderUniformType(const NodeParam::DataType &type)
|
||||
{
|
||||
switch (type) {
|
||||
@@ -405,6 +429,81 @@ QString MathNode::GetShaderVariableCall(const QString &input_id, const NodeParam
|
||||
return input_id;
|
||||
}
|
||||
|
||||
QVector4D MathNode::RetrieveVector(const NodeValue &val)
|
||||
{
|
||||
// QVariant doesn't know that QVector*D can convert themselves so we do it here
|
||||
switch (val.type()) {
|
||||
case NodeParam::kVec2:
|
||||
return val.data().value<QVector2D>();
|
||||
case NodeParam::kVec3:
|
||||
return val.data().value<QVector3D>();
|
||||
case NodeParam::kVec4:
|
||||
default:
|
||||
return val.data().value<QVector4D>();
|
||||
}
|
||||
}
|
||||
|
||||
void MathNode::PushVector(NodeValueTable *output, NodeParam::DataType type, const QVector4D &vec)
|
||||
{
|
||||
switch (type) {
|
||||
case NodeParam::kVec2:
|
||||
output->Push(type, QVector2D(vec));
|
||||
break;
|
||||
case NodeParam::kVec3:
|
||||
output->Push(type, QVector3D(vec));
|
||||
break;
|
||||
case NodeParam::kVec4:
|
||||
output->Push(type, vec);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float MathNode::RetrieveNumber(const NodeValue &val)
|
||||
{
|
||||
if (val.type() == NodeParam::kRational) {
|
||||
return val.data().value<rational>().toDouble();
|
||||
} else {
|
||||
return val.data().toFloat();
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::PairingCalculator::PairingCalculator(const NodeValueTable &table_a, const NodeValueTable &table_b)
|
||||
{
|
||||
QVector<int> pair_likelihood_a = GetPairLikelihood(table_a);
|
||||
QVector<int> pair_likelihood_b = GetPairLikelihood(table_b);
|
||||
|
||||
int weight_a = qMax(0, table_b.Count() - table_a.Count());
|
||||
int weight_b = qMax(0, table_a.Count() - table_b.Count());
|
||||
|
||||
QVector<int> likelihoods(kPairCount);
|
||||
|
||||
for (int i=0;i<kPairCount;i++) {
|
||||
if (pair_likelihood_a.at(i) == -1 || pair_likelihood_b.at(i) == -1) {
|
||||
likelihoods.replace(i, -1);
|
||||
} else {
|
||||
likelihoods.replace(i, pair_likelihood_a.at(i) + weight_a + pair_likelihood_b.at(i) + weight_b);
|
||||
}
|
||||
}
|
||||
|
||||
most_likely_pairing_ = kPairNone;
|
||||
|
||||
for (int i=0;i<likelihoods.size();i++) {
|
||||
if (likelihoods.at(i) > -1) {
|
||||
if (most_likely_pairing_ == kPairNone
|
||||
|| likelihoods.at(i) > likelihoods.at(most_likely_pairing_)) {
|
||||
most_likely_pairing_ = static_cast<Pairing>(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (most_likely_pairing_ != kPairNone) {
|
||||
most_likely_value_a_ = table_a.At(pair_likelihood_a.at(most_likely_pairing_));
|
||||
most_likely_value_b_ = table_b.At(pair_likelihood_b.at(most_likely_pairing_));
|
||||
}
|
||||
}
|
||||
|
||||
QVector<int> MathNode::PairingCalculator::GetPairLikelihood(const NodeValueTable &table)
|
||||
{
|
||||
// FIXME: When we introduce a manual override, placing it here would be the least problematic
|
||||
@@ -448,88 +547,6 @@ QVector<int> MathNode::PairingCalculator::GetPairLikelihood(const NodeValueTable
|
||||
return likelihood;
|
||||
}
|
||||
|
||||
MathNode::Pairing MathNode::PairingCalculator::GetMostLikelyPairingInternal(const QVector<int> &a,
|
||||
const QVector<int> &b,
|
||||
const int& weight_a,
|
||||
const int& weight_b)
|
||||
{
|
||||
QVector<int> likelihoods(kPairCount);
|
||||
|
||||
for (int i=0;i<likelihoods.size();i++) {
|
||||
if (a.at(i) == -1 || b.at(i) == -1) {
|
||||
likelihoods.replace(i, -1);
|
||||
} else {
|
||||
likelihoods.replace(i, a.at(i) + weight_a + b.at(i) + weight_b);
|
||||
}
|
||||
}
|
||||
|
||||
Pairing pairing = kPairNone;
|
||||
|
||||
for (int i=0;i<likelihoods.size();i++) {
|
||||
if (likelihoods.at(i) > -1) {
|
||||
if (pairing == kPairNone
|
||||
|| likelihoods.at(i) > likelihoods.at(pairing)) {
|
||||
pairing = static_cast<Pairing>(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pairing;
|
||||
}
|
||||
|
||||
QVector4D MathNode::RetrieveVector(const NodeValue &val)
|
||||
{
|
||||
// QVariant doesn't know that QVector*D can convert themselves so we do it here
|
||||
switch (val.type()) {
|
||||
case NodeParam::kVec2:
|
||||
return val.data().value<QVector2D>();
|
||||
case NodeParam::kVec3:
|
||||
return val.data().value<QVector3D>();
|
||||
case NodeParam::kVec4:
|
||||
default:
|
||||
return val.data().value<QVector4D>();
|
||||
}
|
||||
}
|
||||
|
||||
void MathNode::PushVector(NodeValueTable *output, NodeParam::DataType type, const QVector4D &vec)
|
||||
{
|
||||
switch (type) {
|
||||
case NodeParam::kVec2:
|
||||
output->Push(type, QVector2D(vec));
|
||||
break;
|
||||
case NodeParam::kVec3:
|
||||
output->Push(type, QVector3D(vec));
|
||||
break;
|
||||
case NodeParam::kVec4:
|
||||
output->Push(type, vec);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float MathNode::RetrieveNumber(const NodeValue &val)
|
||||
{
|
||||
if (val.type() == NodeParam::kRational) {
|
||||
return val.data().value<rational>().toDouble();
|
||||
} else {
|
||||
return val.data().toFloat();
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::PairingCalculator::PairingCalculator(const NodeValueTable &table_a, const NodeValueTable &table_b) :
|
||||
table_a_(table_a),
|
||||
table_b_(table_b)
|
||||
{
|
||||
pair_likelihood_a_ = GetPairLikelihood(table_a_);
|
||||
pair_likelihood_b_ = GetPairLikelihood(table_b_);
|
||||
|
||||
most_likely_pairing_ = GetMostLikelyPairingInternal(pair_likelihood_a_,
|
||||
pair_likelihood_b_,
|
||||
qMax(0, table_b_.Count() - table_a_.Count()),
|
||||
qMax(0, table_a_.Count() - table_b_.Count()));
|
||||
}
|
||||
|
||||
bool MathNode::PairingCalculator::FoundMostLikelyPairing() const
|
||||
{
|
||||
return (most_likely_pairing_ > kPairNone && most_likely_pairing_ < kPairCount);
|
||||
@@ -542,17 +559,12 @@ MathNode::Pairing MathNode::PairingCalculator::GetMostLikelyPairing() const
|
||||
|
||||
const NodeValue &MathNode::PairingCalculator::GetMostLikelyValueA() const
|
||||
{
|
||||
return GetMostLikelyValue(table_a_, pair_likelihood_a_);
|
||||
return most_likely_value_a_;
|
||||
}
|
||||
|
||||
const NodeValue &MathNode::PairingCalculator::GetMostLikelyValueB() const
|
||||
{
|
||||
return GetMostLikelyValue(table_b_, pair_likelihood_b_);
|
||||
}
|
||||
|
||||
const NodeValue& MathNode::PairingCalculator::GetMostLikelyValue(const NodeValueTable &table, const QVector<int> &likelihood) const
|
||||
{
|
||||
return table.At(likelihood.at(most_likely_pairing_));
|
||||
return most_likely_value_b_;
|
||||
}
|
||||
|
||||
template<typename T, typename U>
|
||||
|
||||
@@ -42,6 +42,7 @@ public:
|
||||
virtual Capabilities GetCapabilities(const NodeValueDatabase&) const override;
|
||||
virtual QString ShaderID(const NodeValueDatabase&) const override;
|
||||
virtual QString ShaderFragmentCode(const NodeValueDatabase&) const override;
|
||||
virtual QString ShaderVertexCode(const NodeValueDatabase&input) const override;
|
||||
|
||||
virtual NodeValue InputValueFromTable(NodeInput* input, NodeValueDatabase &db, bool take) const override;
|
||||
|
||||
@@ -53,7 +54,6 @@ public:
|
||||
NodeInput* param_a_in() const;
|
||||
NodeInput* param_b_in() const;
|
||||
|
||||
private:
|
||||
enum Operation {
|
||||
kOpAdd,
|
||||
kOpSubtract,
|
||||
@@ -63,7 +63,9 @@ private:
|
||||
};
|
||||
|
||||
Operation GetOperation() const;
|
||||
void SetOperation(Operation o);
|
||||
|
||||
private:
|
||||
enum Pairing {
|
||||
kPairNone = -1,
|
||||
|
||||
@@ -96,21 +98,13 @@ private:
|
||||
const NodeValue& GetMostLikelyValueB() const;
|
||||
|
||||
private:
|
||||
static Pairing GetMostLikelyPairingInternal(const QVector<int> &a, const QVector<int> &b, const int &weight_a, const int &weight_b);
|
||||
|
||||
static QVector<int> GetPairLikelihood(const NodeValueTable& table);
|
||||
|
||||
const NodeValue &GetMostLikelyValue(const NodeValueTable& table, const QVector<int>& likelihood) const;
|
||||
|
||||
Pairing most_likely_pairing_;
|
||||
|
||||
const NodeValueTable& table_a_;
|
||||
NodeValue most_likely_value_a_;
|
||||
|
||||
const NodeValueTable& table_b_;
|
||||
|
||||
QVector<int> pair_likelihood_a_;
|
||||
|
||||
QVector<int> pair_likelihood_b_;
|
||||
NodeValue most_likely_value_b_;
|
||||
|
||||
};
|
||||
|
||||
|
||||
+114
-114
@@ -40,7 +40,6 @@ TrackOutput::TrackOutput() :
|
||||
AddInput(block_input_);
|
||||
connect(block_input_, &NodeInputArray::SubParamEdgeAdded, this, &TrackOutput::BlockConnected);
|
||||
connect(block_input_, &NodeInputArray::SubParamEdgeRemoved, this, &TrackOutput::BlockDisconnected);
|
||||
connect(block_input_, &NodeInputArray::SizeChanged, this, &TrackOutput::BlockListSizeChanged);
|
||||
|
||||
muted_input_ = new NodeInput("muted_in", NodeParam::kBoolean);
|
||||
muted_input_->set_is_keyframable(false);
|
||||
@@ -233,7 +232,7 @@ QList<Block *> TrackOutput::BlocksAtTimeRange(const TimeRange &range) const
|
||||
return list;
|
||||
}
|
||||
|
||||
const QVector<Block *> &TrackOutput::Blocks() const
|
||||
const QList<Block *> &TrackOutput::Blocks() const
|
||||
{
|
||||
return block_cache_;
|
||||
}
|
||||
@@ -265,16 +264,25 @@ void TrackOutput::InsertBlockAfter(Block *block, Block *before)
|
||||
|
||||
void TrackOutput::PrependBlock(Block *block)
|
||||
{
|
||||
InsertBlockAtIndex(block, 0);
|
||||
BlockInvalidateCache();
|
||||
|
||||
block_input_->Prepend();
|
||||
NodeParam::ConnectEdge(block->output(), block_input_->First());
|
||||
|
||||
UnblockInvalidateCache();
|
||||
|
||||
// Everything has shifted at this point
|
||||
InvalidateCache(TimeRange(0, track_length()), block_input_, block_input_);
|
||||
}
|
||||
|
||||
void TrackOutput::InsertBlockAtIndex(Block *block, int index)
|
||||
{
|
||||
BlockInvalidateCache();
|
||||
|
||||
block_input_->InsertAt(index);
|
||||
int insert_index = GetInputIndexFromCacheIndex(index);
|
||||
block_input_->InsertAt(insert_index);
|
||||
NodeParam::ConnectEdge(block->output(),
|
||||
block_input_->At(index));
|
||||
block_input_->At(insert_index));
|
||||
|
||||
UnblockInvalidateCache();
|
||||
|
||||
@@ -285,10 +293,8 @@ void TrackOutput::AppendBlock(Block *block)
|
||||
{
|
||||
BlockInvalidateCache();
|
||||
|
||||
int last_index = block_input_->GetSize();
|
||||
block_input_->Append();
|
||||
NodeParam::ConnectEdge(block->output(),
|
||||
block_input_->At(last_index));
|
||||
NodeParam::ConnectEdge(block->output(), block_input_->Last());
|
||||
|
||||
UnblockInvalidateCache();
|
||||
|
||||
@@ -312,9 +318,7 @@ void TrackOutput::RippleRemoveBlock(Block *block)
|
||||
|
||||
rational remove_in = block->in();
|
||||
|
||||
int index_of_block_to_remove = block_cache_.indexOf(block);
|
||||
|
||||
block_input_->RemoveAt(index_of_block_to_remove);
|
||||
block_input_->RemoveAt(GetInputIndexFromCacheIndex(block));
|
||||
|
||||
UnblockInvalidateCache();
|
||||
|
||||
@@ -325,7 +329,7 @@ void TrackOutput::ReplaceBlock(Block *old, Block *replace)
|
||||
{
|
||||
BlockInvalidateCache();
|
||||
|
||||
int index_of_old_block = block_cache_.indexOf(old);
|
||||
int index_of_old_block = GetInputIndexFromCacheIndex(old);
|
||||
|
||||
NodeParam::DisconnectEdge(old->output(),
|
||||
block_input_->At(index_of_old_block));
|
||||
@@ -432,108 +436,107 @@ void TrackOutput::SetLocked(bool e)
|
||||
|
||||
void TrackOutput::UpdateInOutFrom(int index)
|
||||
{
|
||||
Q_ASSERT(index >= 0);
|
||||
Q_ASSERT(index < block_cache_.size());
|
||||
|
||||
rational new_track_length;
|
||||
|
||||
// Find block just before this one to find the last out point
|
||||
for (int i=index-1;i>=0;i--) {
|
||||
rational last_out = (index == 0) ? 0 : block_cache_.at(index - 1)->out();
|
||||
|
||||
// Iterate through all blocks updating their in/outs
|
||||
for (int i=index; i<block_cache_.size(); i++) {
|
||||
Block* b = block_cache_.at(i);
|
||||
|
||||
if (b) {
|
||||
new_track_length = b->out();
|
||||
break;
|
||||
}
|
||||
}
|
||||
b->set_in(last_out);
|
||||
|
||||
for (int i=index;i<block_cache_.size();i++) {
|
||||
Block* b = block_cache_.at(i);
|
||||
last_out += b->length();
|
||||
|
||||
if (b) {
|
||||
// Set in
|
||||
b->set_in(new_track_length);
|
||||
b->set_out(last_out);
|
||||
|
||||
// Set out
|
||||
new_track_length += b->length();
|
||||
b->set_out(new_track_length);
|
||||
|
||||
emit b->Refreshed();
|
||||
}
|
||||
emit b->Refreshed();
|
||||
}
|
||||
|
||||
// Update track length
|
||||
if (new_track_length != track_length_) {
|
||||
if (last_out != track_length_) {
|
||||
rational old_track_length = track_length_;
|
||||
|
||||
track_length_ = new_track_length;
|
||||
track_length_ = last_out;
|
||||
emit TrackLengthChanged();
|
||||
|
||||
InvalidateCache(TimeRange(qMin(old_track_length, new_track_length), qMax(old_track_length, new_track_length)),
|
||||
InvalidateCache(TimeRange(qMin(old_track_length, last_out), qMax(old_track_length, last_out)),
|
||||
block_input_,
|
||||
block_input_);
|
||||
}
|
||||
}
|
||||
|
||||
void TrackOutput::UpdatePreviousAndNextOfIndex(int index)
|
||||
int TrackOutput::GetInputIndexFromCacheIndex(int cache_index)
|
||||
{
|
||||
Block* ref = block_cache_.at(index);
|
||||
return GetInputIndexFromCacheIndex(block_cache_.at(cache_index));
|
||||
}
|
||||
|
||||
Block* previous = nullptr;
|
||||
Block* next = nullptr;
|
||||
|
||||
// Find previous
|
||||
for (int i=index-1;i>=0;i--) {
|
||||
previous = block_cache_.at(i);
|
||||
|
||||
if (previous) {
|
||||
break;
|
||||
int TrackOutput::GetInputIndexFromCacheIndex(Block *block)
|
||||
{
|
||||
for (int i=0; i<block_input_->GetSize(); i++) {
|
||||
if (block_input_->At(i)->get_connected_node() == block) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// Find next
|
||||
for (int i=index+1;i<block_cache_.size();i++) {
|
||||
next = block_cache_.at(i);
|
||||
|
||||
if (next) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ref) {
|
||||
// Link blocks together
|
||||
ref->set_previous(previous);
|
||||
ref->set_next(next);
|
||||
|
||||
if (previous)
|
||||
previous->set_next(ref);
|
||||
|
||||
if (next)
|
||||
next->set_previous(ref);
|
||||
} else {
|
||||
// Link previous and next together
|
||||
if (previous)
|
||||
previous->set_next(next);
|
||||
|
||||
if (next)
|
||||
next->set_previous(previous);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TrackOutput::BlockConnected(NodeEdgePtr edge)
|
||||
{
|
||||
int block_index = block_input_->IndexOfSubParameter(edge->input());
|
||||
|
||||
Q_ASSERT(block_index >= 0);
|
||||
|
||||
// Determine what node was just connected
|
||||
Node* connected_node = edge->output()->parentNode();
|
||||
Block* connected_block = connected_node->IsBlock() ? static_cast<Block*>(connected_node) : nullptr;
|
||||
block_cache_.replace(block_index, connected_block);
|
||||
UpdatePreviousAndNextOfIndex(block_index);
|
||||
UpdateInOutFrom(block_index);
|
||||
|
||||
if (connected_block) {
|
||||
connect(connected_block, SIGNAL(LengthChanged(const rational&)), this, SLOT(BlockLengthChanged()));
|
||||
// If this node is a block, we can do something with it
|
||||
if (connected_node->IsBlock()) {
|
||||
Block* connected_block = static_cast<Block*>(connected_node);
|
||||
|
||||
// See where this input falls in our internal "block cache"
|
||||
Block* next = nullptr;
|
||||
for (int i=block_input_->IndexOfSubParameter(edge->input())+1; i<block_input_->GetSize(); i++) {
|
||||
Node* that_node = block_input_->At(i)->get_connected_node();
|
||||
|
||||
// If we find a block, this is the block that will follow the one just connected
|
||||
if (that_node && that_node->IsBlock()) {
|
||||
next = static_cast<Block*>(that_node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int real_block_index;
|
||||
|
||||
// Either insert or append depending on if we found a "next" block
|
||||
if (next) {
|
||||
// Insert block before this next block
|
||||
real_block_index = block_cache_.indexOf(next);
|
||||
block_cache_.insert(real_block_index, connected_block);
|
||||
|
||||
// Update values with next
|
||||
next->set_previous(connected_block);
|
||||
connected_block->set_next(next);
|
||||
} else {
|
||||
// No "next", this block must come at the end
|
||||
real_block_index = block_cache_.size();
|
||||
block_cache_.append(connected_block);
|
||||
|
||||
// Update next value
|
||||
connected_block->set_next(nullptr);
|
||||
}
|
||||
|
||||
// For all blocks after the block we inserted (including it), update the "previous" and "next"
|
||||
// fields as well as the in/out values
|
||||
if (real_block_index == 0) {
|
||||
connected_block->set_previous(nullptr);
|
||||
} else {
|
||||
Block* prev = block_cache_.at(real_block_index - 1);
|
||||
|
||||
connected_block->set_previous(prev);
|
||||
prev->set_next(connected_block);
|
||||
}
|
||||
|
||||
UpdateInOutFrom(real_block_index);
|
||||
|
||||
// Make connections to this block
|
||||
connect(connected_block, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);
|
||||
|
||||
emit BlockAdded(connected_block);
|
||||
}
|
||||
@@ -541,46 +544,43 @@ void TrackOutput::BlockConnected(NodeEdgePtr edge)
|
||||
|
||||
void TrackOutput::BlockDisconnected(NodeEdgePtr edge)
|
||||
{
|
||||
int block_index = block_input_->IndexOfSubParameter(edge->input());
|
||||
|
||||
Q_ASSERT(block_index >= 0);
|
||||
|
||||
block_cache_.replace(block_index, nullptr);
|
||||
UpdatePreviousAndNextOfIndex(block_index);
|
||||
UpdateInOutFrom(block_index);
|
||||
|
||||
// See what kind of node was just connected
|
||||
Node* connected_node = edge->output()->parentNode();
|
||||
Block* connected_block = connected_node->IsBlock() ? static_cast<Block*>(connected_node) : nullptr;
|
||||
if (connected_block) {
|
||||
disconnect(connected_block, SIGNAL(LengthChanged(const rational&)), this, SLOT(BlockLengthChanged()));
|
||||
|
||||
// Update previous and next references
|
||||
// If this was a block, we would have put it in our block cache in BlockConnected()
|
||||
if (connected_node->IsBlock()) {
|
||||
Block* connected_block = static_cast<Block*>(connected_node);
|
||||
|
||||
// Determine what index this block was in our cache and remove it
|
||||
int index_of_block = block_cache_.indexOf(connected_block);
|
||||
block_cache_.removeAt(index_of_block);
|
||||
|
||||
// If there were blocks following this one, update their ins/outs
|
||||
if (index_of_block < block_cache_.size()) {
|
||||
UpdateInOutFrom(index_of_block);
|
||||
}
|
||||
|
||||
// Join the previous and next blocks together
|
||||
if (connected_block->previous()) {
|
||||
connected_block->previous()->set_next(connected_block->next());
|
||||
}
|
||||
|
||||
if (connected_block->next()) {
|
||||
connected_block->next()->set_previous(connected_block->previous());
|
||||
}
|
||||
|
||||
disconnect(connected_block, &Block::LengthChanged, this, &TrackOutput::BlockLengthChanged);
|
||||
|
||||
emit BlockRemoved(connected_block);
|
||||
}
|
||||
}
|
||||
|
||||
void TrackOutput::BlockListSizeChanged(int size)
|
||||
{
|
||||
int old_size = block_cache_.size();
|
||||
|
||||
block_cache_.resize(size);
|
||||
|
||||
// Fill new slots with nullptr
|
||||
for (int i=old_size;i<size;i++) {
|
||||
block_cache_.replace(i, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void TrackOutput::BlockLengthChanged()
|
||||
{
|
||||
// Assumes sender is a Block
|
||||
Block* b = static_cast<Block*>(sender());
|
||||
|
||||
int index = block_cache_.indexOf(b);
|
||||
|
||||
Q_ASSERT(index >= 0);
|
||||
|
||||
UpdateInOutFrom(index);
|
||||
UpdateInOutFrom(block_cache_.indexOf(b));
|
||||
}
|
||||
|
||||
void TrackOutput::MutedInputValueChanged()
|
||||
|
||||
@@ -82,7 +82,7 @@ public:
|
||||
Block* BlockAtTime(const rational& time) const;
|
||||
QList<Block*> BlocksAtTimeRange(const TimeRange& range) const;
|
||||
|
||||
const QVector<Block*>& Blocks() const;
|
||||
const QList<Block *> &Blocks() const;
|
||||
|
||||
virtual void InvalidateCache(const TimeRange& range, NodeInput* from, NodeInput *source) override;
|
||||
|
||||
@@ -195,9 +195,10 @@ protected:
|
||||
private:
|
||||
void UpdateInOutFrom(int index);
|
||||
|
||||
void UpdatePreviousAndNextOfIndex(int index);
|
||||
int GetInputIndexFromCacheIndex(int cache_index);
|
||||
int GetInputIndexFromCacheIndex(Block* block);
|
||||
|
||||
QVector<Block*> block_cache_;
|
||||
QList<Block*> block_cache_;
|
||||
|
||||
NodeInputArray* block_input_;
|
||||
|
||||
@@ -222,8 +223,6 @@ private slots:
|
||||
|
||||
void BlockDisconnected(NodeEdgePtr edge);
|
||||
|
||||
void BlockListSizeChanged(int size);
|
||||
|
||||
void BlockLengthChanged();
|
||||
|
||||
void MutedInputValueChanged();
|
||||
|
||||
@@ -161,7 +161,7 @@ void TrackList::TrackConnected(NodeEdgePtr edge)
|
||||
for (int i=track_index+1; i<track_input_->GetSize(); i++) {
|
||||
Node* that_track = track_input_->At(i)->get_connected_node();
|
||||
|
||||
if (that_track->IsTrack()) {
|
||||
if (that_track && that_track->IsTrack()) {
|
||||
next = static_cast<TrackOutput*>(that_track);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -212,8 +212,8 @@ void OpenGLProxy::RunNodeAccelerated(const Node *node, const TimeRange &range, N
|
||||
if (!shader) {
|
||||
// Since we have shader code, compile it now
|
||||
|
||||
QString frag_code = node->ShaderFragmentCode(input_params);
|
||||
QString vert_code = node->ShaderVertexCode(input_params);
|
||||
QString frag_code = node->ShaderFragmentCode(input_params);
|
||||
|
||||
if (frag_code.isEmpty()) {
|
||||
frag_code = OpenGLShader::CodeDefaultFragment();
|
||||
@@ -258,7 +258,7 @@ void OpenGLProxy::RunNodeAccelerated(const Node *node, const TimeRange &range, N
|
||||
NodeInput* input = static_cast<NodeInput*>(param);
|
||||
|
||||
// Get value from database at this input
|
||||
NodeValue meta_value = node->InputValueFromTable(input, input_params, true);
|
||||
NodeValue meta_value = node->InputValueFromTable(input, input_params, false);
|
||||
const QVariant& value = meta_value.data();
|
||||
|
||||
NodeParam::DataType data_type;
|
||||
|
||||
@@ -421,7 +421,7 @@ Node* RenderBackend::CopyNodeConnections(Node* src_node)
|
||||
|
||||
void RenderBackend::CopyNodeMakeConnection(NodeInput* src_input, NodeInput* dst_input)
|
||||
{
|
||||
qDebug() << "Copying input" << src_input->id() << "from" << src_input->parentNode()->id();
|
||||
//qDebug() << "Copying input" << src_input->id() << "from" << src_input->parentNode()->id();
|
||||
|
||||
if (src_input->IsConnected()) {
|
||||
Node* dst_node = CopyNodeConnections(src_input->get_connected_node());
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#version 150
|
||||
|
||||
uniform mat4 matrix_in;
|
||||
uniform mat4 %1;
|
||||
|
||||
uniform vec2 footage_in_resolution;
|
||||
uniform vec2 %2_resolution;
|
||||
uniform vec2 ove_resolution;
|
||||
|
||||
in vec4 a_position;
|
||||
@@ -27,10 +27,10 @@ void main() {
|
||||
transform *= scale_mat4(vec3(1.0 / ove_resolution, 1.0));
|
||||
|
||||
// Multiply by received matrix
|
||||
transform *= matrix_in;
|
||||
transform *= %1;
|
||||
|
||||
// Scale back out to footage size
|
||||
transform *= scale_mat4(vec3(footage_in_resolution, 1.0));
|
||||
transform *= scale_mat4(vec3(%2_resolution, 1.0));
|
||||
|
||||
gl_Position = transform * a_position;
|
||||
ove_texcoord = a_texcoord;
|
||||
@@ -17,7 +17,6 @@
|
||||
<file>solid.xml</file>
|
||||
<file>stroke.frag</file>
|
||||
<file>stroke.xml</file>
|
||||
<file>videoinput.frag</file>
|
||||
<file>videoinput.vert</file>
|
||||
<file>matrix.vert</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#version 150
|
||||
|
||||
uniform sampler2D footage_in;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main(void) {
|
||||
fragColor = texture(footage_in, ove_texcoord);
|
||||
}
|
||||
@@ -42,6 +42,8 @@ NodeParamViewConnectedLabel::NodeParamViewConnectedLabel(NodeInput *input, QWidg
|
||||
connect(connected_to_lbl_, &ClickableLabel::MouseClicked, this, &NodeParamViewConnectedLabel::ConnectionClicked);
|
||||
layout->addWidget(connected_to_lbl_);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
// Set up "link" font
|
||||
QFont link_font = connected_to_lbl_->font();
|
||||
link_font.setUnderline(true);
|
||||
|
||||
@@ -30,9 +30,10 @@
|
||||
#include "core.h"
|
||||
#include "dialog/sequence/sequence.h"
|
||||
#include "node/audio/volume/volume.h"
|
||||
#include "node/distort/transform/transform.h"
|
||||
#include "node/generator/matrix/matrix.h"
|
||||
#include "node/input/media/audio/audio.h"
|
||||
#include "node/input/media/video/video.h"
|
||||
#include "node/math/math/math.h"
|
||||
#include "project/item/sequence/sequence.h"
|
||||
#include "widget/nodeview/nodeviewundo.h"
|
||||
#include "window/mainwindow/mainwindow.h"
|
||||
@@ -397,15 +398,17 @@ void TimelineWidget::ImportTool::DropGhosts(bool insert)
|
||||
VideoInput* video_input = new VideoInput();
|
||||
video_input->SetFootage(footage_stream);
|
||||
new NodeAddCommand(dst_graph, video_input, command);
|
||||
new NodeEdgeAddCommand(video_input->output(), clip->texture_input(), command);
|
||||
|
||||
TransformDistort* transform = new TransformDistort();
|
||||
new NodeAddCommand(dst_graph, transform, command);
|
||||
new NodeEdgeAddCommand(transform->output(), video_input->matrix_input(), command);
|
||||
MatrixGenerator* matrix = new MatrixGenerator();
|
||||
new NodeAddCommand(dst_graph, matrix, command);
|
||||
|
||||
//OpacityNode* opacity = new OpacityNode();
|
||||
//NodeParam::ConnectEdge(opacity->texture_output(), clip->texture_input());
|
||||
//NodeParam::ConnectEdge(media->texture_output(), opacity->texture_input());
|
||||
MathNode* multiply = new MathNode();
|
||||
multiply->SetOperation(MathNode::kOpMultiply);
|
||||
new NodeAddCommand(dst_graph, multiply, command);
|
||||
|
||||
new NodeEdgeAddCommand(video_input->output(), multiply->param_a_in(), command);
|
||||
new NodeEdgeAddCommand(matrix->output(), multiply->param_b_in(), command);
|
||||
new NodeEdgeAddCommand(multiply->output(), clip->texture_input(), command);
|
||||
break;
|
||||
}
|
||||
case Stream::kAudio:
|
||||
|
||||
@@ -613,33 +613,31 @@ void TrackCleanGapsCommand::redo_internal()
|
||||
TrackOutput* track = track_list_->GetTrackAt(track_index_);
|
||||
|
||||
foreach (Block* b, track->Blocks()) {
|
||||
if (b) {
|
||||
if (b->type() == Block::kGap) {
|
||||
if (on_gap) {
|
||||
consecutive_gaps.append(static_cast<GapBlock*>(b));
|
||||
} else {
|
||||
on_gap = static_cast<GapBlock*>(b);
|
||||
}
|
||||
} else if (on_gap) {
|
||||
merged_gaps_.append({on_gap, on_gap->length(), consecutive_gaps});
|
||||
|
||||
// Remove each gap and add to the length of the merged
|
||||
// We can block the IC signal because merging gaps won't actually change anything
|
||||
track->BlockInvalidateCache();
|
||||
rational new_gap_length = on_gap->length();
|
||||
foreach (GapBlock* gap, consecutive_gaps) {
|
||||
track->RippleRemoveBlock(gap);
|
||||
static_cast<NodeGraph*>(track->parent())->TakeNode(gap, &memory_manager_);
|
||||
|
||||
new_gap_length += gap->length();
|
||||
}
|
||||
on_gap->set_length_and_media_out(new_gap_length);
|
||||
track->UnblockInvalidateCache();
|
||||
|
||||
// Reset state
|
||||
on_gap = nullptr;
|
||||
consecutive_gaps.clear();
|
||||
if (b->type() == Block::kGap) {
|
||||
if (on_gap) {
|
||||
consecutive_gaps.append(static_cast<GapBlock*>(b));
|
||||
} else {
|
||||
on_gap = static_cast<GapBlock*>(b);
|
||||
}
|
||||
} else if (on_gap) {
|
||||
merged_gaps_.append({on_gap, on_gap->length(), consecutive_gaps});
|
||||
|
||||
// Remove each gap and add to the length of the merged
|
||||
// We can block the IC signal because merging gaps won't actually change anything
|
||||
track->BlockInvalidateCache();
|
||||
rational new_gap_length = on_gap->length();
|
||||
foreach (GapBlock* gap, consecutive_gaps) {
|
||||
track->RippleRemoveBlock(gap);
|
||||
static_cast<NodeGraph*>(track->parent())->TakeNode(gap, &memory_manager_);
|
||||
|
||||
new_gap_length += gap->length();
|
||||
}
|
||||
on_gap->set_length_and_media_out(new_gap_length);
|
||||
track->UnblockInvalidateCache();
|
||||
|
||||
// Reset state
|
||||
on_gap = nullptr;
|
||||
consecutive_gaps.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,11 @@ void TimelineViewBlockItem::paint(QPainter *painter, const QStyleOptionGraphicsI
|
||||
} else {
|
||||
painter->setPen(Qt::lightGray);
|
||||
}
|
||||
painter->drawText(rect(), static_cast<int>(Qt::AlignLeft | Qt::AlignTop), block_->block_name());
|
||||
|
||||
int text_top = TrackOutput::GetTrackHeightMinimum() / 2 - painter->fontMetrics().height() / 2;
|
||||
QRectF text_rect = rect();
|
||||
text_rect.adjust(0, text_top, 0, 0);
|
||||
painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignTop, block_->block_name());
|
||||
|
||||
// Linked clips are underlined
|
||||
if (block_->HasLinks()) {
|
||||
|
||||
Reference in New Issue
Block a user