The macro defined namespaces confused the hell out of lupdate and more or less broke translations permanently. Looks like the only way we can do it is to have a hardcoded namespace, which goes against my instinct, but honestly how likely is it that we'll change the namespace anyway (I guess forks might want to do it, but that's their problem ;) )
1203 lines
35 KiB
C++
1203 lines
35 KiB
C++
/***
|
|
|
|
Olive - Non-Linear Video Editor
|
|
Copyright (C) 2020 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 "viewer.h"
|
|
|
|
#include <QDateTime>
|
|
#include <QGuiApplication>
|
|
#include <QInputDialog>
|
|
#include <QLabel>
|
|
#include <QMessageBox>
|
|
#include <QResizeEvent>
|
|
#include <QScreen>
|
|
#include <QtMath>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "audio/audiomanager.h"
|
|
#include "common/power.h"
|
|
#include "common/ratiodialog.h"
|
|
#include "common/timecodefunctions.h"
|
|
#include "config/config.h"
|
|
#include "project/item/sequence/sequence.h"
|
|
#include "project/project.h"
|
|
#include "render/rendermanager.h"
|
|
#include "task/taskmanager.h"
|
|
#include "widget/menu/menu.h"
|
|
#include "window/mainwindow/mainwindow.h"
|
|
|
|
namespace olive {
|
|
|
|
QVector<ViewerWidget*> ViewerWidget::instances_;
|
|
|
|
const int kMaxPreQueueSize = 16;
|
|
|
|
ViewerWidget::ViewerWidget(QWidget *parent) :
|
|
TimeBasedWidget(false, true, parent),
|
|
playback_speed_(0),
|
|
frame_cache_job_time_(0),
|
|
color_menu_enabled_(true),
|
|
override_color_manager_(nullptr),
|
|
time_changed_from_timer_(false),
|
|
pause_autocache_during_playback_(false),
|
|
prequeuing_(false)
|
|
{
|
|
// Set up main layout
|
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
layout->setMargin(0);
|
|
|
|
// Set up stacked widget to allow switching away from the viewer widget
|
|
stack_ = new QStackedWidget();
|
|
layout->addWidget(stack_);
|
|
|
|
// Create main OpenGL-based view and sizer
|
|
sizer_ = new ViewerSizer();
|
|
stack_->addWidget(sizer_);
|
|
|
|
display_widget_ = new ViewerDisplayWidget();
|
|
connect(display_widget_, &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
|
|
connect(display_widget_, &ViewerDisplayWidget::CursorColor, this, &ViewerWidget::CursorColor);
|
|
connect(display_widget_, &ViewerDisplayWidget::ColorProcessorChanged, this, &ViewerWidget::ColorProcessorChanged);
|
|
connect(display_widget_, &ViewerDisplayWidget::ColorManagerChanged, this, &ViewerWidget::ColorManagerChanged);
|
|
connect(sizer_, &ViewerSizer::RequestScale, display_widget_, &ViewerDisplayWidget::SetMatrixZoom);
|
|
connect(sizer_, &ViewerSizer::RequestTranslate, display_widget_, &ViewerDisplayWidget::SetMatrixTranslate);
|
|
connect(display_widget_, &ViewerDisplayWidget::HandDragMoved, sizer_, &ViewerSizer::HandDragMove);
|
|
sizer_->SetWidget(display_widget_);
|
|
|
|
// Create waveform view when audio is connected and video isn't
|
|
waveform_view_ = new AudioWaveformView();
|
|
stack_->addWidget(waveform_view_);
|
|
|
|
// Create time ruler
|
|
layout->addWidget(ruler());
|
|
|
|
// Create scrollbar
|
|
layout->addWidget(scrollbar());
|
|
connect(scrollbar(), &QScrollBar::valueChanged, ruler(), &TimeRuler::SetScroll);
|
|
connect(scrollbar(), &QScrollBar::valueChanged, waveform_view_, &AudioWaveformView::SetScroll);
|
|
|
|
// Create lower controls
|
|
controls_ = new PlaybackControls();
|
|
controls_->SetTimecodeEnabled(true);
|
|
controls_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
|
|
connect(controls_, &PlaybackControls::PlayClicked, this, static_cast<void(ViewerWidget::*)()>(&ViewerWidget::Play));
|
|
connect(controls_, &PlaybackControls::PauseClicked, this, &ViewerWidget::Pause);
|
|
connect(controls_, &PlaybackControls::PrevFrameClicked, this, &ViewerWidget::PrevFrame);
|
|
connect(controls_, &PlaybackControls::NextFrameClicked, this, &ViewerWidget::NextFrame);
|
|
connect(controls_, &PlaybackControls::BeginClicked, this, &ViewerWidget::GoToStart);
|
|
connect(controls_, &PlaybackControls::EndClicked, this, &ViewerWidget::GoToEnd);
|
|
connect(controls_, &PlaybackControls::TimeChanged, this, &ViewerWidget::SetTimeAndSignal);
|
|
layout->addWidget(controls_);
|
|
|
|
// FIXME: Magic number
|
|
SetScale(48.0);
|
|
|
|
// Ensures that seeking on the waveform view updates the time as expected
|
|
connect(waveform_view_, &AudioWaveformView::TimeChanged, this, &ViewerWidget::TimeChangedFromWaveform);
|
|
connect(waveform_view_, &AudioWaveformView::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
|
|
|
|
connect(&playback_backup_timer_, &QTimer::timeout, this, &ViewerWidget::PlaybackTimerUpdate);
|
|
|
|
SetAutoMaxScrollBar(true);
|
|
|
|
instances_.append(this);
|
|
}
|
|
|
|
ViewerWidget::~ViewerWidget()
|
|
{
|
|
QList<ViewerWindow*> windows = windows_;
|
|
|
|
foreach (ViewerWindow* window, windows) {
|
|
delete window;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::TimeChangedEvent(const int64_t &i)
|
|
{
|
|
if (!time_changed_from_timer_) {
|
|
Pause();
|
|
}
|
|
|
|
controls_->SetTime(i);
|
|
|
|
{
|
|
qint64 waveform_time;
|
|
|
|
if (waveform_view_->timebase() != this->timebase()) {
|
|
waveform_time = Timecode::rescale_timestamp(i, this->timebase(), waveform_view_->timebase());
|
|
} else {
|
|
waveform_time = i;
|
|
}
|
|
|
|
waveform_view_->SetTime(waveform_time);
|
|
}
|
|
|
|
if (GetConnectedNode() && last_time_ != i) {
|
|
rational time_set = Timecode::timestamp_to_time(i, timebase());
|
|
|
|
if (!IsPlaying()) {
|
|
UpdateTextureFromNode(time_set);
|
|
|
|
PushScrubbedAudio();
|
|
}
|
|
|
|
if (!pause_autocache_during_playback_ || !IsPlaying()) {
|
|
auto_cacher_.SetPlayhead(time_set);
|
|
}
|
|
|
|
display_widget_->SetTime(time_set);
|
|
}
|
|
|
|
last_time_ = i;
|
|
}
|
|
|
|
void ViewerWidget::ConnectNodeInternal(ViewerOutput *n)
|
|
{
|
|
connect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
|
|
connect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
|
|
connect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
|
connect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
|
|
connect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
|
|
connect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
|
|
connect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
|
|
connect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange);
|
|
connect(n, &ViewerOutput::GraphChangedFrom, this, &ViewerWidget::UpdateStack);
|
|
|
|
InterlacingChangedSlot(n->video_params().interlacing());
|
|
|
|
ruler()->SetPlaybackCache(n->video_frame_cache());
|
|
|
|
SetViewerResolution(n->video_params().width(), n->video_params().height());
|
|
SetViewerPixelAspect(n->video_params().pixel_aspect_ratio());
|
|
last_length_ = rational();
|
|
LengthChangedSlot(n->GetLength());
|
|
|
|
ColorManager* using_manager;
|
|
if (override_color_manager_) {
|
|
using_manager = override_color_manager_;
|
|
} else if (n->parent()) {
|
|
using_manager = static_cast<Sequence*>(n->parent())->project()->color_manager();
|
|
} else {
|
|
qWarning() << "Failed to find a suitable color manager for the connected viewer node";
|
|
using_manager = nullptr;
|
|
}
|
|
|
|
auto_cacher_.SetColorManager(using_manager);
|
|
|
|
display_widget_->ConnectColorManager(using_manager);
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->display_widget()->ConnectColorManager(using_manager);
|
|
}
|
|
|
|
UpdateStack();
|
|
|
|
waveform_view_->SetViewer(GetConnectedNode()->audio_playback_cache());
|
|
if (GetConnectedTimelinePoints()) {
|
|
waveform_view_->ConnectTimelinePoints(GetConnectedTimelinePoints());
|
|
}
|
|
|
|
UpdateRendererVideoParameters();
|
|
UpdateRendererAudioParameters();
|
|
|
|
// Set texture to new texture (or null if no viewer node is available)
|
|
ForceUpdate();
|
|
}
|
|
|
|
void ViewerWidget::DisconnectNodeInternal(ViewerOutput *n)
|
|
{
|
|
PauseInternal();
|
|
|
|
disconnect(n, &ViewerOutput::SizeChanged, this, &ViewerWidget::SetViewerResolution);
|
|
disconnect(n, &ViewerOutput::PixelAspectChanged, this, &ViewerWidget::SetViewerPixelAspect);
|
|
disconnect(n, &ViewerOutput::LengthChanged, this, &ViewerWidget::LengthChangedSlot);
|
|
disconnect(n, &ViewerOutput::InterlacingChanged, this, &ViewerWidget::InterlacingChangedSlot);
|
|
disconnect(n, &ViewerOutput::VideoParamsChanged, this, &ViewerWidget::UpdateRendererVideoParameters);
|
|
disconnect(n, &ViewerOutput::AudioParamsChanged, this, &ViewerWidget::UpdateRendererAudioParameters);
|
|
disconnect(n->video_frame_cache(), &FrameHashCache::Invalidated, this, &ViewerWidget::ViewerInvalidatedVideoRange);
|
|
disconnect(n->video_frame_cache(), &FrameHashCache::Shifted, this, &ViewerWidget::ViewerShiftedRange);
|
|
disconnect(n, &ViewerOutput::GraphChangedFrom, this, &ViewerWidget::UpdateStack);
|
|
|
|
ruler()->SetPlaybackCache(nullptr);
|
|
|
|
// Effectively disables the viewer and clears the state
|
|
SetViewerResolution(0, 0);
|
|
|
|
display_widget_->DisconnectColorManager();
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->display_widget()->DisconnectColorManager();
|
|
}
|
|
auto_cacher_.SetColorManager(nullptr);
|
|
|
|
waveform_view_->SetViewer(nullptr);
|
|
waveform_view_->ConnectTimelinePoints(nullptr);
|
|
|
|
// Queue an UpdateStack so that when it runs, the viewer node will be fully disconnected
|
|
QMetaObject::invokeMethod(this, "UpdateStack", Qt::QueuedConnection);
|
|
}
|
|
|
|
void ViewerWidget::ConnectedNodeChanged(ViewerOutput *n)
|
|
{
|
|
auto_cacher_.SetViewerNode(n);
|
|
}
|
|
|
|
void ViewerWidget::ScaleChangedEvent(const double &s)
|
|
{
|
|
TimeBasedWidget::ScaleChangedEvent(s);
|
|
|
|
waveform_view_->SetScale(s);
|
|
}
|
|
|
|
void ViewerWidget::resizeEvent(QResizeEvent *event)
|
|
{
|
|
TimeBasedWidget::resizeEvent(event);
|
|
|
|
UpdateMinimumScale();
|
|
}
|
|
|
|
void ViewerWidget::TogglePlayPause()
|
|
{
|
|
if (IsPlaying()) {
|
|
Pause();
|
|
} else {
|
|
Play();
|
|
}
|
|
}
|
|
|
|
bool ViewerWidget::IsPlaying() const
|
|
{
|
|
return playback_speed_ != 0;
|
|
}
|
|
|
|
void ViewerWidget::ConnectViewerNode(ViewerOutput *node, ColorManager* color_manager)
|
|
{
|
|
override_color_manager_ = color_manager;
|
|
|
|
TimeBasedWidget::ConnectViewerNode(node);
|
|
}
|
|
|
|
void ViewerWidget::SetColorMenuEnabled(bool enabled)
|
|
{
|
|
color_menu_enabled_ = enabled;
|
|
}
|
|
|
|
void ViewerWidget::SetMatrix(const QMatrix4x4 &mat)
|
|
{
|
|
display_widget_->SetMatrixZoom(mat);
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->display_widget()->SetMatrixZoom(mat);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetFullScreen(QScreen *screen)
|
|
{
|
|
if (!screen) {
|
|
// Try to find the screen that contains the mouse cursor currently
|
|
foreach (QScreen* test, QGuiApplication::screens()) {
|
|
if (test->geometry().contains(QCursor::pos())) {
|
|
screen = test;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Fallback, just use the first screen
|
|
if (!screen) {
|
|
screen = QGuiApplication::screens().first();
|
|
}
|
|
}
|
|
|
|
ViewerWindow* vw = new ViewerWindow(this);
|
|
|
|
vw->setGeometry(screen->geometry());
|
|
vw->showFullScreen();
|
|
vw->display_widget()->ConnectColorManager(color_manager());
|
|
connect(vw, &ViewerWindow::destroyed, this, &ViewerWidget::WindowAboutToClose);
|
|
connect(vw->display_widget(), &ViewerDisplayWidget::customContextMenuRequested, this, &ViewerWidget::ShowContextMenu);
|
|
|
|
if (GetConnectedNode()) {
|
|
vw->SetVideoParams(GetConnectedNode()->video_params());
|
|
vw->display_widget()->SetDeinterlacing(vw->display_widget()->IsDeinterlacing());
|
|
}
|
|
|
|
vw->display_widget()->SetImage(display_widget_->last_loaded_buffer());
|
|
|
|
windows_.append(vw);
|
|
}
|
|
|
|
void ViewerWidget::ForceUpdate()
|
|
{
|
|
// Hack that forces the viewer to update
|
|
UpdateTextureFromNode(GetTime());
|
|
}
|
|
|
|
void ViewerWidget::SetAutoCacheEnabled(bool e)
|
|
{
|
|
auto_cacher_.SetPaused(!e);
|
|
}
|
|
|
|
void ViewerWidget::CacheEntireSequence()
|
|
{
|
|
auto_cacher_.ForceCacheRange(TimeRange(rational(), GetConnectedNode()->video_frame_cache()->GetLength()));
|
|
}
|
|
|
|
void ViewerWidget::CacheSequenceInOut()
|
|
{
|
|
if (GetConnectedTimelinePoints() && GetConnectedTimelinePoints()->workarea()->enabled()) {
|
|
auto_cacher_.ForceCacheRange(GetConnectedTimelinePoints()->workarea()->range());
|
|
} else {
|
|
QMessageBox::warning(this,
|
|
tr("Error"),
|
|
tr("No in or out points are set to cache."),
|
|
QMessageBox::Ok);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetGizmos(Node *node)
|
|
{
|
|
display_widget_->SetTimeTarget(GetConnectedNode());
|
|
display_widget_->SetGizmos(node);
|
|
}
|
|
|
|
FramePtr ViewerWidget::DecodeCachedImage(const QString &fn, const rational& time) const
|
|
{
|
|
FramePtr frame = GetConnectedNode()->video_frame_cache()->LoadCacheFrame(fn);
|
|
|
|
if (frame) {
|
|
frame->set_timestamp(time);
|
|
} else {
|
|
qWarning() << "Tried to load cached frame from file but it was null";
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
void ViewerWidget::DecodeCachedImage(RenderTicketPtr ticket, const QString &fn, const rational& time) const
|
|
{
|
|
ticket->Start();
|
|
ticket->Finish(QVariant::fromValue(DecodeCachedImage(fn, time)), false);
|
|
}
|
|
|
|
bool ViewerWidget::ShouldForceWaveform() const
|
|
{
|
|
return GetConnectedNode()
|
|
&& !GetConnectedNode()->texture_input()->is_connected()
|
|
&& GetConnectedNode()->samples_input()->is_connected();
|
|
}
|
|
|
|
void ViewerWidget::UpdateTextureFromNode(const rational& time)
|
|
{
|
|
bool frame_exists_at_time = FrameExistsAtTime(time);
|
|
|
|
// Check playback queue for a frame
|
|
if (IsPlaying()) {
|
|
|
|
// We still run the playback queue even when FrameExistsAtTime returns false because we might be
|
|
// playing backwards and about to start showing frames, so the queue should be prepared for
|
|
// that.
|
|
while (!playback_queue_.empty()) {
|
|
|
|
const ViewerPlaybackFrame& pf = playback_queue_.front();
|
|
|
|
if (pf.timestamp == time) {
|
|
|
|
// Frame was in queue, no need to decode anything
|
|
SetDisplayImage(pf.frame, true);
|
|
return;
|
|
|
|
} else {
|
|
|
|
// Skip this frame
|
|
PopOldestFrameFromPlaybackQueue();
|
|
|
|
}
|
|
}
|
|
|
|
// Only show warning if frame actually exists
|
|
if (frame_exists_at_time) {
|
|
qWarning() << "Playback queue failed to keep up";
|
|
}
|
|
|
|
}
|
|
|
|
if (!frame_exists_at_time) {
|
|
// There is definitely no frame here, we can immediately flip to showing nothing
|
|
nonqueue_watchers_.clear();
|
|
SetDisplayImage(nullptr, false);
|
|
return;
|
|
}
|
|
|
|
// Frame was not in queue, will require rendering or decoding from cache
|
|
RenderTicketWatcher* watcher = new RenderTicketWatcher();
|
|
connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrame);
|
|
nonqueue_watchers_.append(watcher);
|
|
watcher->SetTicket(GetFrame(time, true));
|
|
}
|
|
|
|
void ViewerWidget::PlayInternal(int speed, bool in_to_out_only)
|
|
{
|
|
Q_ASSERT(speed != 0);
|
|
|
|
if (!GetConnectedNode()) {
|
|
// Do nothing if no viewer node is attached
|
|
return;
|
|
}
|
|
|
|
if (timebase().isNull()) {
|
|
qCritical() << "ViewerWidget can't play with an invalid timebase";
|
|
return;
|
|
}
|
|
|
|
// Kindly tell all viewers to stop caching
|
|
if (pause_autocache_during_playback_) {
|
|
foreach (ViewerWidget* viewer, instances_) {
|
|
viewer->auto_cacher_.ClearVideoQueue();
|
|
}
|
|
}
|
|
|
|
// If the playhead is beyond the end, restart at 0
|
|
if (!in_to_out_only && GetTime() >= GetConnectedNode()->GetLength()) {
|
|
if (speed > 0) {
|
|
SetTimeAndSignal(0);
|
|
} else {
|
|
SetTimeAndSignal(Timecode::time_to_timestamp(GetConnectedNode()->GetLength(), timebase()));
|
|
}
|
|
}
|
|
|
|
playback_speed_ = speed;
|
|
play_in_to_out_only_ = in_to_out_only;
|
|
|
|
playback_queue_next_frame_ = ruler()->GetTime();
|
|
|
|
controls_->ShowPauseButton();
|
|
|
|
// Attempt to fill playback queue
|
|
if (stack_->currentWidget() == sizer_) {
|
|
prequeue_length_ = DeterminePlaybackQueueSize();
|
|
|
|
if (prequeue_length_ > 0) {
|
|
prequeuing_ = true;
|
|
|
|
for (int i=0; i<prequeue_length_; i++) {
|
|
RequestNextFrameForQueue();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!prequeuing_) {
|
|
FinishPlayPreprocess();
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::PauseInternal()
|
|
{
|
|
if (IsPlaying()) {
|
|
AudioManager::instance()->StopOutput();
|
|
playback_speed_ = 0;
|
|
controls_->ShowPlayButton();
|
|
|
|
disconnect(display_widget_, &ViewerDisplayWidget::frameSwapped, this, &ViewerWidget::ForceUpdate);
|
|
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->Pause();
|
|
}
|
|
|
|
playback_queue_.clear();
|
|
playback_backup_timer_.stop();
|
|
}
|
|
|
|
prequeuing_ = false;
|
|
}
|
|
|
|
void ViewerWidget::PushScrubbedAudio()
|
|
{
|
|
if (!IsPlaying() && Config::Current()["AudioScrubbing"].toBool()) {
|
|
// Get audio src device from renderer
|
|
AudioPlaybackCache::PlaybackDevice* audio_src = GetConnectedNode()->audio_playback_cache()->CreatePlaybackDevice();
|
|
|
|
if (audio_src->open(QIODevice::ReadOnly)) {
|
|
const AudioParams& params = GetConnectedNode()->audio_playback_cache()->GetParameters();
|
|
|
|
// FIXME: Hardcoded scrubbing interval (20ms)
|
|
int size_of_sample = params.time_to_bytes(rational(20, 1000));
|
|
|
|
// Push audio
|
|
audio_src->seek(params.time_to_bytes(GetTime()));
|
|
QByteArray frame_audio = audio_src->read(size_of_sample);
|
|
AudioManager::instance()->SetOutputParams(params);
|
|
AudioManager::instance()->PushToOutput(frame_audio);
|
|
|
|
audio_src->close();
|
|
}
|
|
|
|
delete audio_src;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateMinimumScale()
|
|
{
|
|
if (!GetConnectedNode()) {
|
|
return;
|
|
}
|
|
|
|
if (GetConnectedNode()->GetLength().isNull()) {
|
|
// Avoids divide by zero
|
|
SetMinimumScale(0);
|
|
} else {
|
|
SetMinimumScale(static_cast<double>(ruler()->width()) / GetConnectedNode()->GetLength().toDouble());
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetColorTransform(const ColorTransform &transform, ViewerDisplayWidget *sender)
|
|
{
|
|
sender->SetColorTransform(transform);
|
|
}
|
|
|
|
QString ViewerWidget::GetCachedFilenameFromTime(const rational &time)
|
|
{
|
|
if (FrameExistsAtTime(time)) {
|
|
QByteArray hash = GetConnectedNode()->video_frame_cache()->GetHash(time);
|
|
|
|
if (!hash.isEmpty()) {
|
|
return GetConnectedNode()->video_frame_cache()->CachePathName(hash);
|
|
}
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
bool ViewerWidget::FrameExistsAtTime(const rational &time)
|
|
{
|
|
return GetConnectedNode()
|
|
&& ((time >= 0 && time < GetConnectedNode()->video_frame_cache()->GetLength())
|
|
|| GetConnectedNode()->video_frame_cache()->GetLength().isNull());
|
|
}
|
|
|
|
void ViewerWidget::SetDisplayImage(FramePtr frame, bool main_only)
|
|
{
|
|
display_widget_->SetImage(frame);
|
|
|
|
if (!main_only) {
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->display_widget()->SetImage(frame);
|
|
}
|
|
}
|
|
|
|
emit LoadedBuffer(frame.get());
|
|
}
|
|
|
|
void ViewerWidget::RequestNextFrameForQueue()
|
|
{
|
|
rational next_time = Timecode::timestamp_to_time(playback_queue_next_frame_,
|
|
timebase());
|
|
|
|
playback_queue_next_frame_ += playback_speed_;
|
|
|
|
RenderTicketWatcher* watcher = new RenderTicketWatcher();
|
|
connect(watcher, &RenderTicketWatcher::Finished, this, &ViewerWidget::RendererGeneratedFrameForQueue);
|
|
watcher->SetTicket(GetFrame(next_time, false));
|
|
}
|
|
|
|
RenderTicketPtr ViewerWidget::GetFrame(const rational &t, bool clear_render_queue)
|
|
{
|
|
QByteArray cached_hash = GetConnectedNode()->video_frame_cache()->GetHash(t);
|
|
|
|
QString cache_fn = GetConnectedNode()->video_frame_cache()->CachePathName(cached_hash);
|
|
|
|
if (cached_hash.isEmpty() || !QFileInfo::exists(cache_fn)) {
|
|
// Frame hasn't been cached, start render job
|
|
if (clear_render_queue) {
|
|
auto_cacher_.ClearVideoQueue();
|
|
}
|
|
|
|
return auto_cacher_.GetSingleFrame(t);
|
|
} else {
|
|
// Frame has been cached, grab the frame
|
|
RenderTicketPtr ticket = std::make_shared<RenderTicket>();
|
|
ticket->setProperty("time", QVariant::fromValue(t));
|
|
QtConcurrent::run(this, &ViewerWidget::DecodeCachedImage, ticket, cache_fn, t);
|
|
|
|
return ticket;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::FinishPlayPreprocess()
|
|
{
|
|
int64_t playback_start_time = ruler()->GetTime();
|
|
|
|
AudioPlaybackCache* audio_cache = GetConnectedNode()->audio_playback_cache();
|
|
AudioManager::instance()->SetOutputParams(audio_cache->GetParameters());
|
|
AudioManager::instance()->StartOutput(audio_cache,
|
|
audio_cache->GetParameters().time_to_bytes(GetTime()),
|
|
playback_speed_);
|
|
|
|
playback_timer_.Start(playback_start_time, playback_speed_, timebase_dbl());
|
|
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->Play(playback_start_time, playback_speed_, timebase());
|
|
}
|
|
|
|
connect(display_widget_, &ViewerDisplayWidget::frameSwapped, this, &ViewerWidget::ForceUpdate);
|
|
|
|
playback_backup_timer_.setInterval(qFloor(timebase_dbl()));
|
|
playback_backup_timer_.start();
|
|
|
|
PlaybackTimerUpdate();
|
|
}
|
|
|
|
int ViewerWidget::DeterminePlaybackQueueSize()
|
|
{
|
|
int64_t end_ts;
|
|
|
|
if (playback_speed_ > 0) {
|
|
end_ts = Timecode::time_to_timestamp(GetConnectedNode()->video_frame_cache()->GetLength(),
|
|
timebase());
|
|
} else {
|
|
end_ts = 0;
|
|
}
|
|
|
|
int remaining_frames = (end_ts - GetTimestamp()) * playback_speed_;
|
|
|
|
return qMin(kMaxPreQueueSize, remaining_frames);
|
|
}
|
|
|
|
void ViewerWidget::PopOldestFrameFromPlaybackQueue()
|
|
{
|
|
playback_queue_.pop_front();
|
|
|
|
if (int(playback_queue_.size()) < DeterminePlaybackQueueSize()) {
|
|
RequestNextFrameForQueue();
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateStack()
|
|
{
|
|
rational new_tb;
|
|
|
|
if (ShouldForceWaveform()) {
|
|
// If we have a node AND video is disconnected AND audio is connected, show waveform view
|
|
stack_->setCurrentWidget(waveform_view_);
|
|
//new_tb = GetConnectedNode()->audio_params().time_base();
|
|
} else {
|
|
// Otherwise show regular display
|
|
stack_->setCurrentWidget(sizer_);
|
|
|
|
/*if (GetConnectedNode()) {
|
|
new_tb = GetConnectedNode()->video_params().time_base();
|
|
}*/
|
|
}
|
|
|
|
/*if (new_tb != timebase()) {
|
|
SetTimebase(new_tb);
|
|
}*/
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetFullScreen(QAction *action)
|
|
{
|
|
SetFullScreen(QGuiApplication::screens().at(action->data().toInt()));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuDisableSafeMargins()
|
|
{
|
|
context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(false));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetSafeMargins()
|
|
{
|
|
context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(true));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuSetCustomSafeMargins()
|
|
{
|
|
bool ok;
|
|
|
|
double new_ratio = GetFloatRatioFromUser(this,
|
|
tr("Safe Margins"),
|
|
&ok);
|
|
|
|
if (ok) {
|
|
context_menu_widget_->SetSafeMargins(ViewerSafeMarginInfo(true, new_ratio));
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::WindowAboutToClose()
|
|
{
|
|
windows_.removeOne(static_cast<ViewerWindow*>(sender()));
|
|
}
|
|
|
|
void ViewerWidget::ContextMenuScopeTriggered(QAction *action)
|
|
{
|
|
emit RequestScopePanel(static_cast<ScopePanel::Type>(action->data().toInt()));
|
|
}
|
|
|
|
void ViewerWidget::RendererGeneratedFrame()
|
|
{
|
|
RenderTicketWatcher* ticket = static_cast<RenderTicketWatcher*>(sender());
|
|
|
|
if (!ticket->WasCancelled()) {
|
|
FramePtr frame = ticket->Get().value<FramePtr>();
|
|
|
|
if (nonqueue_watchers_.contains(ticket)) {
|
|
while (!nonqueue_watchers_.isEmpty()) {
|
|
if (nonqueue_watchers_.takeFirst() == ticket) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
SetDisplayImage(frame, false);
|
|
}
|
|
}
|
|
|
|
delete ticket;
|
|
}
|
|
|
|
void ViewerWidget::RendererGeneratedFrameForQueue()
|
|
{
|
|
RenderTicketWatcher* watcher = static_cast<RenderTicketWatcher*>(sender());
|
|
|
|
if (!watcher->WasCancelled()) {
|
|
FramePtr frame = watcher->Get().value<FramePtr>();
|
|
|
|
// Ignore this signal if we've paused now
|
|
if (IsPlaying() || prequeuing_) {
|
|
playback_queue_.AppendTimewise({frame->timestamp(), frame}, playback_speed_);
|
|
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->queue()->AppendTimewise({frame->timestamp(), frame}, playback_speed_);
|
|
}
|
|
|
|
if (prequeuing_ && int(playback_queue_.size()) == prequeue_length_) {
|
|
prequeuing_ = false;
|
|
FinishPlayPreprocess();
|
|
}
|
|
}
|
|
}
|
|
|
|
delete watcher;
|
|
}
|
|
|
|
void ViewerWidget::ShowContextMenu(const QPoint &pos)
|
|
{
|
|
Menu menu(static_cast<QWidget*>(sender()));
|
|
|
|
context_menu_widget_ = dynamic_cast<ViewerDisplayWidget*>(sender());
|
|
|
|
// ViewerDisplayWidget options
|
|
if (context_menu_widget_) {
|
|
// Color options
|
|
if (context_menu_widget_->color_manager() && color_menu_enabled_) {
|
|
{
|
|
Menu* ocio_display_menu = context_menu_widget_->GetDisplayMenu(&menu);
|
|
menu.addMenu(ocio_display_menu);
|
|
}
|
|
|
|
{
|
|
Menu* ocio_view_menu = context_menu_widget_->GetViewMenu(&menu);
|
|
menu.addMenu(ocio_view_menu);
|
|
}
|
|
|
|
{
|
|
Menu* ocio_look_menu = context_menu_widget_->GetLookMenu(&menu);
|
|
menu.addMenu(ocio_look_menu);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
{
|
|
// Viewer Zoom Level
|
|
Menu* zoom_menu = new Menu(tr("Zoom"), &menu);
|
|
menu.addMenu(zoom_menu);
|
|
|
|
int zoom_levels[] = {10, 25, 50, 75, 100, 150, 200, 400};
|
|
zoom_menu->addAction(tr("Fit"))->setData(0);
|
|
for (int i=0;i<8;i++) {
|
|
zoom_menu->addAction(tr("%1%").arg(zoom_levels[i]))->setData(zoom_levels[i]);
|
|
}
|
|
|
|
connect(zoom_menu, &QMenu::triggered, this, &ViewerWidget::SetZoomFromMenu);
|
|
}
|
|
|
|
{
|
|
// Full Screen Menu
|
|
Menu* full_screen_menu = new Menu(tr("Full Screen"), &menu);
|
|
menu.addMenu(full_screen_menu);
|
|
|
|
for (int i=0;i<QGuiApplication::screens().size();i++) {
|
|
QScreen* s = QGuiApplication::screens().at(i);
|
|
|
|
QAction* a = full_screen_menu->addAction(tr("Screen %1: %2x%3").arg(QString::number(i),
|
|
QString::number(s->size().width()),
|
|
QString::number(s->size().height())));
|
|
|
|
a->setData(i);
|
|
}
|
|
|
|
connect(full_screen_menu, &QMenu::triggered, this, &ViewerWidget::ContextMenuSetFullScreen);
|
|
}
|
|
|
|
{
|
|
// Deinterlace Option
|
|
if (GetConnectedNode()->video_params().interlacing() != VideoParams::kInterlaceNone) {
|
|
QAction* deinterlace_action = menu.addAction(tr("Deinterlace"));
|
|
deinterlace_action->setCheckable(true);
|
|
deinterlace_action->setChecked(display_widget_->IsDeinterlacing());
|
|
connect(deinterlace_action, &QAction::triggered, display_widget_, &ViewerDisplayWidget::SetDeinterlacing);
|
|
}
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
{
|
|
// Scopes
|
|
Menu* scopes_menu = new Menu(tr("Scopes"), &menu);
|
|
menu.addMenu(scopes_menu);
|
|
|
|
for (int i=0;i<ScopePanel::kTypeCount;i++) {
|
|
QAction* scope_action = scopes_menu->addAction(ScopePanel::TypeToName(static_cast<ScopePanel::Type>(i)));
|
|
scope_action->setData(i);
|
|
}
|
|
|
|
connect(scopes_menu, &Menu::triggered, this, &ViewerWidget::ContextMenuScopeTriggered);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
{
|
|
Menu* cache_menu = new Menu(tr("Cache"), &menu);
|
|
menu.addMenu(cache_menu);
|
|
|
|
// Auto-cache
|
|
QAction* autocache_action = cache_menu->addAction(tr("Auto-Cache"));
|
|
autocache_action->setCheckable(true);
|
|
autocache_action->setChecked(!auto_cacher_.IsPaused());
|
|
connect(autocache_action, &QAction::triggered, this, &ViewerWidget::SetAutoCacheEnabled);
|
|
|
|
cache_menu->addSeparator();
|
|
|
|
// Stop auto-cache while playing
|
|
QAction* pause_autocache_while_playing = cache_menu->addAction(tr("Pause Auto-Cache During Playback"));
|
|
pause_autocache_while_playing->setCheckable(true);
|
|
pause_autocache_while_playing->setChecked(pause_autocache_during_playback_);
|
|
connect(pause_autocache_while_playing, &QAction::triggered, this, [this](bool e){
|
|
pause_autocache_during_playback_ = e;
|
|
});
|
|
|
|
cache_menu->addSeparator();
|
|
|
|
// Cache Entire Sequence
|
|
QAction* cache_entire_sequence = cache_menu->addAction(tr("Cache Entire Sequence"));
|
|
connect(cache_entire_sequence, &QAction::triggered, this, &ViewerWidget::CacheEntireSequence);
|
|
|
|
// Cache In/Out Sequence
|
|
QAction* cache_inout_sequence = cache_menu->addAction(tr("Cache Sequence In/Out"));
|
|
connect(cache_inout_sequence, &QAction::triggered, this, &ViewerWidget::CacheSequenceInOut);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
|
|
{
|
|
// Safe Margins
|
|
Menu* safe_margin_menu = new Menu(tr("Safe Margins"), &menu);
|
|
menu.addMenu(safe_margin_menu);
|
|
|
|
QAction* safe_margin_off = safe_margin_menu->addAction(tr("Off"));
|
|
safe_margin_off->setCheckable(true);
|
|
safe_margin_off->setChecked(!context_menu_widget_->GetSafeMargin().is_enabled());
|
|
connect(safe_margin_off, &QAction::triggered, this, &ViewerWidget::ContextMenuDisableSafeMargins);
|
|
|
|
QAction* safe_margin_on = safe_margin_menu->addAction(tr("On"));
|
|
safe_margin_on->setCheckable(true);
|
|
safe_margin_on->setChecked(context_menu_widget_->GetSafeMargin().is_enabled() && !context_menu_widget_->GetSafeMargin().custom_ratio());
|
|
connect(safe_margin_on, &QAction::triggered, this, &ViewerWidget::ContextMenuSetSafeMargins);
|
|
|
|
QAction* safe_margin_custom = safe_margin_menu->addAction(tr("Custom Aspect"));
|
|
safe_margin_custom->setCheckable(true);
|
|
safe_margin_custom->setChecked(context_menu_widget_->GetSafeMargin().is_enabled() && context_menu_widget_->GetSafeMargin().custom_ratio());
|
|
connect(safe_margin_custom, &QAction::triggered, this, &ViewerWidget::ContextMenuSetCustomSafeMargins);
|
|
}
|
|
|
|
menu.addSeparator();
|
|
}
|
|
|
|
{
|
|
QAction* show_waveform_action = menu.addAction(tr("Show Audio Waveform"));
|
|
show_waveform_action->setCheckable(true);
|
|
show_waveform_action->setChecked(stack_->currentWidget() == waveform_view_);
|
|
show_waveform_action->setEnabled(!ShouldForceWaveform());
|
|
connect(show_waveform_action, &QAction::triggered, this, &ViewerWidget::ManualSwitchToWaveform);
|
|
}
|
|
|
|
menu.exec(static_cast<QWidget*>(sender())->mapToGlobal(pos));
|
|
}
|
|
|
|
void ViewerWidget::Play(bool in_to_out_only)
|
|
{
|
|
if (in_to_out_only) {
|
|
if (GetConnectedTimelinePoints()
|
|
&& GetConnectedTimelinePoints()->workarea()->enabled()) {
|
|
// Jump to in point
|
|
SetTimeAndSignal(Timecode::time_to_timestamp(GetConnectedTimelinePoints()->workarea()->in(), timebase()));
|
|
} else {
|
|
in_to_out_only = false;
|
|
}
|
|
}
|
|
|
|
PlayInternal(1, in_to_out_only);
|
|
}
|
|
|
|
void ViewerWidget::Play()
|
|
{
|
|
Play(false);
|
|
}
|
|
|
|
void ViewerWidget::Pause()
|
|
{
|
|
PauseInternal();
|
|
|
|
auto_cacher_.SetPlayhead(GetTime());
|
|
}
|
|
|
|
void ViewerWidget::ShuttleLeft()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
PauseInternal();
|
|
}
|
|
|
|
current_speed--;
|
|
|
|
if (current_speed == 0) {
|
|
current_speed--;
|
|
}
|
|
|
|
PlayInternal(current_speed, false);
|
|
}
|
|
|
|
void ViewerWidget::ShuttleStop()
|
|
{
|
|
Pause();
|
|
}
|
|
|
|
void ViewerWidget::ShuttleRight()
|
|
{
|
|
int current_speed = playback_speed_;
|
|
|
|
if (current_speed != 0) {
|
|
PauseInternal();
|
|
}
|
|
|
|
current_speed++;
|
|
|
|
if (current_speed == 0) {
|
|
current_speed++;
|
|
}
|
|
|
|
PlayInternal(current_speed, false);
|
|
}
|
|
|
|
void ViewerWidget::SetColorTransform(const ColorTransform &transform)
|
|
{
|
|
SetColorTransform(transform, display_widget_);
|
|
}
|
|
|
|
void ViewerWidget::SetSignalCursorColorEnabled(bool e)
|
|
{
|
|
display_widget_->SetSignalCursorColorEnabled(e);
|
|
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->display_widget()->SetSignalCursorColorEnabled(e);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::TimebaseChangedEvent(const rational &timebase)
|
|
{
|
|
TimeBasedWidget::TimebaseChangedEvent(timebase);
|
|
|
|
controls_->SetTimebase(timebase);
|
|
|
|
controls_->SetTime(ruler()->GetTime());
|
|
LengthChangedSlot(GetConnectedNode() ? GetConnectedNode()->GetLength() : 0);
|
|
}
|
|
|
|
void ViewerWidget::PlaybackTimerUpdate()
|
|
{
|
|
int64_t current_time = playback_timer_.GetTimestampNow();
|
|
|
|
int64_t min_time, max_time;
|
|
|
|
{
|
|
if ((play_in_to_out_only_ || Config::Current()["Loop"].toBool())
|
|
&& GetConnectedTimelinePoints()
|
|
&& GetConnectedTimelinePoints()->workarea()->enabled()) {
|
|
|
|
// If "play in to out" is enabled or we're looping AND we have a workarea, only play the workarea
|
|
min_time = Timecode::time_to_timestamp(GetConnectedTimelinePoints()->workarea()->in(), timebase());
|
|
max_time = Timecode::time_to_timestamp(GetConnectedTimelinePoints()->workarea()->out(), timebase());
|
|
|
|
} else {
|
|
|
|
// Otherwise set the bounds to the range of the sequence
|
|
min_time = 0;
|
|
max_time = Timecode::time_to_timestamp(GetConnectedNode()->GetLength(), timebase());
|
|
|
|
}
|
|
}
|
|
|
|
if ((playback_speed_ < 0 && current_time <= min_time)
|
|
|| (playback_speed_ > 0 && current_time >= max_time)) {
|
|
|
|
// Determine which timestamp we tripped
|
|
int64_t tripped_time;
|
|
|
|
if (current_time <= min_time) {
|
|
tripped_time = min_time;
|
|
} else {
|
|
tripped_time = max_time;
|
|
}
|
|
|
|
if (Config::Current()[QStringLiteral("Loop")].toBool()) {
|
|
|
|
// If we're looping, jump to the other side of the workarea and continue
|
|
int64_t opposing_time = (tripped_time == min_time) ? max_time : min_time;
|
|
|
|
// Cache the current speed
|
|
int current_speed = playback_speed_;
|
|
|
|
// Jump to the other side and keep playing at the same speed
|
|
SetTimeAndSignal(opposing_time);
|
|
PlayInternal(current_speed, play_in_to_out_only_);
|
|
|
|
} else {
|
|
|
|
// Pause at the boundary
|
|
SetTimeAndSignal(tripped_time);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Sets time, wrapping in this bool ensures we don't pause from setting the time
|
|
time_changed_from_timer_ = true;
|
|
SetTimeAndSignal(current_time);
|
|
time_changed_from_timer_ = false;
|
|
|
|
}
|
|
|
|
if (!isVisible()) {
|
|
while (!playback_queue_.empty() && playback_queue_.front().timestamp != GetTime()) {
|
|
PopOldestFrameFromPlaybackQueue();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetViewerResolution(int width, int height)
|
|
{
|
|
sizer_->SetChildSize(width, height);
|
|
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->SetResolution(width, height);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::SetViewerPixelAspect(const rational &ratio)
|
|
{
|
|
sizer_->SetPixelAspectRatio(ratio);
|
|
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->SetPixelAspectRatio(ratio);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::LengthChangedSlot(const rational &length)
|
|
{
|
|
if (last_length_ != length) {
|
|
controls_->SetEndTime(Timecode::time_to_timestamp(length, timebase()));
|
|
UpdateMinimumScale();
|
|
|
|
if (length < last_length_ && GetTime() >= length) {
|
|
ForceUpdate();
|
|
}
|
|
|
|
last_length_ = length;
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::InterlacingChangedSlot(VideoParams::Interlacing interlacing)
|
|
{
|
|
// Automatically set a "sane" deinterlacing option
|
|
display_widget_->SetDeinterlacing(interlacing != VideoParams::kInterlaceNone);
|
|
|
|
foreach (ViewerWindow* vw, windows_) {
|
|
vw->display_widget()->SetDeinterlacing(interlacing != VideoParams::kInterlaceNone);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateRendererVideoParameters()
|
|
{
|
|
display_widget_->SetVideoParams(GetConnectedNode()->video_params());
|
|
foreach (ViewerWindow* window, windows_) {
|
|
window->display_widget()->SetVideoParams(GetConnectedNode()->video_params());
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::UpdateRendererAudioParameters()
|
|
{
|
|
}
|
|
|
|
void ViewerWidget::SetZoomFromMenu(QAction *action)
|
|
{
|
|
sizer_->SetZoom(action->data().toInt());
|
|
}
|
|
|
|
void ViewerWidget::ViewerInvalidatedVideoRange(const TimeRange &range)
|
|
{
|
|
// If our current frame is within this range, we need to update
|
|
if (GetTime() >= range.in() && (GetTime() < range.out() || range.in() == range.out())) {
|
|
QMetaObject::invokeMethod(this, "ForceUpdate", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::ManualSwitchToWaveform(bool e)
|
|
{
|
|
if (e) {
|
|
stack_->setCurrentWidget(waveform_view_);
|
|
} else {
|
|
stack_->setCurrentWidget(sizer_);
|
|
}
|
|
}
|
|
|
|
void ViewerWidget::TimeChangedFromWaveform(qint64 t)
|
|
{
|
|
if (waveform_view_->timebase() != this->timebase()) {
|
|
// Transform time to our timebase
|
|
t = Timecode::rescale_timestamp(t, waveform_view_->timebase(), this->timebase());
|
|
}
|
|
|
|
SetTimeAndSignal(t);
|
|
}
|
|
|
|
void ViewerWidget::ViewerShiftedRange(const rational &from, const rational &to)
|
|
{
|
|
if (GetTime() >= qMin(from, to)) {
|
|
QMetaObject::invokeMethod(this, "ForceUpdate", Qt::QueuedConnection);
|
|
}
|
|
}
|
|
|
|
}
|