Initial commit

This commit is contained in:
torsten committed 2026-08-13 18:33:10 +02:00
commit b7d90b9afe
16245 files changed
+3396666

No files matched your search

@@ -0,0 +1,260 @@
/**
* @RDE_HEADER@
* HMI Plugin for AudioVideoManagement - AVS Sequence Model
* $Id$
* $HeadURL$
* $Author$
* Created by Patric Karwat, SLSE, 2017-08-21
*/
#include "avm_sequence_model.h"
#include <QJsonObject>
#include <QColor>
#include <chrono>
namespace AudioVideoManagement
{
AvmSequenceModel::AvmSequenceModel(QObject *parent)
: LtsWidgets::LtsAbstractTableModel<AvsSequence>({QLatin1String("Order"),
tr("Start"),
tr("End"),
tr("Duration [s]"),
tr("Stream"),
tr("Speed"),
tr("Volume"),
tr("Description")},
parent)
, m_logger(getLogger("AvmSequenceModel"))
{}
void AvmSequenceModel::setTimezoneHoursOffset(int timezoneHoursOffset)
{
m_timezoneHoursOffset = timezoneHoursOffset;
emit dataChanged(index(0, Start), index(rowCount()-1, End), {Qt::DisplayRole});
}
void AvmSequenceModel::moveUp(const QModelIndex &idx)
{
// should better be done within moveRows
int row = idx.row();
auto it = std::find_if(m_highlightedRows.begin(), m_highlightedRows.end(), [row](int currentRow)
{
return (row == currentRow);
});
if (it != m_highlightedRows.end())
(*it)--;
moveRows(idx.parent(), idx.row(), 1, idx.parent(), idx.row() - 1);
}
void AvmSequenceModel::moveDown(const QModelIndex &idx)
{
// should better be done within moveRows
int row = idx.row();
auto it = std::find_if(m_highlightedRows.begin(), m_highlightedRows.end(), [row](int currentRow)
{
return (row == currentRow);
});
if (it != m_highlightedRows.end())
(*it)++;
moveRows(idx.parent(), idx.row(), 1, idx.parent(), idx.row() + 1);
}
bool AvmSequenceModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
{
Q_UNUSED(count)
if(sourceRow > m_vecData.size() -1 || sourceRow < 0 ||
destinationChild > m_vecData.size() -1 || destinationChild < 0)
return false;
int moveTo = destinationChild;
if (destinationChild > sourceRow)
moveTo++;
beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, moveTo);
m_vecData.move(sourceRow, destinationChild);
endMoveRows();
return true;
}
static inline std::chrono::milliseconds fromJsonValue(const QJsonValue &val)
{
qint64 d = static_cast<qint64>(val.toDouble());
return std::chrono::milliseconds(d);
}
bool AvmSequenceModel::initModel(const QJsonArray &data)
{
m_highlightedRows.clear();
clear();
AvsSequence seq;
for (const auto &ele : data)
{
const QJsonObject &seqObj = ele.toObject();
if (seqObj.isEmpty())
continue;
seq.internalStreamName = seqObj.value(QStringLiteral("stream")).toString();
seq.speed = seqObj.value(QStringLiteral("speed")).toInt();
seq.volume = seqObj.value(QStringLiteral("volume")).toInt();
seq.start = AvaTimePoint(std::chrono::duration_cast<AvaDuration>(fromJsonValue(seqObj.value(QStringLiteral("start")))));
seq.duration = std::chrono::duration_cast<AvaDuration>(fromJsonValue(seqObj.value(QStringLiteral("duration"))));
seq.pint = seqObj.value(QStringLiteral("pint")).toBool();
seq.description = seqObj.value(QStringLiteral("description")).toString();
insertRow(seq);
}
return true;
}
QJsonArray AvmSequenceModel::getSequencesAsJsonArray() const
{
QJsonArray ret;
if (m_vecData.size() <= 0)
return ret;
for (int i = 0; i < m_vecData.size(); ++i)
{
const AvsSequence &seq = m_vecData.at(i);
QJsonObject seqJson;
seqJson.insert(QLatin1String("pos"), i);
seqJson.insert(QLatin1String("description"), seq.description);
seqJson.insert(QLatin1String("start"), static_cast<qint64>(std::chrono::milliseconds(seq.start.time_since_epoch()).count()));
seqJson.insert(QLatin1String("duration"), static_cast<qint64>(std::chrono::milliseconds(seq.duration).count()));
seqJson.insert(QLatin1String("speed"), static_cast<int>(seq.speed));
seqJson.insert(QLatin1String("stream"), seq.internalStreamName);
seqJson.insert(QLatin1String("volume"), static_cast<int>(seq.volume));
seqJson.insert(QLatin1String("pint"), seq.pint);
ret.append(seqJson);
}
return ret;
}
QString AvmSequenceModel::dateTimeToStringx(const QDateTime &dt) const
{
return QDateTime::fromMSecsSinceEpoch(dt.toMSecsSinceEpoch() + (m_timezoneHoursOffset * 3600 * 1000)).toString(QStringLiteral("dd.MM.yy HH:mm:ss.zzz"));
}
QString AvmSequenceModel::msecsSinceEpochToStringx(qint64 msecs) const
{
return dateTimeToStringx(QDateTime::fromMSecsSinceEpoch(msecs));
}
QVariant AvmSequenceModel::data(const QModelIndex &idx, int role) const
{
if (role != Qt::DisplayRole && role != Qt::BackgroundRole)
return QVariant();
if (!indexIsValid(idx))
return QVariant();
if (role == Qt::DisplayRole)
{
const AvsSequence &seq = m_vecData.at(idx.row());
switch (idx.column())
{
case Order:
return idx.row() + 1;
case Start:
return msecsSinceEpochToStringx(std::chrono::milliseconds(seq.start.time_since_epoch()).count());
case End:
return msecsSinceEpochToStringx(std::chrono::milliseconds((seq.start+seq.duration).time_since_epoch()).count());
case Duration:
{
qint64 duration = std::chrono::milliseconds(seq.duration).count();
qint64 durationDisplay = duration / seq.speed * 100;
qint64 secs = durationDisplay / 1000;
qint64 msecs = (durationDisplay % 1000);
return QStringLiteral("%1.%2").arg(secs).arg(msecs, 3, 10, QLatin1Char('0'));
}
case Description:
return seq.description;
case Streams:
return seq.internalStreamName;
case Volume:
return QVariant(seq.volume);
case Speed:
return QVariant(seq.speed);
}
}
else if (role == Qt::BackgroundRole)
{
if (m_highlightedRows.contains(idx.row()))
return QColor(Qt::green);
}
return QVariant();
}
void AvmSequenceModel::setHighlightedRow(int highlighted, bool reset)
{
QVector<int> oldSelected = m_highlightedRows;
if (highlighted < 0)
m_highlightedRows.clear();
else if (reset)
m_highlightedRows = { highlighted };
else
m_highlightedRows += highlighted;
if (m_highlightedRows == oldSelected)
return; // no row data changed, nothing to do
QVector<int> changedRows;
changedRows.reserve(oldSelected.size() + m_highlightedRows.size());
if (m_highlightedRows.isEmpty() || reset)
for (int row : qAsConst(oldSelected))
changedRows += row;
for (int row : qAsConst(m_highlightedRows))
changedRows += row;
for (int row : qAsConst(changedRows))
emit dataChanged(index(row, 0), index(row, Description), { Qt::BackgroundRole });
}
void AvmSequenceProxyModel::setSourceModel(QAbstractItemModel *sourceModel)
{
m_sourceModel = qobject_cast<AvmSequenceModel*>(sourceModel);
LtsWidgets::LtsColumnFilterProxyModel::setSourceModel(sourceModel);
}
bool AvmSequenceProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
{
if (!m_sourceModel ||
source_left.column() != source_right.column() ||
!source_left.isValid() ||
!source_right.isValid())
return LtsWidgets::LtsColumnFilterProxyModel::lessThan(source_left, source_right);
const auto &dataLeft = m_sourceModel->m_vecData.at(source_left.row());
const auto dataRight = m_sourceModel->m_vecData.at(source_right.row());
switch (source_left.column())
{
case AvmSequenceModel::Start:
return std::chrono::milliseconds(dataLeft.start.time_since_epoch()).count() < std::chrono::milliseconds(dataRight.start.time_since_epoch()).count();
case AvmSequenceModel::End:
return std::chrono::milliseconds((dataLeft.start+dataLeft.duration).time_since_epoch()).count() < std::chrono::milliseconds((dataRight.start+dataLeft.duration).time_since_epoch()).count();
case AvmSequenceModel::Order:
case AvmSequenceModel::Speed:
case AvmSequenceModel::Streams:
case AvmSequenceModel::Volume:
case AvmSequenceModel::Description:
break;
}
return LtsWidgets::LtsColumnFilterProxyModel::lessThan(source_left, source_right);
}
} // namespace AudioVideoManagement
@@ -0,0 +1,86 @@
/**
* @RDE_HEADER@
* HMI Plugin for AudioVideoManagement - AVS Sequence Model
* $Id$
* $HeadURL$
* $Author$
* Created by Patric Karwat, SLSE, 2017-08-21
*/
#pragma once
#include <LtsWidgets/lts_abstract_table_model.h>
#include <LtsWidgets/lts_column_filter_proxy_model.h>
#include <AudioVideoManagement/logger.h>
#include <AudioVideoManagement/ava_types.h>
#include <QJsonArray>
namespace AudioVideoManagement
{
class AvsSequence
{
public:
QString internalStreamName;
AvaSpeed speed{100};
AvaVolume volume{0};
AvaTimePoint start;
AvaDuration duration;
bool pint = false;
QString description;
};
class AvmSequenceModel final : public LtsWidgets::LtsAbstractTableModel<AvsSequence>
{
Q_OBJECT
public:
enum Columns
{
Order = 0,
Start,
End,
Duration,
Streams,
Speed,
Volume,
Description
};
explicit AvmSequenceModel(QObject *parent = nullptr);
void moveUp(const QModelIndex &idx);
void moveDown(const QModelIndex &idx);
bool initModel(const QJsonArray &data);
bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) override;
QJsonArray getSequencesAsJsonArray() const;
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
QVector<int> highlightedRows() const { return m_highlightedRows; }
void setHighlightedRow(int highlighted, bool reset = true);
friend class AvmSequenceProxyModel;
public slots:
void setTimezoneHoursOffset(int timezoneHoursOffset);
private:
QString dateTimeToStringx(const QDateTime &dt) const;
QString msecsSinceEpochToStringx(qint64 msecs) const;
QVector<int> m_highlightedRows;
int m_timezoneHoursOffset = 0;
LOGGER_T m_logger;
};
class AvmSequenceProxyModel final : public LtsWidgets::LtsColumnFilterProxyModel
{
Q_OBJECT
public:
using LtsWidgets::LtsColumnFilterProxyModel::LtsColumnFilterProxyModel;
void setSourceModel(QAbstractItemModel *sourceModel) override;
protected:
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
private:
AvmSequenceModel *m_sourceModel = nullptr;
};
}
@@ -0,0 +1,684 @@
/**
* @RDE_HEADER@
* Topic Editor for the Situation Topic in the Script Editor
* $Id$
* $HeadURL$
* $Author$
*
* Created by Mike Wille, SLSE, 2017-04-04
*/
#include "topic_editor.h"
#include "ui_topic_editor.h"
#include <AudioVideoManagement/av_helper.h>
#include <AudioVideoManagement/ava_comm.h>
#include <AudioVideoManagement/ava_session.h>
#include <AudioVideoManagement/avs_dialog.h>
#include <AudioVideoManagement/Shared/playback_app.h>
#include <LtsSession/lts_session_data.h>
#include <TopicEditorBase/topic_previewer.h>
#include <boost/optional.hpp>
#include <cmath>
#include <QJsonArray>
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
#include <QScopeGuard>
#endif
static constexpr int s_defaultOffset = 10 * 1000; // 10s
namespace AudioVideoManagement
{
QString dateTimeToStringz(const QDateTime &dt)
{
return dt.toString(QStringLiteral("dd.MM.yy HH:mm:ss.zzz"));
}
QString msecsSinceEpochToStringz(qint64 msecs)
{
return dateTimeToStringz(QDateTime::fromMSecsSinceEpoch(msecs));
}
qint64 timePointToMsecs(const AvaTimePoint &atp)
{
return std::chrono::duration_cast<std::chrono::milliseconds>(atp.time_since_epoch()).count();
}
AvaDuration msecsToAvaDuration(qint64 msecs)
{
return std::chrono::duration_cast<AvaDuration>(std::chrono::milliseconds(msecs));
}
AvaTimePoint msecsToAvaTimePoint(qint64 msecs)
{
return AvaTimePoint(msecsToAvaDuration(msecs));
}
TopicEditor::TopicEditor(LtsSession::LtsSession *session, const TopicEditorBase::TopicData &data, QWidget *parent)
: TopicEditorBase::TopicEditor(parent)
, m_session(session)
, m_avmDataIfc(nullptr)
, m_model(new AvmSequenceModel(this))
, m_playbackMenu(nullptr)
, m_ui(new Ui::AVTopicEditor)
, m_logger(getLogger("TopicEditor"))
{
Q_ASSERT(session);
m_ui->setupUi(this);
m_ui->tvSequences->hideColumn(AvmSequenceModel::Order);
init(m_ui->widget);
m_isAudio = (data.topicType == TopicEditorBase::TopicType::Audio);
if (m_isAudio)
setOnlineHelpFilename(QStringLiteral("Script_Editor-Add_Audio_Topic"));
else
setOnlineHelpFilename(QStringLiteral("Script_Editor-Add_Video_Topic"));
setTopicData(data);
m_topicData.properties[QStringLiteral("IsAudio")] = bool(m_topicData.topicType == TopicEditorBase::TopicType::Audio);
m_ui->widget->previewer()->setTopicData(m_topicData);
QJsonArray arr = getTopicVDataAsArray(QLatin1String("Sequences"));
m_model->initModel(arr);
m_ui->tvSequences->resizeColumnsToContents();
refreshDurationText();
m_proxyModel = new AvmSequenceProxyModel(this);
m_proxyModel->setSourceModel(m_model);
m_ui->tvSequences->setModel(m_proxyModel);
connect(m_ui->tvSequences, SIGNAL(doubleClicked(QModelIndex)),
m_ui->tvSequences->selectionModel(), SLOT(select(QModelIndex, QItemSelectionModel::Toggle)));
connect(m_ui->tvSequences->selectionModel(), &QItemSelectionModel::selectionChanged,
this, &TopicEditor::onEnaDisa);
connect(m_ui->pbDelete, &QToolButton::clicked,
this, &TopicEditor::onDeleteSequences);
connect(m_ui->pbUp, &QToolButton::clicked,
this, &TopicEditor::onSequenceUp);
connect(m_ui->pbDown, &QToolButton::clicked,
this, &TopicEditor::onSequenceDown);
connect(m_ui->pbPreview, &QPushButton::clicked, this, [this]()
{
m_playbackMenu->setStreamTime(m_ui->dtStart->msecsSinceEpoch());
});
if (!m_session->getForeignDataInterface_NEW<AudioVideoManagement::DataInterface>(m_avmDataIfc))
{
playbackDlgNotAvailable(tr("The audio/video interface is not available in this session."));
return;
}
PlaybackApp *app = m_isAudio ? m_avmDataIfc->audioPlaybackApp() : m_avmDataIfc->videoPlaybackApp();
if (app == nullptr)
{
playbackDlgNotAvailable(tr("The audio/video interface is currently not available."));
return;
}
if (app->inUse())
{
playbackDlgNotAvailable(tr("The audio/video interface is already in use by another dialog."));
return;
}
// AFTER THIS POINT IT IS GUARANTEED TO HAVE A PLAYBACK DIALOG!
m_ui->pbApply->setEnabled(false);
m_ui->pbDelete->setEnabled(false);
m_ui->pbUp->setEnabled(false);
m_ui->pbDown->setEnabled(false);
// playback menu
boost::optional<QString> stream = boost::none; // will be used as stream preset
if (m_topicData.properties.contains(QStringLiteral("stream")))
stream = m_topicData.properties.value(QStringLiteral("stream")).toString();
// get preset start time from topic (e.g. from evaluation topic)
qint64 startMsecs = 0;
boost::optional<AvaTimePoint> start = boost::none; // will be used as current time preset
{
const QJsonValue jsonValue(getTopicDataValue(QStringLiteral("start")));
if (jsonValue.isDouble())
{
startMsecs = static_cast<qint64>(jsonValue.toDouble());
start = boost::optional<AvaTimePoint>(toTimePoint(startMsecs));
}
}
// get preset end time from topic (e.g. from evaluation topic)
qint64 endMsecs = 0;
{
const QJsonValue jsonValue(getTopicDataValue(QStringLiteral("end")));
if (jsonValue.isDouble())
endMsecs = static_cast<qint64>(jsonValue.toDouble());
}
m_playbackMenu = new AvmPlaybackMenu(this);
m_playbackMenu->init(
m_session, m_avmDataIfc->avaSession(), PlaybackAppHandle(app), m_avmDataIfc->streamNameMapping(), m_isAudio, true /* is inside topic editor */,
stream, start
);
m_ui->playbackParent->layout()->addWidget(m_playbackMenu);
//m_playbackDlg->hideStreamSelection();
connect(m_playbackMenu, &AvmPlaybackMenu::setTopicButtonStates,
this, &TopicEditor::onSetTopicButtonStates);
connect(m_playbackMenu, &AvmPlaybackMenu::markerSelected,
this, &TopicEditor::onMarkerSelected);
connect(m_playbackMenu, &AvmPlaybackMenu::signalCurrentStreamChanged, this, [this](const QString &displayName)
{
m_ui->lblStream->setText(displayName);
});
connect(m_playbackMenu, &AvmPlaybackMenu::signalSetStart, this, [this](qint64 start)
{
m_ui->dtStart->setMsecsSinceEpoch(start);
if (m_ui->dtEnd->msecsSinceEpoch() < start)
m_ui->dtEnd->setMsecsSinceEpoch(start + s_defaultOffset);
});
connect(m_playbackMenu, &AvmPlaybackMenu::signalSetEnd, this, [this](qint64 end)
{
m_ui->dtEnd->setMsecsSinceEpoch(end);
if (m_ui->dtStart->msecsSinceEpoch() > end)
m_ui->dtStart->setMsecsSinceEpoch(end - s_defaultOffset);
});
m_playbackMenu->show();
// NOTE: don't confuse the time *inside* the script timeline with a time since epoch!! following code lines do not make sense:
// // initial values
if (m_topicData.startTime > -1)
{
m_ui->dtStart->setMsecsSinceEpoch(static_cast<qint64>(m_topicData.startTime) * 1000);
if (m_topicData.duration > 0)
m_ui->dtEnd->setMsecsSinceEpoch(static_cast<qint64>(m_topicData.startTime + m_topicData.duration) * 1000);
} // if
// the playback dialog part of the topic editor
connect(m_ui->tvSequences, &QTableView::doubleClicked,
this, &TopicEditor::onEditSequence);
connect(m_ui->dtStart, &LtsWidgets::LtsMSecDateTimeEdit::dateTimeChanged,
this, &TopicEditor::onTimeChanged);
connect(m_ui->dtEnd, &LtsWidgets::LtsMSecDateTimeEdit::dateTimeChanged,
this, &TopicEditor::onTimeChanged);
connect(m_ui->pbAdd, &QToolButton::clicked,
this, &TopicEditor::onAddSequence);
connect(m_ui->pbApply, &QToolButton::clicked,
this, &TopicEditor::onApplySequence);
connect(m_ui->pbEditSequence, &QPushButton::clicked,
this, &TopicEditor::onEditSequence);
// if start and end times were set (e.g. by evaluation topic), then auto set them as start & end time for the new sequence
if (startMsecs > 0 && endMsecs > 0)
{
m_ui->dtStart->setMsecsSinceEpoch(startMsecs);
m_ui->dtEnd->setMsecsSinceEpoch(endMsecs);
}
connect(m_playbackMenu, &AvmPlaybackMenu::signalTimezoneChanged,
m_ui->dtStart, &LtsWidgets::LtsMSecDateTimeEdit::setTimezoneHoursOffset);
connect(m_playbackMenu, &AvmPlaybackMenu::signalTimezoneChanged,
m_ui->dtEnd, &LtsWidgets::LtsMSecDateTimeEdit::setTimezoneHoursOffset);
connect(m_playbackMenu, &AvmPlaybackMenu::signalTimezoneChanged,
m_model, &AvmSequenceModel::setTimezoneHoursOffset);
int offset = m_playbackMenu->currentOffset();
m_model->setTimezoneHoursOffset(offset); // as the emit happens before the commits!
m_ui->dtStart->setTimezoneHoursOffset(offset);
m_ui->dtEnd->setTimezoneHoursOffset(offset);
}
TopicEditor::~TopicEditor()
{
delete m_ui;
delete m_model;
}
///////////////////////////////////////////////////////////////////////////////
// PRIVATE
void TopicEditor::updateInternalData()
{
if (hasTopicDataProperties())
{
m_topicStartTime = getTopicDataValue(QStringLiteral("start")).toDouble();
m_topicStreamName = getTopicDataValue(QStringLiteral("stream")).toString();
QJsonArray arr = getTopicVDataAsArray(QLatin1String("Sequences"));
m_model->initModel(arr);
m_ui->tvSequences->resizeColumnsToContents();
setDurationText(calcTrackLength(arr));
}
m_ui->pbApply->setEnabled(false);
m_ui->pbDelete->setEnabled(false);
}
void TopicEditor::retranslate()
{
// translate(m_ui, trackEntryType() == TopicType::Audio ? tr("Audio Topic Editor") : tr("Video Topic Editor") );
translate(m_ui, m_topicData.topicType == TopicEditorBase::TopicType::Audio ? tr("Audio Topic Editor") : tr("Video Topic Editor") );
m_buttonBox->getButton(LtsWidgets::DialogButton::Apply)->setText(tr("Apply to topic", "Auf Topic übernehmen"));
}
AvsSequence TopicEditor::createSequence(const AvaTimePoint &start, const AvaTimePoint &end) const
{
AvsSequence seq;
seq.start = start;
seq.duration = end - start; // not sure if AVS needs duration or end
seq.description = m_ui->leDescription->text();
if (m_playbackMenu != nullptr)
{
seq.speed = m_playbackMenu->getSpeed();
seq.volume = m_playbackMenu->getVolume();
seq.internalStreamName = m_playbackMenu->getInternalStreamName();
seq.pint = m_playbackMenu->getSkipPause();
}
return seq;
}
AvsSequence TopicEditor::createCurrentSequence() const
{
return createSequence(AvaTimePoint(std::chrono::duration_cast<AvaDuration>(std::chrono::milliseconds(m_ui->dtStart->msecsSinceEpoch()))),
AvaTimePoint(std::chrono::duration_cast<AvaDuration>(std::chrono::milliseconds(m_ui->dtEnd->msecsSinceEpoch()))));
}
void TopicEditor::refreshDurationText()
{
setDurationText(calcTrackLength(m_model->getSequencesAsJsonArray()));
}
void TopicEditor::setDurationText(qint64 length)
{
if (length > s_durationLimit)
m_ui->lblDuration->setText(tr("Playback too long!"));
else
m_ui->lblDuration->setText(QTime::fromMSecsSinceStartOfDay(length).toString(QStringLiteral("HH:mm:ss.zzz")));
}
qint64 TopicEditor::calcTrackLength(const QJsonArray &arr) const
{
qint64 length = 0;
for (const auto &ele : arr)
{
const QJsonObject &seqObj = ele.toObject();
if (seqObj.isEmpty())
continue;
// length += std::ceil(seqObj.value(QLatin1String("duration")).toDouble() * (std::fabs(seqObj.value(QLatin1String("speed")).toDouble(100)) / 100)); Original
length += std::ceil(seqObj.value(QLatin1String("duration")).toDouble() / (std::fabs(seqObj.value(QLatin1String("speed")).toDouble(100))) * 100 );
}
return length;
}
void TopicEditor::playbackDlgNotAvailable(const QString &msg)
{
new LtsWidgets::LtsMessageDialogNew(this, msg);
m_ui->groupBox->hide();
m_ui->line->hide();
// left side:
m_ui->pbEditSequence->hide();
}
///////////////////////////////////////////////////////////////////////////////
// PRIVATE SLOTS
bool TopicEditor::apply2()
{
const QJsonArray &arr = m_model->getSequencesAsJsonArray();
qint64 duration = calcTrackLength(arr);
if (duration > s_durationLimit)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("This topic has exceeded the maximum length of %1 seconds.").arg(s_durationLimit));
return false;
}
setDurationText(duration);
if (LtsSessionData::LtsSessionData::getInstance()->debugFlagValue(QStringLiteral("AVM.AllowIncompleteTopic")) != QVariant(true)) // don't execute code block if AllowIncompleteTopic is set !
{
if (duration <= 0)
{
new LtsWidgets::LtsMessageDialogNew(
this, tr("The total playback length is less than 1 second long. <br>"
"Please verify that you have one or more sequences in the sequences list and that these sequences are of at least 1 second length."),
tr("Playback length too short")
);
return false;
}
qint64 secs = duration / 1000; // strip msecs ...
qint64 msecs = duration - (secs * 1000); // ... and set to zero !
qWarning() << "secs" << secs << "msecs" << msecs;
// a ratio between 500 missing mseconds of a video and 500 msecs of no video output seems balanced
if (msecs >= 500)
++secs;
qWarning() << "secs now" << secs;
m_topicData.duration = secs;
}
else // AllowIncompleteTopic is set! fake it!
m_topicData.duration = 10;
setTopicDataProperty(QStringLiteral("Sequences"), m_model->getSequencesAsJsonArray());
setTopicDataProperty(QStringLiteral("ScriptName"), QString(QStringLiteral("S") + QString::number(m_topicData.id)));
setTopicDataProperty(QStringLiteral("IsAudio"), bool(m_topicData.topicType == TopicEditorBase::TopicType::Audio));
setDescription(m_ui->widget->description());
m_ui->widget->previewer()->setTopicData(m_topicData);
return true;
}
void TopicEditor::onEditSequence()
{
if (!m_playbackMenu)
return;
DEBUG_LOG(m_logger) << "slotEditSequence ";
const auto selRows = m_ui->tvSequences->selectionModel()->selectedRows();
if (selRows.size() != 1)
{
m_model->setHighlightedRow(-1);
return;
}
int row = selRows.at(0).row();
m_model->setHighlightedRow(row);
const AvsSequence selectedSeq = m_model->getRowData(row);
DEBUG_LOG(m_logger) << "selectedSeq.internalStreamName=" << selectedSeq.internalStreamName;
m_playbackMenu->setStreamName(selectedSeq.internalStreamName);
m_ui->dtStart->setMsecsSinceEpoch(std::chrono::milliseconds(selectedSeq.start.time_since_epoch()).count());
m_ui->dtEnd->setMsecsSinceEpoch(std::chrono::milliseconds((selectedSeq.start + selectedSeq.duration).time_since_epoch()).count());
m_ui->leDescription->setText(selectedSeq.description);
m_playbackMenu->setStreamByName(selectedSeq.internalStreamName);
m_playbackMenu->setSpeed(selectedSeq.speed);
m_playbackMenu->setVolume(selectedSeq.volume);
m_playbackMenu->setSkipPause(selectedSeq.pint);
m_playbackMenu->setSavedPos(m_ui->dtStart->msecsSinceEpoch()); // plays that timepoint as soon as the stream was selected and QueryTimeline was successful not working.. : m_playbackDlg->setStreamTime(m_ui->dteStart->msecsSinceEpoch());
m_ui->pbApply->setEnabled(true);
}
void TopicEditor::onTimeChanged()
{
/* user gets a warning on 'add' if times don't add up, otherwise there is too much movement in the end-time dte! */
// LtsWidgets::LtsMSecDateTimeEdit *dtSender = qobject_cast<LtsWidgets::LtsMSecDateTimeEdit*>(sender());
// if (!dtSender)
// return;
// if (dtSender == m_ui->dtEnd)
// {
// if (m_ui->dtEnd->msecsSinceEpoch() < m_ui->dtStart->msecsSinceEpoch())
// {
// qint64 duration = static_cast<qint64>(m_trackEntry->m_length > 0 ? m_trackEntry->m_length : s_defaultOffset) * 1000;
// m_ui->dteStart->setMsecsSinceEpoch(m_ui->dteEnd->msecsSinceEpoch() - duration);
// } // if
// } // if
// else // start is sender
// {
// if (m_ui->dteEnd->msecsSinceEpoch() < m_ui->dteStart->msecsSinceEpoch())
// {
// qint64 duration = static_cast<qint64>(m_trackEntry->m_length > 0 ? m_trackEntry->m_length : s_defaultOffset) * 1000;
// m_ui->dteEnd->setMsecsSinceEpoch(m_ui->dteStart->msecsSinceEpoch() + duration);
// } // if
// } // else
}
void TopicEditor::onApplySequence()
{
const auto seq = createCurrentSequence();
if (seq.start.time_since_epoch().count() == 0 || seq.duration.count() == 0)
return;
const auto rows = m_model->highlightedRows();
if (rows.count() != 1)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("Please select a sequence from the sequence list first, then click 'Edit' to edit a sequence in the editor."));
return;
}
m_model->setRowData(rows.first(), seq);
refreshDurationText();
slotChangesMade();
}
void TopicEditor::onAddSequence()
{
qint64 start = m_ui->dtStart->msecsSinceEpoch();
qint64 end = m_ui->dtEnd->msecsSinceEpoch();
if (start == 0 || end == 0 || end < start)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("Please check if start and end times are set and the end time is not lower than the start time."),
tr("The start and/or end time you entered is invalid"));
return;
}
AvsSequence seq = createCurrentSequence();
if (seq.pint) // if empty parts should be skipped, the duration has to be calculated (as there can be more than 1 skipped / empty part in a sequence)
{
// async!
AvaTimePoint theStart = msecsToAvaTimePoint(m_ui->dtStart->msecsSinceEpoch());
AvaTimePoint theEnd = msecsToAvaTimePoint(m_ui->dtEnd->msecsSinceEpoch());
QueryTimeline *qt = m_playbackMenu->queryTimeline(theStart, theEnd);
if (qt == nullptr)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("Currently unable to request sequences from the audio / video system.<br />Please try again later."));
return;
}
setOverlay(new QWidget());
connect(qt, &QueryTimeline::signalTimelineComplete,
this, [this, theStart, theEnd](const AvaTimeline &timeline)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0)
auto unchangableReset = qScopeGuard([this] { unsetOverlay(); });
#else
std::shared_ptr<int> unchangeableReset(nullptr, [this](int *) { unsetOverlay(); });
#endif
static const QString msgTitle(tr("Sequences needed to be determined for audio / video clips with skipped silence"));
const QVector<AvaTimelinePart> &parts = timeline.parts();
if (parts.isEmpty())
{
new LtsWidgets::LtsMessageDialogNew(this, tr("No sequences could be found inside the specified range. <br />Please select another range that contains audio / video."),
msgTitle);
return;
}
QStringList partsStrings;
for (const AvaTimelinePart &part : parts)
{
AvaTimePoint start = part.first;
AvaTimePoint end = part.second;
if (theStart > start)
start = theStart;
if (theEnd < end)
end = theEnd;
qint64 duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
qint64 secs = duration / 1000;
qint64 msecs = (duration % 1000);
QString durationString(QStringLiteral("%1.%2").arg(secs).arg(msecs, 3, 10, QLatin1Char('0')));
partsStrings += QStringLiteral("<span>%1 - %2 (%3 %4)</span>").arg(msecsSinceEpochToStringz(std::chrono::milliseconds(start.time_since_epoch()).count()),
msecsSinceEpochToStringz(std::chrono::milliseconds(end.time_since_epoch()).count()),
durationString,
tr("seconds"));
}
// Patrick said the message is not needed
//#define AskUserSequenceList
#ifdef AskUserSequenceList
auto msg = new LtsWidgets::LtsMessageDialogNew(this, tr("The following sequences were determined: <br /> <br /> %1 <br /> <br />Do you want to add the listed sequences to the sequences list?").arg(partsStrings.join(QStringLiteral("<br />"))),
msgTitle, LtsWidgets::Behaviour::Modal, LtsWidgets::Button::Yes | LtsWidgets::Button::Cancel, { { LtsWidgets::Button::Yes, tr("Add") } });
connect(msg, &LtsWidgets::LtsMessageDialogNew::yes,
this, [this, parts, theStart, theEnd]()
#endif
{
m_model->setHighlightedRow(-1); // initial highlight reset
for (const AvaTimelinePart &part : parts)
{
AvaTimePoint start = part.first;
AvaTimePoint end = part.second;
if (theStart > start)
start = theStart;
if (theEnd < end)
end = theEnd;
m_model->insertRow(createSequence(start, end));
int row = m_model->rowCount() - 1;
QModelIndex idx = m_model->index(row, 0);
if (!idx.isValid())
{
new LtsWidgets::LtsMessageDialogNew(this, tr("One or more sequences could not be added to the sequences list. <br />Please try again later."),
msgTitle);
return;
}
m_model->setHighlightedRow(row, false /* add, don't reset after adding 1 of n rows */);
m_ui->tvSequences->setCurrentIndex(idx);
}
refreshDurationText();
m_ui->pbApply->setEnabled(true); // new seq now editable
slotChangesMade();
}
#ifdef AskUserSequenceList
);
#endif
});
return;
} // if (seq.pint)
if (seq.start.time_since_epoch().count() == 0 || seq.duration.count() == 0)
{
new LtsWidgets::LtsMessageDialogNew(this, tr("Start and end times are not valid. Please check if your input values are valid."));
return;
}
m_model->insertRow(seq);
int row = m_model->rowCount() - 1;
QModelIndex idx = m_model->index(row, 0);
if (!idx.isValid())
return;
m_model->setHighlightedRow(row);
m_ui->tvSequences->setCurrentIndex(idx);
refreshDurationText();
m_ui->pbApply->setEnabled(true); // new seq now editable
slotChangesMade();
}
void TopicEditor::onDeleteSequences()
{
const auto selRows = m_ui->tvSequences->selectionModel()->selectedRows();
QVector<int> rowIds;
rowIds.reserve(selRows.size());
for (const QModelIndex &idx : selRows)
rowIds += idx.row();
std::sort(rowIds.begin(), rowIds.end(), std::greater<int>());
const auto rows = m_model->highlightedRows();
int curEditedRow = -1;
if (rows.count() == 1)
curEditedRow = rows.first();
for (int row : qAsConst(rowIds))
{
m_model->removeRow(row);
if (curEditedRow == row)
{
curEditedRow = -1;
m_model->setHighlightedRow(curEditedRow);
m_ui->pbApply->setEnabled(false);
onEnaDisa();
}
if (curEditedRow > row)
--curEditedRow;
}
m_model->setHighlightedRow(curEditedRow);
refreshDurationText();
slotChangesMade();
}
void TopicEditor::onSequenceUp()
{
m_model->moveUp(m_ui->tvSequences->currentIndex());
slotChangesMade();
}
void TopicEditor::onSequenceDown()
{
m_model->moveDown(m_ui->tvSequences->currentIndex());
slotChangesMade();
}
void TopicEditor::onSetTopicButtonStates(bool enable)
{
if (enable && m_topicStartTime)
m_ui->dtStart->setMsecsSinceEpoch(m_topicStartTime);
}
void TopicEditor::onEnaDisa()
{
const auto selRows = m_ui->tvSequences->selectionModel()->selectedRows();
const bool hasSelection = (selRows.size() != 0);
const bool hasSingleSelection = (selRows.size() == 1);
m_ui->pbEditSequence->setEnabled(hasSingleSelection);
m_ui->pbDelete->setEnabled(hasSelection);
m_ui->pbDown->setEnabled(hasSingleSelection);
m_ui->pbUp->setEnabled(hasSingleSelection);
}
void TopicEditor::onMarkerSelected(const QString &description)
{
m_ui->leDescription->setText(description);
}
} // AudioVideoManagement FileManagement
@@ -0,0 +1,80 @@
/**
* @RDE_HEADER@
* Topic Editor for the Situation Topic in the Script Editor
* $Id$
* $HeadURL$
* $Author$
*
* Created by Mike Wille, SLSE, 2017-04-04
*/
#pragma once
#include "avm_sequence_model.h"
#include "../avm_playback_menu.h"
#include <AudioVideoManagement/ava_control.h>
#include <AudioVideoManagement/logger.h>
#include <LtsSession/lts_session.h>
#include <Plugins/AudioVideoManagement/avm_data_plugin_interface.h>
#include <TopicEditorBase/topic_editor_base.h>
namespace Ui { class AVTopicEditor; }
namespace AudioVideoManagement
{
class TopicEditor : public TopicEditorBase::TopicEditor
{
Q_OBJECT
public:
explicit TopicEditor(LtsSession::LtsSession *session, const TopicEditorBase::TopicData &data, QWidget *parent = nullptr);
~TopicEditor();
private:
void updateInternalData() override final;
void retranslate() override final;
AvsSequence createSequence(const AvaTimePoint &start, const AvaTimePoint &end) const;
AvsSequence createCurrentSequence() const;
void refreshDurationText();
void setDurationText(qint64 length);
qint64 calcTrackLength(const QJsonArray &arr) const;
void playbackDlgNotAvailable(const QString &msg);
private slots:
bool apply2() override final;
void onEditSequence();
void onTimeChanged();
void onApplySequence();
void onAddSequence();
void onDeleteSequences();
void onSequenceUp();
void onSequenceDown();
void onSetTopicButtonStates(bool enable);
void onEnaDisa();
void onMarkerSelected(const QString &description);
private:
const int s_durationLimit{86400000};
LtsSession::LtsSession *m_session{nullptr};
std::shared_ptr<AudioVideoManagement::DataInterface> m_avmDataIfc{nullptr};
AvmSequenceModel *m_model{nullptr};
AvmSequenceProxyModel *m_proxyModel{nullptr};
AvmPlaybackMenu *m_playbackMenu{nullptr};
bool m_isAudio{false};
qint64 m_topicStartTime{0};
QString m_topicStreamName;
Ui::AVTopicEditor *m_ui{nullptr};
NGL::INGLLogger &m_logger;
};
} // namespace AudioVideoManagement
@@ -0,0 +1,457 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AVTopicEditor</class>
<widget class="QWidget" name="AVTopicEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1150</width>
<height>577</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string notr="true">Script Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="1" rowspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="lbxSequences">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string>Sequences:</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lbxTopicDuration">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string>Topic duration:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="lblDuration">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string notr="true">0</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_3">
<item row="1" column="3">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="3">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="3">
<widget class="QToolButton" name="pbDown">
<property name="toolTip">
<string>Sequence Down</string>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/sort-down.svg</normaloff>:/style/sort-down.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QToolButton" name="pbUp">
<property name="text">
<string notr="true"/>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/sort-up.svg</normaloff>:/style/sort-up.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
</widget>
</item>
<item row="5" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="pbDelete">
<property name="toolTip">
<string>Delete selected sequence from list</string>
</property>
<property name="text">
<string>Delete sequence</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/delete.svg</normaloff>:/style/delete.svg</iconset>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QPushButton" name="pbEditSequence">
<property name="toolTip">
<string>Edit selected sequence from list in the right side of the layout</string>
</property>
<property name="text">
<string>Edit sequence</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/document-edit.svg</normaloff>:/style/document-edit.svg</iconset>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="4" colspan="3">
<widget class="QTableView" name="tvSequences">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="TopicEditorBase::TopicPreviewWidget" name="widget" native="true"/>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="2" rowspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Edit / Create sequence</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,0,0,0">
<item>
<widget class="QWidget" name="playbackParent" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
<item>
<widget class="Line" name="line_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="lbxStrean">
<property name="text">
<string>Stream:</string>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QLabel" name="lbxEndTime">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string>End Time:</string>
</property>
</widget>
</item>
<item row="1" column="8">
<widget class="QPushButton" name="pbPreview">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Set as current time</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QLabel" name="lbxDescription">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbxStartTime">
<property name="toolTip">
<string notr="true"/>
</property>
<property name="text">
<string>Start Time:</string>
</property>
</widget>
</item>
<item row="1" column="6">
<widget class="LtsWidgets::LtsMSecDateTimeEdit" name="dtEnd"/>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLabel" name="lblStream">
<property name="text">
<string notr="true">-</string>
</property>
</widget>
</item>
<item row="1" column="7">
<spacer name="horizontalSpacer_7">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="5" colspan="4">
<widget class="QLineEdit" name="leDescription"/>
</item>
<item row="1" column="3">
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="2">
<widget class="LtsWidgets::LtsMSecDateTimeEdit" name="dtStart"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pbApply">
<property name="text">
<string>Apply changes to sequence in list</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/dialog-ok-apply.svg</normaloff>:/style/dialog-ok-apply.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pbAdd">
<property name="text">
<string>Append as new sequence to list</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/style/document-new.svg</normaloff>:/style/document-new.svg</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>TopicEditorBase::TopicPreviewWidget</class>
<extends>QWidget</extends>
<header>TopicEditorBase/topic_preview_widget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>LtsWidgets::LtsMSecDateTimeEdit</class>
<extends>QDateTimeEdit</extends>
<header>LtsWidgets/lts_msec_date_time_edit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>tvSequences</tabstop>
<tabstop>pbUp</tabstop>
<tabstop>pbDown</tabstop>
<tabstop>pbDelete</tabstop>
<tabstop>pbEditSequence</tabstop>
<tabstop>leDescription</tabstop>
<tabstop>pbPreview</tabstop>
<tabstop>pbApply</tabstop>
<tabstop>pbAdd</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>