/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 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 .
***/
#include "viewerdisplay.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "common/define.h"
#include "common/functiontimer.h"
#include "common/html.h"
#include "common/qtutils.h"
#include "config/config.h"
#include "core.h"
#include "node/block/subtitle/subtitle.h"
#include "node/gizmo/path.h"
#include "node/gizmo/point.h"
#include "node/gizmo/polygon.h"
#include "node/gizmo/screen.h"
#include "viewertexteditor.h"
#include "window/mainwindow/mainwindow.h"
namespace olive {
#define super ManagedDisplayWidget
ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) :
super(parent),
deinterlace_texture_(nullptr),
signal_cursor_color_(false),
gizmos_(nullptr),
current_gizmo_(nullptr),
gizmo_drag_started_(false),
show_subtitles_(true),
subtitle_tracks_(nullptr),
hand_dragging_(false),
deinterlace_(false),
show_fps_(false),
frames_skipped_(0),
show_widget_background_(false),
playback_speed_(0),
push_mode_(kPushNull),
add_band_(nullptr),
queue_starved_(false)
{
connect(Core::instance(), &Core::ToolChanged, this, &ViewerDisplayWidget::ToolChanged);
connect(this, &ViewerDisplayWidget::InnerWidgetMouseMove, this, &ViewerDisplayWidget::EmitColorAtCursor);
// Initializes cursor based on tool
UpdateCursor();
const int kFrameRateAverageCount = 8;
frame_rate_averages_.resize(kFrameRateAverageCount);
}
void ViewerDisplayWidget::SetMatrixTranslate(const QMatrix4x4 &mat)
{
translate_matrix_ = mat;
UpdateMatrix();
}
void ViewerDisplayWidget::SetMatrixZoom(const QMatrix4x4 &mat)
{
scale_matrix_ = mat;
UpdateMatrix();
}
void ViewerDisplayWidget::SetMatrixCrop(const QMatrix4x4 &mat)
{
crop_matrix_ = mat;
update();
}
void ViewerDisplayWidget::UpdateCursor()
{
if (Core::instance()->tool() == Tool::kHand) {
setCursor(Qt::OpenHandCursor);
} else if (Core::instance()->tool() == Tool::kAdd) {
setCursor(Qt::CrossCursor);
} else {
unsetCursor();
}
}
void ViewerDisplayWidget::SetSignalCursorColorEnabled(bool e)
{
signal_cursor_color_ = e;
inner_widget()->setMouseTracking(e);
}
void ViewerDisplayWidget::SetImage(const QVariant &buffer)
{
load_frame_ = buffer;
if (load_frame_.isNull()) {
push_mode_ = kPushNull;
} else {
push_mode_ = kPushFrame;
}
update();
}
void ViewerDisplayWidget::SetBlank()
{
push_mode_ = kPushBlank;
update();
}
void ViewerDisplayWidget::ToolChanged()
{
UpdateCursor();
}
void ViewerDisplayWidget::SetDeinterlacing(bool e)
{
deinterlace_ = e;
if (!deinterlace_) {
if (!deinterlace_shader_.isNull()) {
renderer()->DestroyNativeShader(deinterlace_shader_);
deinterlace_shader_.clear();
}
deinterlace_texture_ = nullptr;
}
update();
}
const ViewerSafeMarginInfo &ViewerDisplayWidget::GetSafeMargin() const
{
return safe_margin_;
}
void ViewerDisplayWidget::SetSafeMargins(const ViewerSafeMarginInfo &safe_margin)
{
if (safe_margin_ != safe_margin) {
safe_margin_ = safe_margin;
update();
}
}
void ViewerDisplayWidget::SetGizmos(Node *node)
{
if (gizmos_ != node) {
gizmos_ = node;
update();
}
}
void ViewerDisplayWidget::SetVideoParams(const VideoParams ¶ms)
{
gizmo_params_ = params;
if (gizmos_) {
update();
}
}
void ViewerDisplayWidget::SetTime(const rational &time)
{
time_ = time;
if (gizmos_) {
update();
}
}
void ViewerDisplayWidget::SetSubtitleTracks(Sequence *list)
{
if (subtitle_tracks_) {
disconnect(subtitle_tracks_, &Sequence::SubtitlesChanged, this, &ViewerDisplayWidget::SubtitlesChanged);
}
subtitle_tracks_ = list;
if (subtitle_tracks_) {
connect(subtitle_tracks_, &Sequence::SubtitlesChanged, this, &ViewerDisplayWidget::SubtitlesChanged);
}
update();
}
QPointF ViewerDisplayWidget::TransformViewerSpaceToBufferSpace(const QPointF &pos)
{
/*
* Inversion will only fail if the viewer has been scaled by 0 in any direction
* which I think should never happen.
*/
return pos * GenerateDisplayTransform().inverted();
}
void ViewerDisplayWidget::ResetFPSTimer()
{
fps_timer_start_ = QDateTime::currentMSecsSinceEpoch();
fps_timer_update_count_ = 0;
frames_skipped_ = 0;
frame_rate_average_count_ = 0;
Core::instance()->ClearStatusBarMessage();
}
void ViewerDisplayWidget::IncrementSkippedFrames()
{
frames_skipped_++;
Core::instance()->ShowStatusBarMessage(tr("%n skipped frame(s) detected during playback", nullptr, frames_skipped_), 10000);
}
void ViewerDisplayWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton && Core::instance()->tool() == Tool::kAdd
&& (Core::instance()->GetSelectedAddableObject() == Tool::kAddableShape || Core::instance()->GetSelectedAddableObject() == Tool::kAddableTitle)) {
add_band_start_ = event->pos();
add_band_ = new QRubberBand(QRubberBand::Rectangle, this);
add_band_->setGeometry(QRect(add_band_start_, add_band_start_));
add_band_->show();
} else if (event->button() == Qt::LeftButton && gizmos_
&& (gizmo_last_draw_transform_inverted_ = gizmo_last_draw_transform_.inverted(),
current_gizmo_ = TryGizmoPress(gizmo_db_, gizmo_last_draw_transform_inverted_.map(event->pos())))) {
// Handle gizmo click
gizmo_start_drag_ = event->pos();
gizmo_last_drag_ = gizmo_start_drag_;
current_gizmo_->SetGlobals(NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime()));
} else if (IsHandDrag(event)) {
// Handle hand drag
hand_last_drag_pos_ = event->pos();
hand_dragging_ = true;
emit HandDragStarted();
setCursor(Qt::ClosedHandCursor);
} else {
if (event->button() == Qt::LeftButton) {
// Handle standard drag
emit DragStarted();
}
super::mousePressEvent(event);
}
}
void ViewerDisplayWidget::mouseMoveEvent(QMouseEvent *event)
{
// Handle hand dragging
if (hand_dragging_) {
// Emit movement
emit HandDragMoved(event->x() - hand_last_drag_pos_.x(),
event->y() - hand_last_drag_pos_.y());
hand_last_drag_pos_ = event->pos();
} else if (add_band_) {
add_band_->setGeometry(QRect(event->pos(), add_band_start_).normalized());
} else if (current_gizmo_) {
// Signal movement
if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) {
if (!gizmo_drag_started_) {
QPointF start = gizmo_start_drag_ * gizmo_last_draw_transform_inverted_;
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 = event->pos() * gizmo_last_draw_transform_inverted_;
switch (draggable->GetDragValueBehavior()) {
case DraggableGizmo::kAbsolute:
// Above value is correct
break;
case DraggableGizmo::kDeltaFromPrevious:
v -= gizmo_last_drag_ * gizmo_last_draw_transform_inverted_;
gizmo_last_drag_ = event->pos();
break;
case DraggableGizmo::kDeltaFromStart:
v -= gizmo_start_drag_ * gizmo_last_draw_transform_inverted_;
break;
}
draggable->DragMove(v.x(), v.y(), event->modifiers());
}
} else {
// Default behavior
super::mouseMoveEvent(event);
}
}
void ViewerDisplayWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (hand_dragging_) {
// Handle hand drag
emit HandDragEnded();
hand_dragging_ = false;
UpdateCursor();
} else if (add_band_) {
const QRect &band_rect = add_band_->geometry();
if (band_rect.width() > 1 && band_rect.height() > 1) {
QRectF r = GenerateDisplayTransform().inverted().mapRect(add_band_->geometry());
emit CreateAddableAt(r);
}
add_band_->deleteLater();
add_band_ = nullptr;
} else if (current_gizmo_) {
// Handle gizmo
if (gizmo_drag_started_) {
MultiUndoCommand *command = new MultiUndoCommand();
if (DraggableGizmo *draggable = dynamic_cast(current_gizmo_)) {
draggable->DragEnd(command);
}
Core::instance()->undo_stack()->pushIfHasChildren(command);
gizmo_drag_started_ = false;
}
current_gizmo_ = nullptr;
} else {
// Default behavior
super::mouseReleaseEvent(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(g)) {
if (text->GetRect().contains(ptr)) {
OpenTextGizmo(text, event);
break;
}
}
}
}
super::mouseDoubleClickEvent(event);
}
void ViewerDisplayWidget::dragEnterEvent(QDragEnterEvent *event)
{
emit DragEntered(event);
if (!event->isAccepted()) {
super::dragEnterEvent(event);
}
}
void ViewerDisplayWidget::dragLeaveEvent(QDragLeaveEvent *event)
{
emit DragLeft(event);
if (!event->isAccepted()) {
super::dragLeaveEvent(event);
}
}
void ViewerDisplayWidget::dropEvent(QDropEvent *event)
{
emit Dropped(event);
if (!event->isAccepted()) {
super::dropEvent(event);
}
}
void ViewerDisplayWidget::OnPaint()
{
// Clear background to empty
QColor bg_color = show_widget_background_ ? palette().window().color() : Qt::black;
renderer()->ClearDestination(nullptr, bg_color.redF(), bg_color.greenF(), bg_color.blueF());
// We only draw if we have a pipeline
if (push_mode_ != kPushNull) {
// Draw texture through color transform
int device_width = width() * devicePixelRatioF();
int device_height = height() * devicePixelRatioF();
VideoParams::Format device_format = static_cast(OLIVE_CONFIG("OfflinePixelFormat").toInt());
VideoParams device_params(device_width, device_height, device_format, VideoParams::kInternalChannelCount);
if (push_mode_ == kPushBlank) {
if (blank_shader_.isNull()) {
blank_shader_ = renderer()->CreateNativeShader(ShaderCode());
}
ShaderJob job;
job.Insert(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, combined_matrix_flipped_));
job.Insert(QStringLiteral("ove_cropmatrix"), NodeValue(NodeValue::kMatrix, crop_matrix_));
renderer()->Blit(blank_shader_, job, device_params, false);
} else if (color_service()) {
if (FramePtr frame = load_frame_.value()) {
// This is a CPU frame, upload it now
if (!texture_
|| texture_->renderer() != renderer() // Some implementations don't like it if we upload to a texture created in another (albeit shared) context
|| texture_->width() != frame->width()
|| texture_->height() != frame->height()
|| texture_->format() != frame->format()
|| texture_->channel_count() != frame->channel_count()) {
texture_ = renderer()->CreateTexture(frame->video_params(), frame->data(), frame->linesize_pixels());
} else {
texture_->Upload(frame->data(), frame->linesize_pixels());
}
} else if (TexturePtr texture = load_frame_.value()) {
// This is a GPU texture, switch to it directly
texture_ = texture;
}
emit TextureChanged(texture_);
push_mode_ = kPushUnnecessary;
TexturePtr texture_to_draw = texture_;
if (deinterlace_) {
if (deinterlace_shader_.isNull()) {
deinterlace_shader_ = renderer()->CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(QStringLiteral(":/shaders/deinterlace.frag"))));
}
if (!deinterlace_texture_
|| deinterlace_texture_->params() != texture_to_draw->params()) {
// (Re)create texture
deinterlace_texture_ = renderer()->CreateTexture(texture_to_draw->params());
}
ShaderJob job;
job.Insert(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, QVector2D(texture_to_draw->width(), texture_to_draw->height())));
job.Insert(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture_to_draw)));
renderer()->BlitToTexture(deinterlace_shader_, job, deinterlace_texture_.get());
texture_to_draw = deinterlace_texture_;
}
ColorTransformJob ctj;
ctj.SetColorProcessor(color_service());
ctj.SetInputTexture(texture_to_draw);
ctj.SetInputAlphaAssociation(OLIVE_CONFIG("ReassocLinToNonLin").toBool() ? kAlphaAssociated : kAlphaNone);
ctj.SetClearDestinationEnabled(false);
ctj.SetTransformMatrix(combined_matrix_flipped_);
ctj.SetCropMatrix(crop_matrix_);
renderer()->BlitColorManaged(ctj, device_params);
}
}
// Draw gizmos if we have any
if (gizmos_) {
NodeTraverser gt;
gt.SetCacheVideoParams(gizmo_params_);
TimeRange range = GenerateGizmoTime();
gizmo_db_ = gt.GenerateRow(gizmos_, range);
QPainter p(inner_widget());
gizmo_last_draw_transform_ = GenerateGizmoTransform(gt, range);
p.setWorldTransform(gizmo_last_draw_transform_);
gizmos_->UpdateGizmoPositions(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, range));
foreach (NodeGizmo *gizmo, gizmos_->GetGizmos()) {
if (gizmo->IsVisible()) {
gizmo->Draw(&p);
}
}
}
// Draw action/title safe areas
if (safe_margin_.is_enabled()) {
QPainter p(inner_widget());
p.setWorldTransform(GenerateWorldTransform());
p.setPen(QPen(Qt::lightGray, 0));
p.setBrush(Qt::NoBrush);
int x = 0, y = 0, w = width(), h = height();
if (safe_margin_.custom_ratio()) {
double widget_ar = static_cast(width()) / static_cast(height());
if (widget_ar > safe_margin_.ratio()) {
// Widget is wider than margins
w = h * safe_margin_.ratio();
x = width() / 2 - w / 2;
} else {
h = w / safe_margin_.ratio();
y = height() / 2 - h / 2;
}
}
p.drawRect(w / 20 + x, h / 20 + y, w / 10 * 9, h / 10 * 9);
p.drawRect(w / 10 + x, h / 10 + y, w / 10 * 8, h / 10 * 8);
int cross = qMin(w, h) / 32;
QLine lines[] = {QLine(rect().center().x() - cross, rect().center().y(),rect().center().x() + cross, rect().center().y()),
QLine(rect().center().x(), rect().center().y() - cross, rect().center().x(), rect().center().y() + cross)};
p.drawLines(lines, 2);
}
if (show_fps_) {
{
qint64 now = QDateTime::currentMSecsSinceEpoch();
double frame_rate;
if (now == fps_timer_start_) {
// This will cause a divide by zero, so we do nothing here
frame_rate = 0;
} else {
frame_rate = double(fps_timer_update_count_) / double((now - fps_timer_start_)/1000.0);
}
if (frame_rate > 0) {
frame_rate_averages_[frame_rate_average_count_%frame_rate_averages_.size()] = frame_rate;
frame_rate_average_count_++;
}
}
if (frame_rate_average_count_ >= frame_rate_averages_.size()) {
QPainter p(inner_widget());
double average = 0.0;
for (int i=0; irect(), tr("%1 FPS").arg(QString::number(average, 'f', 1)));
if (frames_skipped_ > 0) {
DrawTextWithCrudeShadow(&p, inner_widget()->rect().adjusted(0, p.fontMetrics().height(), 0, 0),
tr("%1 frames skipped").arg(frames_skipped_));
}
}
}
// Extraordinarily basic subtitle renderer. Hoping to swap this out with libass at some point.
if (show_subtitles_ && subtitle_tracks_) {
const QVector