Commit 234b8fdf authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Merge branch 'sassycode/kdenlive-subtitler'

parents c4539f47 271a8906
Pipeline #39620 canceled with stage
......@@ -8,6 +8,7 @@ set(kdenlive_SRCS
bin/filewatcher.cpp
bin/generators/generators.cpp
bin/model/markerlistmodel.cpp
bin/model/subtitlemodel.cpp
bin/projectclip.cpp
bin/projectfolder.cpp
bin/projectitemmodel.cpp
......
This diff is collapsed.
#ifndef SUBTITLEMODEL_HPP
#define SUBTITLEMODEL_HPP
#include "bin/bin.h"
#include "definitions.h"
#include "gentime.h"
#include "undohelper.hpp"
#include <QAbstractListModel>
#include <QReadWriteLock>
#include <array>
#include <map>
#include <memory>
#include <mlt++/MltProperties.h>
#include <mlt++/Mlt.h>
class DocUndoStack;
class SnapInterface;
class AssetParameterModel;
/* @brief This class is the model for a list of subtitles.
*/
class SubtitleModel:public QAbstractListModel
{
Q_OBJECT
public:
/* @brief Construct a subtitle list bound to the timeline */
explicit SubtitleModel(Mlt::Tractor *tractor = nullptr, QObject *parent = nullptr);
enum { SubtitleRole = Qt::UserRole + 1, StartPosRole, EndPosRole, StartFrameRole, EndFrameRole };
/** @brief Function that parses through a subtitle file */
void addSubtitle(GenTime start,GenTime end, QString &str);
/** @brief Converts string of time to GenTime */
GenTime stringtoTime(QString &str);
/** @brief Return model data item according to the role passed */
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;// overide the same function of QAbstractListModel
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
/** @brief Returns all subtitles in the model */
QList<SubtitledTime> getAllSubtitles() const;
/** @brief Registers a snap model to the subtitle model */
void registerSnap(const std::weak_ptr<SnapInterface> &snapModel);
/** @brief Edit subtitle end timing
@param startPos is start timing position of subtitles
@param oldPos is the old position of the end time
@param pos defines the new position of the end time
*/
void editEndPos(GenTime startPos, GenTime newEndPos);
/** @brief Edit subtitle , i.e. text and/or end time
@param startPos is start timing position of subtitles
@param newSubtitleText is (new) subtitle text
@param endPos defines the (new) position of the end time
*/
void editSubtitle(GenTime startPos, QString newSubtitleText, GenTime endPos);
/** @brief Remove subtitle at start position (pos) */
void removeSubtitle(GenTime pos);
/** @brief Move an existing subtitle
@param oldPos is original start position of subtitle
@param newPos is new start position of subtitle
*/
void moveSubtitle(GenTime oldPos, GenTime newPos);
/** @brief Exports the subtitle model to json */
QString toJson();
public slots:
/** @brief Function that parses through a subtitle file */
void parseSubtitle();
/** @brief Import model to a temporary subtitle file to which the Subtitle effect is applied*/
void jsontoSubtitle(const QString &data);
private:
std::weak_ptr<DocUndoStack> m_undoStack;
std::map<GenTime, std::pair<QString, GenTime>> m_subtitleList;
QString scriptInfoSection="", styleSection = "",eventSection="";
QString styleName="";
QString m_subFilePath;
//To get subtitle file from effects parameter:
//std::unique_ptr<Mlt::Properties> m_asset;
//std::shared_ptr<AssetParameterModel> m_model;
std::vector<std::weak_ptr<SnapInterface>> m_regSnaps;
mutable QReadWriteLock m_lock;
std::unique_ptr<Mlt::Filter> m_subtitleFilter;
Mlt::Tractor *m_tractor;
signals:
void modelChanged();
protected:
/** @brief Helper function that retrieves a pointer to the subtitle model*/
static std::shared_ptr<SubtitleModel> getModel();
/** @brief Add start time as snap in the registered snap model */
void addSnapPoint(GenTime startpos);
/** @brief Connect changes in model with signal */
void setup();
};
Q_DECLARE_METATYPE(SubtitleModel *)
#endif // SUBTITLEMODEL_HPP
......@@ -203,3 +203,61 @@ PlaylistState::ClipState stateFromBool(std::pair<bool, bool> av)
return PlaylistState::Disabled;
}
}
SubtitledTime::SubtitledTime()
: m_starttime(0)
, m_endtime(0)
{
}
SubtitledTime::SubtitledTime(const GenTime &start, QString sub, const GenTime &end)
: m_starttime(start)
, m_subtitle(sub)
, m_endtime(end)
{
}
QString SubtitledTime::subtitle()
{
return m_subtitle;
}
GenTime SubtitledTime::start()
{
return m_starttime;
}
GenTime SubtitledTime::end()
{
return m_endtime;
}
void SubtitledTime::setSubtitle(const QString& sub)
{
m_subtitle = sub;
}
void SubtitledTime::setEndTime(const GenTime& end)
{
m_endtime = end;
}
bool SubtitledTime::operator>(const SubtitledTime& op) const
{
return(m_starttime > op.m_starttime && m_endtime > op.m_endtime && m_starttime > op.m_endtime);
}
bool SubtitledTime::operator<(const SubtitledTime& op) const
{
return(m_starttime < op.m_starttime && m_endtime < op.m_endtime && m_endtime < op.m_starttime);
}
bool SubtitledTime::operator==(const SubtitledTime& op) const
{
return(m_starttime == op.m_starttime && m_endtime == op.m_endtime);
}
bool SubtitledTime::operator!=(const SubtitledTime& op) const
{
return(m_starttime != op.m_starttime && m_endtime != op.m_endtime);
}
......@@ -266,6 +266,34 @@ private:
int m_type{0};
};
class SubtitledTime
{
public:
SubtitledTime();
SubtitledTime(const GenTime &start, QString sub, const GenTime &end);
QString subtitle();
GenTime start();
GenTime end();
void setSubtitle(const QString &sub);
void setEndTime(const GenTime &end);
/* Implementation of > operator; Works identically as with basic types. */
bool operator>(const SubtitledTime &op) const;
/* Implementation of < operator; Works identically as with basic types. */
bool operator<(const SubtitledTime &op) const;
/* Implementation of == operator; Works identically as with basic types. */
bool operator==(const SubtitledTime &op) const;
/* Implementation of != operator; Works identically as with basic types. */
bool operator!=(const SubtitledTime &op) const;
private:
GenTime m_starttime;
QString m_subtitle;
GenTime m_endtime;
};
QDebug operator<<(QDebug qd, const ItemInfo &info);
// we provide hash function for qstring and QPersistentModelIndex
......
......@@ -23,6 +23,7 @@
#include "bin/binplaylist.hpp"
#include "bin/clipcreator.hpp"
#include "bin/model/markerlistmodel.hpp"
#include "bin/model/subtitlemodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectitemmodel.h"
#include "core.h"
......@@ -88,7 +89,7 @@ KdenliveDoc::KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *und
connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified);
connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack, Qt::DirectConnection);
// connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool)));
// init default document properties
m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('8');
m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1');
......@@ -1294,6 +1295,7 @@ void KdenliveDoc::loadDocumentProperties()
if (pl.isNull()) {
return;
}
//QMetaObject::invokeMethod(m_subtitleModel.get(), "parseSubtitle", Qt::QueuedConnection);
QDomNodeList props = pl.elementsByTagName(QStringLiteral("property"));
QString name;
QDomElement e;
......@@ -1771,3 +1773,23 @@ int KdenliveDoc::audioChannels() const
QString& KdenliveDoc::modifiedDecimalPoint() {
return m_modifiedDecimalPoint;
}
std::shared_ptr<SubtitleModel> KdenliveDoc::getSubtitleModel() const
{
return m_subtitleModel;
}
void KdenliveDoc::subtitlesChanged()
{
//m_subtitleModel->parseSubtitle();
m_subtitleModel->jsontoSubtitle(m_subtitleModel->toJson()); //Update subtitle file everytime the subtitle model is changed
return;
}
void KdenliveDoc::initializeSubtitles(const std::shared_ptr<SubtitleModel> m_subtitle)
{
m_subtitleModel = m_subtitle;
connect(m_subtitleModel.get(), &SubtitleModel::modelChanged, this, &KdenliveDoc::subtitlesChanged);
m_subtitleModel->parseSubtitle();
//QMetaObject::invokeMethod(m_subtitle.get(), "parseSubtitle", Qt::QueuedConnection);
}
\ No newline at end of file
......@@ -33,6 +33,7 @@
#include <qdom.h>
#include <kautosavefile.h>
#include "../bin/model/subtitlemodel.hpp"
#include "definitions.h"
#include "gentime.h"
......@@ -44,6 +45,7 @@ class ProjectClip;
class MarkerListModel;
class Render;
class ProfileParam;
class SubtitleModel;
class QUndoGroup;
class QUndoCommand;
......@@ -162,11 +164,16 @@ public:
/** @brief Returns the number of audio channels for this project */
int audioChannels() const;
/**
* If the document used a decimal point different than “.”, it is stored in this property.
* @return Original decimal point, or an empty string if it was “.” already
*/
QString &modifiedDecimalPoint();
/** @brief Returns a pointer to the subtitle model */
std::shared_ptr<SubtitleModel> getSubtitleModel() const;
/** @brief Initialize and connect subtitle model */
void initializeSubtitles(const std::shared_ptr<SubtitleModel> m_subtitle);
private:
QUrl m_url;
......@@ -197,6 +204,7 @@ private:
QMap<QString, QString> m_documentProperties;
QMap<QString, QString> m_documentMetadata;
std::shared_ptr<MarkerListModel> m_guideModel;
std::shared_ptr<SubtitleModel> m_subtitleModel;
QString m_modifiedDecimalPoint;
......@@ -232,6 +240,8 @@ public slots:
void slotAutoSave(const QString &scene);
/** @brief Groups were changed, save to MLT. */
void groupsChanged(const QString &groups);
/** @brief Subtitles were changed, update subtitle file */
void subtitlesChanged();
private slots:
void slotModified();
......@@ -264,4 +274,4 @@ signals:
void updateCompositionMode(int);
};
#endif
#endif
\ No newline at end of file
......@@ -282,6 +282,8 @@
<Separator />
<Action name="timeline_preview_button" />
<Action name="audiomixer_button" />
<Separator />
<Action name="subtitle_tool" />
</ToolBar>
<ToolBar name="extraToolBar" >
<text>Extra Toolbar</text>
......
......@@ -21,6 +21,7 @@
#include "assets/assetpanel.hpp"
#include "bin/clipcreator.hpp"
#include "bin/generators/generators.h"
#include "bin/model/subtitlemodel.hpp"
#include "bin/projectclip.h"
#include "bin/projectfolder.h"
#include "bin/projectitemmodel.h"
......@@ -583,6 +584,7 @@ void MainWindow::init()
timelineRulerMenu->addAction(actionCollection()->action(QStringLiteral("edit_guide")));
timelineRulerMenu->addMenu(guideMenu);
timelineRulerMenu->addAction(actionCollection()->action(QStringLiteral("add_project_note")));
timelineRulerMenu->addAction(actionCollection()->action(QStringLiteral("add_subtitle")));
// Timeline headers menu
QMenu *timelineHeadersMenu = new QMenu(this);
......@@ -1117,6 +1119,12 @@ void MainWindow::setupActions()
m_timeFormatButton->setToolButtonPopupMode(QToolButton::InstantPopup);
addAction(QStringLiteral("timeline_timecode"), m_timeFormatButton);
m_buttonSubtitleEditTool = new QAction(QIcon::fromTheme(QStringLiteral("input-keyboard")), i18n("Edit Subtitle tool"), this);
m_buttonSubtitleEditTool->setCheckable(true);
m_buttonSubtitleEditTool->setChecked(false);
addAction(QStringLiteral("subtitle_tool"), m_buttonSubtitleEditTool);
connect(m_buttonSubtitleEditTool, &QAction::triggered, this, &MainWindow::slotEditSubtitle);
// create tools buttons
m_buttonSelectTool = new QAction(QIcon::fromTheme(QStringLiteral("cursor-arrow")), i18n("Selection tool"), this);
// toolbar->addAction(m_buttonSelectTool);
......@@ -1692,6 +1700,7 @@ void MainWindow::setupActions()
addAction(QStringLiteral("edit_guide"), i18n("Edit Guide"), this, SLOT(slotEditGuide()), QIcon::fromTheme(QStringLiteral("document-properties")));
addAction(QStringLiteral("delete_all_guides"), i18n("Delete All Guides"), this, SLOT(slotDeleteAllGuides()),
QIcon::fromTheme(QStringLiteral("edit-delete")));
addAction(QStringLiteral("add_subtitle"), i18n("Add Subtitle"), this, SLOT(slotAddSubtitle()), QIcon::fromTheme(QStringLiteral("list-add")), Qt::SHIFT +Qt::Key_S);
m_saveAction = KStandardAction::save(pCore->projectManager(), SLOT(saveFile()), actionCollection());
m_saveAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
......@@ -4163,6 +4172,19 @@ void MainWindow::slotActivateTarget()
}
}
void MainWindow::slotEditSubtitle()
{
std::shared_ptr<SubtitleModel> m_subtitleModel;
m_subtitleModel.reset(new SubtitleModel(getMainTimeline()->controller()->tractor(),this));
pCore->currentDoc()->initializeSubtitles(m_subtitleModel);
getMainTimeline()->connectSubtitleModel();
}
void MainWindow::slotAddSubtitle()
{
getCurrentTimeline()->controller()->addSubtitle();
}
#ifdef DEBUG_MAINW
#undef DEBUG_MAINW
#endif
......@@ -235,6 +235,7 @@ private:
QAction *m_playZone;
QAction *m_loopClip;
QAction *m_proxyClip;
QAction *m_buttonSubtitleEditTool;
QString m_theme;
KIconLoader *m_iconLoader;
KToolBar *m_timelineToolBar;
......@@ -507,6 +508,8 @@ private slots:
void slotActivateVideoTrackSequence();
/** @brief Select target for current track */
void slotActivateTarget();
void slotEditSubtitle();
void slotAddSubtitle();
signals:
Q_SCRIPTABLE void abortRenderJob(const QString &url);
......
......@@ -27,6 +27,7 @@ the Free Software Foundation, either version 3 of the License, or
// Temporary for testing
#include "bin/model/markerlistmodel.hpp"
#include "bin/model/subtitlemodel.hpp"
#include "profiles/profilerepository.hpp"
#include "project/notesplugin.h"
......@@ -1086,3 +1087,8 @@ void ProjectManager::addAudioTracks(int tracksCount)
{
pCore->window()->getMainTimeline()->controller()->addTracks(0, tracksCount);
}
std::shared_ptr<SubtitleModel> ProjectManager::getSubtitleModel()
{
return current()->getSubtitleModel();
}
\ No newline at end of file
......@@ -30,6 +30,7 @@ class KAutoSaveFile;
class KJob;
class KdenliveDoc;
class MarkerListModel;
class SubtitleModel;
class NotesPlugin;
class Project;
class QAction;
......@@ -97,6 +98,11 @@ public:
*/
void addAudioTracks(int tracksCount);
/** @brief Return the current Subtitle Model
The method is virtual to allow mocking
*/
virtual std::shared_ptr<SubtitleModel> getSubtitleModel();
public slots:
void newFile(QString profileName, bool showProjectSettings = true);
void newFile(bool showProjectSettings = true);
......
......@@ -307,6 +307,7 @@ Rectangle {
property double dar: 16/9
property bool paletteUnchanged: true
property int maxLabelWidth: 20 * root.baseUnit * Math.sqrt(root.timeScale)
property bool showSubtitles: false
onSeekingFinishedChanged : {
playhead.opacity = seekingFinished ? 1 : 0.5
......@@ -703,6 +704,7 @@ Rectangle {
}
Column {
id: trackHeaders
y: subtitleTrack.height
spacing: 0
Repeater {
id: trackHeaderRepeater
......@@ -1027,6 +1029,7 @@ Rectangle {
// It is important that these are not part of the track visual hierarchy;
// otherwise, the clips will be obscured by the Track's background.
Column {
y: subtitleTrack.height
topPadding: -scrollView.contentY
Repeater {
model: multitrack
......@@ -1068,10 +1071,17 @@ Rectangle {
//Component.onCompleted: contentItem.interactive = false
contentWidth: tracksContainerArea.width
contentHeight: tracksContainerArea.height
Item {
id: subtitleTrack
width: tracksContainerArea.width
height: showSubtitles? 50 : 0
Repeater { id: subtitlesRepeater; model: subtitleDelegateModel }
}
Item {
id: tracksContainerArea
width: Math.max(scrollView.width - vertScroll.width, timeline.fullDuration * timeScale)
height: trackHeaders.height
y: subtitleTrack.height
//Math.max(trackHeaders.height, scrollView.contentHeight - scrollView.__horizontalScrollBar.height)
//color: root.color
Item {
......@@ -1273,15 +1283,15 @@ Rectangle {
}
Repeater { id: guidesRepeater; model: guidesDelegateModel }
}
Rectangle {
id: cursor
visible: root.consumerPosition > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: parent.height
x: root.consumerPosition * timeline.scaleFactor
}
}
Rectangle {
id: cursor
visible: root.consumerPosition > -1
color: root.textColor
width: Math.max(1, 1 * timeline.scaleFactor)
opacity: (width > 2) ? 0.5 : 1
height: tracksContainerArea.height + subtitleTrack.height
x: root.consumerPosition * timeline.scaleFactor
}
}
}
......@@ -1457,6 +1467,255 @@ Rectangle {
}
DelegateModel {
id: subtitleDelegateModel
model: subtitleModel
Item {
id: subtitleRoot
visible : true
height: parent.height
z: 20
property real duration : (model.endframe - model.startframe)
property int oldStartX
property double oldStartFrame: subtitleBase.x
Rectangle {
id: subtitleBase
width: duration * timeScale // to make width change wrt timeline scale factor
height: parent.height
x: model.startframe * timeScale;
property bool textEditBegin: false
color: 'yellow'
border.width: 1
border.color: 'orange'
/*Text {
id: subtitleText
anchors.fill: parent
visible: true
text: model.subtitle
color: 'black'
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}*/
MouseArea {
// Clip shifting
id: subtitleClipArea
anchors.fill: parent
hoverEnabled: true
enabled: true
property int newStart: -1
property int diff: -1
property double delta: -1
property double originalDuration: -1
property double oldDelta: 0
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: (pressed ? Qt.ClosedHandCursor : ((startMouseArea.drag.active || endMouseArea.drag.active)? Qt.SizeHorCursor: Qt.PointingHandCursor));
drag.target: subtitleBase
drag.axis: Drag.XAxis
onPressed: {
console.log('IT IS PRESSED')
if (mouse.button == Qt.RightButton) {
console.log('RIGHT BUTTON CLICKED')
timeline.deleteSubtitle(subtitleBase.x / timeline.scaleFactor)
}
else {
root.autoScrolling = false
oldStartX = mouseX
oldStartFrame = subtitleBase.x
originalDuration = subtitleBase.width/timeScale
console.log("originalDuration",originalDuration)
}
}
onPositionChanged: {
if (pressed) {
newStart = Math.round((subtitleBase.x + (mouseX-oldStartX)) / timeScale)
if (mouseX != oldStartX) {
diff = (mouseX - oldStartX) / timeScale
subtitleBase.x = subtitleBase.x + diff
delta = subtitleBase.x/timeline.scaleFactor - oldStartFrame/timeline.scaleFactor
var diffDelta = delta - oldDelta
oldDelta = delta
subtitleBase.width = originalDuration * timeline.scaleFactor
}
}
}
onReleased: {
console.log('IT IS RELEASED')
root.autoScrolling = timeline.autoScroll
if (subtitleBase.x < 0)
subtitleBase.x = 0
if (mouseX != oldStartX && oldStartFrame!= subtitleBase.x) {
console.log("old start frame",oldStartFrame/timeline.scaleFactor, "new frame afer shifting ",oldStartFrame/timeline.scaleFactor + delta)
timeline.shiftSubtitle(oldStartFrame/timeline.scaleFactor , subtitleBase.x / timeline.scaleFactor, subtitleBase.x / timeline.scaleFactor + duration, subtitleEdit.text)
}
console.log("originalDuration after shifting",originalDuration)
}
onDoubleClicked: {
parent.textEditBegin = true
}
}
TextField {
id: subtitleEdit
font: miniFont
activeFocusOnPress: true
selectByMouse: true
onEditingFinished: {
subtitleEdit.focus = false