Merge branch 'master' into nodeview-redux

This commit is contained in:
itsmattkc
2021-06-16 17:20:05 -07:00
23 changed files with 8608 additions and 5122 deletions
+27 -6
View File
@@ -162,6 +162,28 @@ void AudioVisualWaveform::OverwriteSums(const AudioVisualWaveform &sums, const r
length_ = qMax(length_, dest + length);
}
void AudioVisualWaveform::OverwriteSilence(const rational &start, const rational &length)
{
for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) {
rational rate = it->first;
Sample& our_arr = it->second;
double rate_dbl = rate.toDouble();
// Get our destination sample
int our_start_index = time_to_samples(start, rate_dbl);
int our_length_index = time_to_samples(length, rate_dbl);
int our_end_index = our_start_index + our_length_index;
if (our_arr.size() < our_end_index) {
our_arr.resize(our_end_index);
}
memset(reinterpret_cast<char*>(our_arr.data()) + our_start_index, 0, our_length_index);
}
}
void AudioVisualWaveform::Shift(const rational &from, const rational &to)
{
for (auto it=mipmapped_data_.begin(); it!=mipmapped_data_.end(); it++) {
@@ -184,9 +206,7 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to)
// Shifting backwards <-
int copy_sz = data.size() - from_index;
for (int i=0; i<copy_sz; i++) {
data.replace(to_index + i, data.at(from_index + i));
}
memcpy(&data.data()[to_index], &data.data()[from_index], copy_sz * sizeof(SamplePerChannel));
data.resize(data.size() - (from_index - to_index));
} else {
@@ -199,9 +219,10 @@ void AudioVisualWaveform::Shift(const rational &from, const rational &to)
int copy_sz = old_sz - from_index;
for (int i=0; i<copy_sz; i++) {
data.replace(data.size() - i - 1, data.at(old_sz - i - 1));
}
// Copy to a temporary buffer first to prevent overwriting bytes we need to copy
QByteArray temp(copy_sz * sizeof(SamplePerChannel), Qt::Uninitialized);
memcpy(temp.data(), &data.data()[from_index], temp.size());
memcpy(&data.data()[to_index], temp.data(), temp.size());
memset(reinterpret_cast<char*>(&data[from_index]), 0, distance * sizeof(SamplePerChannel));
}
+2
View File
@@ -88,6 +88,8 @@ public:
*/
void OverwriteSums(const AudioVisualWaveform& sums, const rational& dest, const rational& offset = 0, const rational &length = 0);
void OverwriteSilence(const rational &start, const rational &length);
void Shift(const rational& from, const rational& to);
Sample GetSummaryFromTime(const rational& start, const rational& length) const;
+18 -15
View File
@@ -148,10 +148,14 @@ bool FFmpegDecoder::OpenInternal()
FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const RetrieveVideoParams &params)
{
if (!InitScaler(params)) {
return nullptr;
}
AVStream* s = instance_.avstream();
// Retrieve frame
FFmpegFramePool::ElementPtr return_frame = RetrieveFrame(timecode, params);
FFmpegFramePool::ElementPtr return_frame = RetrieveFrame(timecode);
// We found the frame, we'll return a copy
if (return_frame) {
@@ -161,7 +165,7 @@ FramePtr FFmpegDecoder::RetrieveVideoInternal(const rational &timecode, const Re
native_pix_fmt_,
native_channel_count_,
av_guess_sample_aspect_ratio(instance_.fmt_ctx(), s, nullptr), // May be incorrect,
VideoParams::kInterlaceNone, // May be incorrect
VideoParams::kInterlaceNone,
filter_params_.divider));
copy->set_timestamp(timecode);
copy->allocate();
@@ -182,13 +186,9 @@ void FFmpegDecoder::CloseInternal()
instance_.Close();
}
int FFmpegDecoder::GetFilteredFrame(AVPacket* packet, AVFrame* output_frame, const RetrieveVideoParams& params)
int FFmpegDecoder::GetFilteredFrame(AVPacket* packet, AVFrame* output_frame)
{
// Ensure scaler is correct for these parameters
if (!InitScaler(params)) {
return AVERROR(EINVAL);
}
int ret;
AVFrame* working_frame = av_frame_alloc();
@@ -642,15 +642,18 @@ void FFmpegDecoder::CacheFrameToDisk(AVFrame *f)
void FFmpegDecoder::ClearFrameCache()
{
cached_frames_.clear();
cache_at_eof_ = false;
cache_at_zero_ = false;
if (!cached_frames_.isEmpty()) {
cached_frames_.clear();
cache_at_eof_ = false;
cache_at_zero_ = false;
// Filter graph may rely on "continuous" video frames, so we free the scaler here
FreeScaler();
// Filter graph may rely on "continuous" video frames, so we free the scaler here
FreeScaler();
InitScaler(filter_params_);
}
}
FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, const RetrieveVideoParams &params)
FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time)
{
int64_t target_ts = GetTimeInTimebaseUnits(time, instance_.avstream()->time_base, instance_.avstream()->start_time);
@@ -658,7 +661,7 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, c
int64_t seek_ts = target_ts;
bool still_seeking = false;
if (params.src_interlacing != VideoParams::kInterlaceNone) {
if (filter_params_.src_interlacing != VideoParams::kInterlaceNone) {
// If we are de-interlacing, the timebase is doubled because we get one frame per field, so we
// double the target timestamp too
target_ts *= 2;
@@ -696,7 +699,7 @@ FFmpegFramePool::ElementPtr FFmpegDecoder::RetrieveFrame(const rational& time, c
// Pull from the decoder
av_frame_unref(working_frame);
ret = GetFilteredFrame(pkt, working_frame, params);
ret = GetFilteredFrame(pkt, working_frame);
// Handle any errors that aren't EOF (EOF is handled later on)
if (ret < 0 && ret != AVERROR_EOF) {
+2 -2
View File
@@ -112,7 +112,7 @@ private:
};
int GetFilteredFrame(AVPacket *packet, AVFrame *frame, const RetrieveVideoParams &params);
int GetFilteredFrame(AVPacket *packet, AVFrame *frame);
/**
* @brief Handle an FFmpeg error code
@@ -138,7 +138,7 @@ private:
void ClearFrameCache();
FFmpegFramePool::ElementPtr RetrieveFrame(const rational &time, const RetrieveVideoParams &params);
FFmpegFramePool::ElementPtr RetrieveFrame(const rational &time);
void RemoveFirstFrame();
+1
View File
@@ -30,6 +30,7 @@ namespace olive {
class ExportSubtitlesTab : public QWidget
{
Q_OBJECT
public:
ExportSubtitlesTab(QWidget *parent = nullptr);
+7 -9
View File
@@ -47,14 +47,14 @@ ShaderCode MathNodeBase::GetShaderCodeInternal(const QString &shader_id, const Q
const QString& mat_in = (type_a == NodeValue::kTexture) ? param_b_in : param_a_in;
// No-op frag shader (can we return QString() instead?)
operation = QStringLiteral("texture(%1, ove_texcoord)").arg(tex_in);
operation = QStringLiteral("texture2D(%1, ove_texcoord)").arg(tex_in);
vert = QStringLiteral("uniform mat4 %1;\n"
"\n"
"in vec4 a_position;\n"
"in vec2 a_texcoord;\n"
"attribute vec4 a_position;\n"
"attribute vec2 a_texcoord;\n"
"\n"
"out vec2 ove_texcoord;\n"
"varying vec2 ove_texcoord;\n"
"\n"
"void main() {\n"
" gl_Position = %1 * a_position;\n"
@@ -96,12 +96,10 @@ ShaderCode MathNodeBase::GetShaderCodeInternal(const QString &shader_id, const Q
frag = QStringLiteral("uniform %1 %3;\n"
"uniform %2 %4;\n"
"\n"
"in vec2 ove_texcoord;\n"
"\n"
"out vec4 fragColor;\n"
"varying vec2 ove_texcoord;\n"
"\n"
"void main(void) {\n"
" fragColor = %5;\n"
" gl_FragColor = %5;\n"
"}\n").arg(GetShaderUniformType(type_a),
GetShaderUniformType(type_b),
param_a_in,
@@ -128,7 +126,7 @@ QString MathNodeBase::GetShaderUniformType(const olive::NodeValue::Type &type)
QString MathNodeBase::GetShaderVariableCall(const QString &input_id, const NodeValue::Type &type, const QString& coord_op)
{
if (type == NodeValue::kTexture) {
return QStringLiteral("texture(%1, ove_texcoord%2)").arg(input_id, coord_op);
return QStringLiteral("texture2D(%1, ove_texcoord%2)").arg(input_id, coord_op);
}
return input_id;
+11 -2
View File
@@ -48,6 +48,7 @@ void AudioPlaybackCache::SetParameters(const AudioParams &params)
}
params_ = params;
visual_.set_channel_count(params_.channel_count());
// Restart empty file so there's always "something" to play
ClearPlaylist();
@@ -55,7 +56,7 @@ void AudioPlaybackCache::SetParameters(const AudioParams &params)
emit ParametersChanged();
}
void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const qint64 &job_time)
void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64 &job_time)
{
QList<TimeRange> valid_ranges = GetValidRanges(range, job_time);
if (valid_ranges.isEmpty()) {
@@ -83,6 +84,7 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample
foreach (const TimeRange& r, valid_ranges) {
rational this_segment_in = 0;
// Write PCM to playlist
for (auto it=playlist_.begin(); it!=playlist_.end(); it++) {
rational this_segment_out = this_segment_in + params_.bytes_to_time((*it).size());
@@ -138,6 +140,13 @@ void AudioPlaybackCache::WritePCM(const TimeRange &range, SampleBufferPtr sample
// Each segment is contiguous, so this out will be the next segment's in
this_segment_in = this_segment_out;
}
// Write visual
if (waveform) {
visual_.OverwriteSums(*waveform, r.in(), r.in() - range.in(), r.length());
} else {
visual_.OverwriteSilence(r.in(), r.length());
}
}
foreach (const TimeRange& v, ranges_we_validated) {
@@ -149,7 +158,7 @@ void AudioPlaybackCache::WriteSilence(const TimeRange &range, qint64 job_time)
{
// WritePCM will automatically fill non-existent bytes with silence, so we just have to send
// it an empty sample buffer
WritePCM(range, nullptr, job_time);
WritePCM(range, nullptr, nullptr, job_time);
}
void AudioPlaybackCache::ShiftEvent(const rational &from_in_time, const rational &to_in_time)
+9 -1
View File
@@ -21,6 +21,7 @@
#ifndef AUDIOPLAYBACKCACHE_H
#define AUDIOPLAYBACKCACHE_H
#include "audio/audiovisualwaveform.h"
#include "common/timerange.h"
#include "codec/samplebuffer.h"
#include "render/playbackcache.h"
@@ -65,7 +66,7 @@ public:
void SetParameters(const AudioParams& params);
void WritePCM(const TimeRange &range, SampleBufferPtr samples, const qint64& job_time);
void WritePCM(const TimeRange &range, SampleBufferPtr samples, const AudioVisualWaveform *waveform, const qint64& job_time);
void WriteSilence(const TimeRange &range, qint64 job_time);
@@ -181,6 +182,11 @@ public:
*/
PlaybackDevice* CreatePlaybackDevice(QObject *parent = nullptr) const;
const AudioVisualWaveform &visual() const
{
return visual_;
}
signals:
void ParametersChanged();
@@ -212,6 +218,8 @@ private:
AudioParams params_;
AudioVisualWaveform visual_;
};
}
+5 -4
View File
@@ -39,9 +39,12 @@ namespace olive {
QMutex FrameHashCache::currently_saving_frames_mutex_;
QMap<QByteArray, FramePtr> FrameHashCache::currently_saving_frames_;
const QString FrameHashCache::kCacheFormatExtension = QStringLiteral(".exr");
#define super PlaybackCache
FrameHashCache::FrameHashCache(QObject *parent) :
PlaybackCache(parent)
super(parent)
{
if (DiskManager::instance()) {
connect(DiskManager::instance(), &DiskManager::DeletedFrame, this, &FrameHashCache::HashDeleted);
@@ -419,11 +422,9 @@ QString FrameHashCache::CachePathName(const QByteArray& hash) const
QString FrameHashCache::CachePathName(const QString &cache_path, const QByteArray &hash)
{
QString ext = GetFormatExtension();
QDir cache_dir(QDir(cache_path).filePath(QString(hash.left(1).toHex())));
QString filename = QStringLiteral("%1%2").arg(QString(hash.mid(1).toHex()), ext);
QString filename = QStringLiteral("%1%2").arg(QString(hash.mid(1).toHex()), kCacheFormatExtension);
// Register that in some way this hash has been accessed
QMetaObject::invokeMethod(DiskManager::instance(),
+1 -2
View File
@@ -70,8 +70,6 @@ public:
FramePtr LoadCacheFrame(const QByteArray& hash) const;
static FramePtr LoadCacheFrame(const QString& fn);
static QString GetFormatExtension();
static QVector<rational> GetFrameListFromTimeRange(TimeRangeList range_list, const rational& timebase);
QVector<rational> GetFrameListFromTimeRange(const TimeRangeList &range);
QVector<rational> GetInvalidatedFrames();
@@ -94,6 +92,7 @@ private:
static QMutex currently_saving_frames_mutex_;
static QMap<QByteArray, FramePtr> currently_saving_frames_;
static const QString kCacheFormatExtension;
private slots:
void HashDeleted(const QString &s, const QByteArray& hash);
+4 -1
View File
@@ -171,8 +171,11 @@ void PreviewAutoCacher::AudioRendered()
if (watcher->HasResult()) {
const TimeRange &range = audio_tasks_.value(watcher);
AudioVisualWaveform waveform = watcher->GetTicket()->property("waveform").value<AudioVisualWaveform>();
viewer_node_->audio_playback_cache()->WritePCM(range,
watcher->Get().value<SampleBufferPtr>(),
&waveform,
watcher->GetTicket()->GetJobTime());
bool pcm_is_usable = true;
@@ -506,7 +509,7 @@ void PreviewAutoCacher::TryRender()
if (!invalidated_audio_.isEmpty()) {
foreach (const TimeRange& range, invalidated_audio_) {
std::list<TimeRange> chunks = range.Split(2);
std::list<TimeRange> chunks = range.Split(30);
foreach (const TimeRange& r, chunks) {
RenderTicketWatcher* watcher = new RenderTicketWatcher();
+10 -1
View File
@@ -173,7 +173,16 @@ void RenderProcessor::Run()
table = GenerateTable(texture_output.node(), texture_output.output(), time);
}
ticket_->Finish(table.Get(NodeValue::kSamples));
QVariant sample_variant = table.Get(NodeValue::kSamples);
SampleBufferPtr samples = sample_variant.value<SampleBufferPtr>();
if (samples && ticket_->property("enablewaveforms").toBool()) {
AudioVisualWaveform vis;
vis.set_channel_count(samples->audio_params().channel_count());
vis.OverwriteSamples(samples, samples->audio_params().sample_rate());
ticket_->setProperty("waveform", QVariant::fromValue(vis));
}
ticket_->Finish(sample_variant);
break;
}
case RenderManager::kTypeVideoDownload:
+993 -209
View File
File diff suppressed because it is too large Load Diff
+3034 -2090
View File
File diff suppressed because it is too large Load Diff
+2483 -1771
View File
File diff suppressed because it is too large Load Diff
+1968 -933
View File
File diff suppressed because it is too large Load Diff
+7 -8
View File
@@ -266,8 +266,7 @@ Length: %4</source>
<message>
<location filename="../tool/tool.h" line="108"/>
<source>Bars</source>
<translatorcomment></translatorcomment>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../tool/tool.h" line="110"/>
@@ -282,8 +281,7 @@ Length: %4</source>
<message>
<location filename="../tool/tool.h" line="114"/>
<source>Tone</source>
<translatorcomment></translatorcomment>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../tool/tool.h" line="119"/>
@@ -1425,7 +1423,8 @@ Make sure a sequence is loaded and it has a connected Viewer node.</source>
<message>
<location filename="../dialog/export/exportvideotab.cpp" line="57"/>
<source>Basic</source>
<translation type="unfinished"></translation>
<translatorcomment>Would &quot;&quot; / &quot;&quot; be more appropriate?</translatorcomment>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../dialog/export/exportvideotab.cpp" line="61"/>
@@ -2032,7 +2031,7 @@ Make sure a sequence is loaded and it has a connected Viewer node.</source>
<message>
<location filename="../window/mainwindow/mainmenu.cpp" line="664"/>
<source>Cache Entire Sequence</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../window/mainwindow/mainmenu.cpp" line="665"/>
@@ -2112,7 +2111,7 @@ Make sure a sequence is loaded and it has a connected Viewer node.</source>
<message>
<location filename="../window/mainwindow/mainmenu.cpp" line="685"/>
<source>Enable Snapping</source>
<translation type="unfinished">/</translation>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../window/mainwindow/mainmenu.cpp" line="686"/>
@@ -4369,7 +4368,7 @@ What would you like to do with these clips?</source>
<message>
<location filename="../widget/toolbar/toolbar.cpp" line="108"/>
<source>Toggle Snapping</source>
<translation type="unfinished"></translation>
<translation type="unfinished"></translation>
</message>
</context>
<context>
+16
View File
@@ -1370,6 +1370,8 @@ public:
Track* track = timeline_->GetTrackAt(track_index_);
track->BeginOperation();
bool append = (in_ >= track->track_length());
// Check if the placement location is past the end of the timeline
@@ -1409,6 +1411,12 @@ public:
}
}
track->EndOperation();
if (ripple_remove_command_) {
track->Node::InvalidateCache(TimeRange(insert_->in(), insert_->out()), Track::kBlockInput);
}
for (int i=0; i<position_commands_.size(); i++) {
position_commands_.at(i)->redo();
}
@@ -1422,7 +1430,10 @@ public:
Track* t = timeline_->GetTrackAt(track_index_);
TimeRange insert_range(insert_->in(), insert_->out());
// Firstly, remove our insert
t->BeginOperation();
t->RippleRemoveBlock(insert_);
if (ripple_remove_command_) {
@@ -1432,6 +1443,11 @@ public:
t->RippleRemoveBlock(gap_);
gap_->setParent(&memory_manager_);
}
t->EndOperation();
if (ripple_remove_command_) {
t->Node::InvalidateCache(insert_range, Track::kBlockInput);
}
// Remove tracks if we added them
for (int i=add_track_commands_.size()-1; i>=0; i--) {
+5 -1
View File
@@ -87,7 +87,11 @@ void ImportTool::DragEnter(TimelineViewMouseEvent *event)
PrepGhosts(drag_start_.GetFrame() - parent()->SceneToTime(import_pre_buffer_),
drag_start_.GetTrack().index());
event->accept();
if (parent()->HasGhosts() || !parent()->GetConnectedNode()) {
event->accept();
} else {
event->ignore();
}
} else {
// FIXME: Implement dropping from file
event->ignore();
+3 -47
View File
@@ -60,8 +60,7 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback)
pool_.clear();
pool_.waitForDone();
disconnect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::RenderRange);
//disconnect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::RenderRange);
disconnect(playback_, &AudioPlaybackCache::Validated, this, static_cast<void(AudioWaveformView::*)()>(&AudioWaveformView::update));
SetTimebase(0);
}
@@ -69,14 +68,9 @@ void AudioWaveformView::SetViewer(AudioPlaybackCache *playback)
playback_ = playback;
if (playback_) {
connect(playback_, &AudioPlaybackCache::Validated, this, &AudioWaveformView::RenderRange);
//connect(playback_, &AudioPlaybackCache::ParametersChanged, this, &AudioWaveformView::RenderRange);
connect(playback_, &AudioPlaybackCache::Validated, this, static_cast<void(AudioWaveformView::*)()>(&AudioWaveformView::update));
SetTimebase(playback_->GetParameters().sample_rate_as_time_base());
waveform_.set_channel_count(playback_->GetParameters().channel_count());
RenderRange(TimeRange(0, playback_->GetLength()));
}
}
@@ -101,7 +95,7 @@ void AudioWaveformView::paintEvent(QPaintEvent *event)
// Draw waveform
p.setPen(QColor(64, 255, 160)); // FIXME: Hardcoded color
AudioVisualWaveform::DrawWaveform(&p, rect(), GetScale(), waveform_, SceneToTime(GetScroll()));
AudioVisualWaveform::DrawWaveform(&p, rect(), GetScale(), playback_->visual(), SceneToTime(GetScroll()));
// Draw playhead
p.setPen(PLAYHEAD_COLOR);
@@ -110,42 +104,4 @@ void AudioWaveformView::paintEvent(QPaintEvent *event)
p.drawLine(playhead_x, 0, playhead_x, height());
}
void AudioWaveformView::RenderRange(TimeRange range)
{
// Limit range to length
range = TimeRange(qMax(rational(0), range.in()), qMin(playback_->GetLength(), range.out()));
// Floor to second increments
int64_t start = qFloor(range.in().toDouble());
int64_t end = qCeil(range.out().toDouble());
for (; start!=end; start++) {
TimeRange this_range(start, start+1);
QFutureWatcher<AudioVisualWaveform>* watcher = new QFutureWatcher<AudioVisualWaveform>();
connect(watcher, &QFutureWatcher<AudioVisualWaveform>::finished, this, &AudioWaveformView::BackgroundFinished);
jobs_.insert(this_range, watcher);
watcher->setFuture(QtConcurrent::run(&pool_, GenerateWaveform, playback_->CreatePlaybackDevice(), playback_->GetParameters(), this_range));
}
}
void AudioWaveformView::BackgroundFinished()
{
QFutureWatcher<AudioVisualWaveform>* watcher = static_cast<QFutureWatcher<AudioVisualWaveform>*>(sender());
for (auto it=jobs_.begin(); it!=jobs_.end(); it++) {
if (it.value() == watcher) {
AudioVisualWaveform rendered = watcher->result();
waveform_.OverwriteSums(rendered, it.key().in());
jobs_.erase(it);
update();
break;
}
}
delete watcher;
}
}
-17
View File
@@ -24,7 +24,6 @@
#include <QtConcurrent/QtConcurrent>
#include <QWidget>
#include "audio/audiovisualwaveform.h"
#include "render/audioparams.h"
#include "render/audioplaybackcache.h"
#include "widget/timeruler/seekablewidget.h"
@@ -37,32 +36,16 @@ class AudioWaveformView : public SeekableWidget
public:
AudioWaveformView(QWidget* parent = nullptr);
//void SetData(const QString& file, const AudioRenderingParams& params);
void SetViewer(AudioPlaybackCache *playback);
const AudioVisualWaveform* waveform() const
{
return &waveform_;
}
protected:
virtual void paintEvent(QPaintEvent* event) override;
private:
void RenderRange(TimeRange range);
QThreadPool pool_;
AudioPlaybackCache *playback_;
AudioVisualWaveform waveform_;
QHash<TimeRange, QFutureWatcher<AudioVisualWaveform>*> jobs_;
private slots:
void BackgroundFinished();
};
}
+1 -1
View File
@@ -455,7 +455,7 @@ void ViewerWidget::StartAudioOutput()
AudioManager::instance()->StartOutput(audio_cache,
audio_cache->GetParameters().time_to_bytes(GetTime()),
playback_speed_);
emit AudioManager::instance()->OutputWaveformStarted(waveform_view_->waveform(),
emit AudioManager::instance()->OutputWaveformStarted(&audio_cache->visual(),
GetTime(), playback_speed_);
}
}
+1 -2
View File
@@ -639,8 +639,7 @@ void MainMenu::Retranslate()
// Edit menu
edit_menu_->setTitle(tr("&Edit"));
//edit_undo_item_->setText(tr("&Undo")); FIXME: Does Qt translate these automatically?
//edit_redo_item_->setText(tr("Redo"));
Core::instance()->undo_stack()->UpdateActions(); // Update undo and redo
edit_delete2_item_->setText(tr("Delete (alt)"));
edit_insert_item_->setText(tr("Insert"));
edit_overwrite_item_->setText(tr("Overwrite"));