/*** 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 . ***/ #include "viewerdisplay.h" #include #include #include #include #include #include #include #include #include "common/define.h" #include "common/functiontimer.h" #include "config/config.h" #include "core.h" #include "node/traverser.h" namespace olive { #define super ManagedDisplayWidget ViewerDisplayWidget::ViewerDisplayWidget(QWidget *parent) : super(parent), deinterlace_texture_(nullptr), signal_cursor_color_(false), gizmos_(nullptr), gizmo_click_(false), hand_dragging_(false), deinterlace_(false), show_fps_(false), frames_skipped_(0), show_widget_background_(false), push_mode_(kPushNull) { connect(Core::instance(), &Core::ToolChanged, this, &ViewerDisplayWidget::UpdateCursor); 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 { 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::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(); } } QPoint ViewerDisplayWidget::TransformViewerSpaceToBufferSpace(QPoint pos) { /* * Inversion will only fail if the viewer has been scaled by 0 in any direction * which I think should never happen. */ return pos * GenerateGizmoTransform().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 && gizmos_ && gizmos_->GizmoPress(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, GenerateGizmoTime()), TransformViewerSpaceToBufferSpace(event->pos()))) { // Handle gizmo click gizmo_click_ = true; gizmo_drag_time_ = GetGizmoTime(); gizmo_start_drag_ = event->pos(); } 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 (gizmo_click_) { // Signal movement gizmos_->GizmoMove(TransformViewerSpaceToBufferSpace(event->pos()), gizmo_drag_time_, event->modifiers()); gizmo_start_drag_ = event->pos(); update(); } 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 (gizmo_click_) { // Handle gizmo gizmos_->GizmoRelease(); gizmo_click_ = false; } else { // Default behavior super::mouseReleaseEvent(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::showEvent(QShowEvent *event) { emit VisibilityChanged(true); super::showEvent(event); } void ViewerDisplayWidget::hideEvent(QHideEvent *event) { emit VisibilityChanged(false); super::hideEvent(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(Config::Current()["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.InsertValue(QStringLiteral("ove_mvpmat"), NodeValue(NodeValue::kMatrix, combined_matrix_flipped_)); job.InsertValue(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.InsertValue(QStringLiteral("resolution_in"), NodeValue(NodeValue::kVec2, QVector2D(texture_to_draw->width(), texture_to_draw->height()))); job.InsertValue(QStringLiteral("ove_maintex"), NodeValue(NodeValue::kTexture, QVariant::fromValue(texture_to_draw))); renderer()->BlitToTexture(deinterlace_shader_, job, deinterlace_texture_.get()); texture_to_draw = deinterlace_texture_; } renderer()->BlitColorManaged(color_service(), texture_to_draw, Renderer::kAlphaNone, device_params, false, combined_matrix_flipped_, crop_matrix_); } } // 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()); p.setWorldTransform(GenerateGizmoTransform()); gizmos_->DrawGizmos(gizmo_db_, NodeTraverser::GenerateGlobals(gizmo_params_, range), &p); } // Draw action/title safe areas if (safe_margin_.is_enabled()) { QPainter p(inner_widget()); p.setWorldTransform(GenerateWorldTransform()); p.setPen(Qt::lightGray); 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_)); } } } } void ViewerDisplayWidget::OnDestroy() { renderer()->DestroyNativeShader(deinterlace_shader_); deinterlace_shader_.clear(); renderer()->DestroyNativeShader(blank_shader_); blank_shader_.clear(); super::OnDestroy(); texture_ = nullptr; deinterlace_texture_ = nullptr; if (load_frame_.isNull()) { push_mode_ = kPushNull; } else { push_mode_ = kPushFrame; } } QPointF ViewerDisplayWidget::GetTexturePosition(const QPoint &screen_pos) { return GetTexturePosition(screen_pos.x(), screen_pos.y()); } QPointF ViewerDisplayWidget::GetTexturePosition(const QSize &size) { return GetTexturePosition(size.width(), size.height()); } QPointF ViewerDisplayWidget::GetTexturePosition(const double &x, const double &y) { return QPointF(x / gizmo_params_.width(), y / gizmo_params_.height()); } void ViewerDisplayWidget::DrawTextWithCrudeShadow(QPainter *painter, const QRect &rect, const QString &text) { painter->setPen(Qt::black); painter->drawText(rect.adjusted(1, 1, 0, 0), text); painter->setPen(Qt::white); painter->drawText(rect, text); } rational ViewerDisplayWidget::GetGizmoTime() { return GetAdjustedTime(GetTimeTarget(), gizmos_, time_, true); } bool ViewerDisplayWidget::IsHandDrag(QMouseEvent *event) const { return event->button() == Qt::MiddleButton || Core::instance()->tool() == Tool::kHand; } void ViewerDisplayWidget::UpdateMatrix() { combined_matrix_ = scale_matrix_ * translate_matrix_; combined_matrix_flipped_.setToIdentity(); combined_matrix_flipped_.scale(1.0, -1.0, 1.0); combined_matrix_flipped_ *= combined_matrix_; update(); } QTransform ViewerDisplayWidget::GenerateWorldTransform() { /* * Get matrix elements (roughly) as below in column major order * * | Sx 0 0 Tx | * | 0 Sy 0 Ty | * | 0 0 Sz Tz | * | 0 0 0 1 | */ float *d = combined_matrix_.data(); QTransform world; // Move corner of canvas to correct point world.translate(width() * 0.5 - width() * *(d)*0.5, height() * 0.5 - height() * *(d + 5) * 0.5); // Scale world.scale(*(d), *(d + 5)); // Translate for mouse movement world.translate(*(d + 12) * width() * 0.5 / *(d), *(d + 13) * height() * 0.5 / *(d + 5)); return world; } QTransform ViewerDisplayWidget::GenerateGizmoTransform() { QVector2D viewer_scale(GetTexturePosition(size())); QTransform gizmo_transform = GenerateWorldTransform(); gizmo_transform.scale(viewer_scale.x(), viewer_scale.y()); gizmo_transform.scale(gizmo_params_.pixel_aspect_ratio().flipped().toDouble(), 1); return gizmo_transform; } void ViewerDisplayWidget::EmitColorAtCursor(QMouseEvent *e) { // Do this no matter what, emits signal to any pixel samplers if (signal_cursor_color_) { Color reference, display; if (texture_) { QPointF pixel_pos = GenerateGizmoTransform().inverted().map(e->pos()); pixel_pos /= texture_->params().divider(); reference = renderer()->GetPixelFromTexture(texture_.get(), pixel_pos); display = color_service()->ConvertColor(reference); } emit CursorColor(reference, display); } } void ViewerDisplayWidget::SetShowFPS(bool e) { show_fps_ = e; update(); } }