Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Initial commit for qml timeline, very messy but at least displays something and gives us an idea.

Qml borrowed from Shotcut.
parent 3ed57110
......@@ -291,7 +291,7 @@ if (KF5_FILEMETADATA)
target_link_libraries(kdenlive KF5::FileMetaData)
endif()
qt5_use_modules( kdenlive Script Widgets Concurrent Qml Quick)
qt5_use_modules( kdenlive Script Widgets Concurrent Qml Quick QuickWidgets)
if (Qt5WebKitWidgets_FOUND)
message(STATUS "Found Qt5 WebKitWidgets. You can use your Freesound.org credentials to download files")
......
......@@ -18,6 +18,7 @@ the Free Software Foundation, either version 3 of the License, or
#include "library/librarywidget.h"
#include <QCoreApplication>
#include "kdenlive_debug.h"
#include "timeline2/view/timelinewidget.h"
#include <locale>
#ifdef Q_OS_MAC
......@@ -80,7 +81,7 @@ void Core::initialize()
//TODO
/*connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/
m_timelineWidget = new TimelineWidget(m_mainWindow);
emit coreIsReady();
}
......@@ -124,6 +125,11 @@ LibraryWidget *Core::library()
return m_library;
}
TimelineWidget *Core::timeline()
{
return m_timelineWidget;
}
void Core::initLocale()
{
QLocale systemLocale = QLocale();
......
......@@ -21,6 +21,7 @@ class BinController;
class Bin;
class LibraryWidget;
class ProducerQueue;
class TimelineWidget;
#define pCore Core::self()
......@@ -66,6 +67,8 @@ public:
ProducerQueue *producerQueue();
/** @brief Returns a pointer to the library. */
LibraryWidget *library();
/** @brief Returns a pointer to the timeline. */
TimelineWidget *timeline();
private:
explicit Core(MainWindow *mainWindow);
......@@ -81,6 +84,7 @@ private:
ProducerQueue *m_producerQueue;
Bin *m_binWidget;
LibraryWidget *m_library;
TimelineWidget *m_timelineWidget;
signals:
void coreIsReady();
......
......@@ -117,6 +117,7 @@ int main(int argc, char *argv[])
app.setApplicationDisplayName(aboutData.displayName());
app.setOrganizationDomain(aboutData.organizationDomain());
app.setApplicationVersion(aboutData.version());
app.setAttribute(Qt::AA_DontCreateNativeWidgetSiblings, true);
// Create command line parser with options
QCommandLineParser parser;
......
......@@ -64,6 +64,7 @@
#include "utils/thememanager.h"
#include "utils/progressbutton.h"
#include "effectslist/effectslistwidget.h"
#include "timeline2/view/timelinewidget.h"
#include "utils/KoIconUtils.h"
#include "project/dialogs/temporarydata.h"
......@@ -274,6 +275,7 @@ MainWindow::MainWindow(const QString &MltPath, const QUrl &Url, const QString &c
setupActions();
QDockWidget *libraryDock = addDock(i18n("Library"), QStringLiteral("library"), pCore->library());
QDockWidget *timelineDock = addDock(i18n("Timeline2"), QStringLiteral("timeline2"), pCore->timeline());
m_clipMonitor = new Monitor(Kdenlive::ClipMonitor, pCore->monitorManager(), this);
pCore->bin()->setMonitor(m_clipMonitor);
......
......@@ -4,5 +4,6 @@ set(kdenlive_SRCS
timeline2/model/trackmodel.cpp
timeline2/model/clipmodel.cpp
timeline2/model/groupsmodel.cpp
timeline2/view/timelinewidget.cpp
PARENT_SCOPE)
......@@ -25,12 +25,16 @@
#include "clipmodel.hpp"
#include "groupsmodel.hpp"
#include <klocalizedstring.h>
#include <QDebug>
#include <mlt++/MltTractor.h>
#include <mlt++/MltProfile.h>
int TimelineModel::next_id = 0;
static const quintptr NO_PARENT_ID = quintptr(-1);
TimelineModel::TimelineModel() :
m_tractor()
TimelineModel::TimelineModel() : QAbstractItemModel(),
m_tractor(new Mlt::Tractor())
{
}
......@@ -40,7 +44,15 @@ std::shared_ptr<TimelineModel> TimelineModel::construct(bool populate)
ptr->m_groups = std::unique_ptr<GroupsModel>(new GroupsModel(ptr));
if (populate) {
TrackModel::construct(ptr);
TrackModel::construct(ptr);
int ix = TrackModel::construct(ptr);
// Testing: add a clip on first track
Mlt::Profile profile;
std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(profile,"color", "red"));
int clipId = ClipModel::construct(ptr, prod);
// Not sure this is the right way to insert a clip...
std::shared_ptr<ClipModel> clip(ptr->getClip(clipId));
std::unique_ptr<TrackModel>& track(ptr->getTrackById(ix));
track->requestClipInsertion(clip, 100, false);
}
return ptr;
}
......@@ -52,9 +64,186 @@ TimelineModel::~TimelineModel()
}
}
int TimelineModel::getTracksCount()
QModelIndex TimelineModel::index(int row, int column, const QModelIndex &parent) const
{
if (column > 0)
return QModelIndex();
// LOG_DEBUG() << __FUNCTION__ << row << column << parent;
QModelIndex result;
if (parent.isValid()) {
//TODO: do we need a separate index like shotcut?
int i = row; //m_trackList.at(parent.row()).mlt_index;
QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
if (track) {
Mlt::Playlist playlist((mlt_playlist) track->get_producer());
if (row < playlist.count())
result = createIndex(row, column, parent.row());
}
} else if (row < getTracksCount()) {
result = createIndex(row, column, NO_PARENT_ID);
}
return result;
}
QModelIndex TimelineModel::makeIndex(int trackIndex, int clipIndex) const
{
return index(clipIndex, 0, index(trackIndex));
}
QModelIndex TimelineModel::parent(const QModelIndex &index) const
{
// LOG_DEBUG() << __FUNCTION__ << index;
if (!index.isValid() || index.internalId() == NO_PARENT_ID)
return QModelIndex();
else
return createIndex(index.internalId(), 0, NO_PARENT_ID);
}
int TimelineModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
if (parent.isValid()) {
// return number of clip in a specific track
if (parent.internalId() != NO_PARENT_ID)
return 0;
QScopedPointer<Mlt::Producer> track(m_tractor->track(parent.row()));
if (track) {
Mlt::Playlist playlist((mlt_playlist) track->get_producer());
return playlist.count();
}
return 0;
/*int i = m_trackList.at(parent.row()).mlt_index;
QScopedPointer<Mlt::Producer> track(m_tractor->track(i));
if (track) {
Mlt::Playlist playlist(*track);
int n = playlist.count();
// LOG_DEBUG() << __FUNCTION__ << parent << i << n;
return n;
} else {
return 0;
}*/
}
return getTracksCount();
}
int TimelineModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QHash<int, QByteArray> TimelineModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[ResourceRole] = "resource";
roles[ServiceRole] = "mlt_service";
roles[IsBlankRole] = "blank";
roles[StartRole] = "start";
roles[DurationRole] = "duration";
roles[InPointRole] = "in";
roles[OutPointRole] = "out";
roles[FramerateRole] = "fps";
roles[IsMuteRole] = "mute";
roles[IsHiddenRole] = "hidden";
roles[IsAudioRole] = "audio";
roles[AudioLevelsRole] = "audioLevels";
roles[IsCompositeRole] = "composite";
roles[IsLockedRole] = "locked";
roles[FadeInRole] = "fadeIn";
roles[FadeOutRole] = "fadeOut";
roles[IsTransitionRole] = "isTransition";
roles[FileHashRole] = "hash";
roles[SpeedRole] = "speed";
return roles;
}
QVariant TimelineModel::data(const QModelIndex &index, int role) const
{
int count = m_tractor.count();
if (!m_tractor || !index.isValid()) {
return QVariant();
}
if (index.parent().isValid()) {
// Get data for a clip
switch (role) {
//TODO
case NameRole: {
return QString("service");
}
case ResourceRole:
case Qt::DisplayRole: {
return QString("resource");
}
case ServiceRole:
return QString("service2");
break;
case IsBlankRole:
return false;
//return playlist.is_blank(index.row());
case StartRole:
return 50;
case DurationRole:
return 100;
case InPointRole:
return 0;
case OutPointRole:
return 50;
case FramerateRole:
return 25;
default:
break;
}
} else {
switch (role) {
case NameRole:
case Qt::DisplayRole:
return QString("Track %1").arg(getTrackById_const(index.row())->getId());
case DurationRole:
return 100;
case IsMuteRole:
return 0;
case IsHiddenRole:
return 0;
case IsAudioRole:
return false;
case IsLockedRole:
return 0;
case IsCompositeRole: {
return Qt::Unchecked;
}
default:
break;
}
}
return QVariant();
}
int TimelineModel::trackHeight() const
{
//TODO
return 50;
}
void TimelineModel::setTrackHeight(int height)
{
//TODO
}
double TimelineModel::scaleFactor() const
{
//TODO
return 3.0;
}
void TimelineModel::setScaleFactor(double scale)
{
//TODO
}
int TimelineModel::getTracksCount() const
{
int count = m_tractor->count();
Q_ASSERT(count >= 0);
Q_ASSERT(count == static_cast<int>(m_allTracks.size()));
return count;
......@@ -138,7 +327,7 @@ void TimelineModel::registerTrack(std::unique_ptr<TrackModel>&& track, int pos)
Q_ASSERT(pos <= static_cast<int>(m_allTracks.size()));
//effective insertion (MLT operation)
int error = m_tractor.insert_track(*track ,pos);
int error = m_tractor->insert_track(*track ,pos);
Q_ASSERT(error == 0); //we might need better error handling...
// we now insert in the list
......@@ -158,6 +347,13 @@ void TimelineModel::registerClip(std::shared_ptr<ClipModel> clip)
m_groups->createGroupItem(id);
}
std::shared_ptr<ClipModel> TimelineModel::getClip(int id)
{
// is there a cleaner way to get a clip to add it to a track?
Q_ASSERT(m_allClips.count(id) > 0);
return m_allClips[id];
}
void TimelineModel::registerGroup(int groupId)
{
Q_ASSERT(m_allGroups.count(groupId) == 0);
......@@ -169,7 +365,7 @@ void TimelineModel::deregisterTrack(int id)
auto it = m_iteratorTable[id]; //iterator to the element
m_iteratorTable.erase(id); //clean table
auto index = std::distance(m_allTracks.begin(), it); //compute index in list
m_tractor.remove_track(static_cast<int>(index)); //melt operation
m_tractor->remove_track(static_cast<int>(index)); //melt operation
m_allTracks.erase(it); //actual deletion of object
}
......
......@@ -19,10 +19,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
***************************************************************************/
#ifndef TIMELINEMODEL_H
#define TIMELINEMODEL_H
#include <memory>
#include <unordered_map>
#include <unordered_set>
#include <QVector>
#include <QAbstractItemModel>
#include <mlt++/MltTractor.h>
class TrackModel;
......@@ -32,8 +36,12 @@ class GroupsModel;
/* @brief This class represents a Timeline object, as viewed by the backend.
In general, the Gui associated with it will send modification queries (such as resize or move), and this class authorize them or not depending on the validity of the modifications
*/
class TimelineModel
class TimelineModel : public QAbstractItemModel
{
Q_OBJECT
Q_PROPERTY(int trackHeight READ trackHeight WRITE setTrackHeight NOTIFY trackHeightChanged)
Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged)
public:
/* @brief construct a timeline object and returns a pointer to the created object
*/
......@@ -50,9 +58,44 @@ public:
friend class GroupsModel;
~TimelineModel();
/// Two level model: tracks and clips on track
enum {
NameRole = Qt::UserRole + 1,
ResourceRole, /// clip only
ServiceRole, /// clip only
IsBlankRole, /// clip only
StartRole, /// clip only
DurationRole,
InPointRole, /// clip only
OutPointRole, /// clip only
FramerateRole, /// clip only
IsMuteRole, /// track only
IsHiddenRole, /// track only
IsAudioRole,
AudioLevelsRole, /// clip only
IsCompositeRole, /// track only
IsLockedRole, /// track only
FadeInRole, /// clip only
FadeOutRole, /// clip only
IsTransitionRole,/// clip only
FileHashRole, /// clip only
SpeedRole /// clip only
};
int rowCount(const QModelIndex & parent = QModelIndex()) const Q_DECL_OVERRIDE;
int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE;
QHash<int, QByteArray> roleNames() const Q_DECL_OVERRIDE;
QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;
QModelIndex makeIndex(int trackIndex, int clipIndex) const;
QModelIndex parent(const QModelIndex &index) const;
int trackHeight() const;
void setTrackHeight(int height);
double scaleFactor() const;
void setScaleFactor(double scale);
/* @brief returns the number of tracks */
int getTracksCount();
int getTracksCount() const;
/* @brief returns the number of clips */
int getClipsCount() const;
......@@ -107,6 +150,8 @@ protected:
/* @brief Register a new track. This is a call-back meant to be called from ClipModel
*/
void registerClip(std::shared_ptr<ClipModel> clip);
std::shared_ptr<ClipModel> getClip(int id);
/* @brief Register a new group. This is a call-back meant to be called from GroupsModel
*/
......@@ -133,7 +178,7 @@ protected:
*/
static int getNextId();
private:
Mlt::Tractor m_tractor;
Mlt::Tractor *m_tractor;
QVector<int> m_snapPoints; // this will be modified from a lot of different places, we will probably need a mutex
std::list<std::unique_ptr<TrackModel>> m_allTracks;
......@@ -148,4 +193,9 @@ private:
std::unordered_set<int> m_allGroups; //ids of all the groups
signals:
void trackHeightChanged();
void scaleFactorChanged();
};
#endif
This diff is collapsed.
/*
* Copyright (c) 2013 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
*
* 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/>.
*/
import QtQuick 2.0
import QtQuick.Controls 1.0
Rectangle {
property int stepSize: 34
property int index: 0
property real timeScale: 1.0
SystemPalette { id: activePalette }
id: rulerTop
enabled: false
height: 24
color: activePalette.base
Repeater {
model: parent.width / stepSize
Rectangle {
anchors.bottom: rulerTop.bottom
height: (index % 4)? ((index % 2) ? 3 : 7) : 14
width: 1
color: activePalette.windowText
x: index * stepSize
}
}
Repeater {
model: parent.width / stepSize / 4
Label {
anchors.bottom: rulerTop.bottom
anchors.bottomMargin: 2
color: activePalette.windowText
x: index * stepSize * 4 + 2
text: timeline.timecode(index * stepSize * 4 / timeScale)
font.pointSize: 7.5
}
}
}
/*
* Copyright (c) 2013-2015 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
*
* 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/>.
*/
function scrollIfNeeded() {
var x = timeline.position * multitrack.scaleFactor;
if (!scrollView) return;
if (x > scrollView.flickableItem.contentX + scrollView.width - 50)
scrollView.flickableItem.contentX = x - scrollView.width + 50;
else if (x < 50)
scrollView.flickableItem.contentX = 0;
else if (x < scrollView.flickableItem.contentX + 50)
scrollView.flickableItem.contentX = x - 50;
}
function dragging(pos, duration) {
if (tracksRepeater.count > 0) {
var headerHeight = ruler.height + toolbar.height
dropTarget.x = pos.x
dropTarget.width = duration * multitrack.scaleFactor
for (var i = 0; i < tracksRepeater.count; i++) {
var trackY = tracksRepeater.itemAt(i).y + headerHeight - scrollView.flickableItem.contentY
var trackH = tracksRepeater.itemAt(i).height
if (pos.y >= trackY && pos.y < trackY + trackH) {
currentTrack = i
if (pos.x > headerWidth) {
dropTarget.height = trackH
dropTarget.y = trackY
if (dropTarget.y < headerHeight) {
dropTarget.height -= headerHeight - dropTarget.y
dropTarget.y = headerHeight
}
dropTarget.visible = true
}
break
}
}
if (i === tracksRepeater.count || pos.x <= headerWidth)
dropTarget.visible = false
// Scroll tracks if at edges.
if (pos.x > headerWidth + scrollView.width - 50) {
// Right edge
scrollTimer.backwards = false
scrollTimer.start()
} else if (pos.x >= headerWidth && pos.x < headerWidth + 50) {
// Left edge
if (scrollView.flickableItem.contentX < 50) {
scrollTimer.stop()
scrollView.flickableItem.contentX = 0;
} else {
scrollTimer.backwards = true
scrollTimer.start()
}
} else {
scrollTimer.stop()
}
if (toolbar.scrub) {
timeline.position = Math.round(
(pos.x + scrollView.flickableItem.contentX - headerWidth) / multitrack.scaleFactor)
}
if (toolbar.snap) {
for (i = 0; i < tracksRepeater.count; i++)
tracksRepeater.itemAt(i).snapDrop(pos)
}
}
}
function dropped() {
dropTarget.visible = false
scrollTimer.running = false
}
function acceptDrop(xml) {
var position = Math.round((dropTarget.x + scrollView.flickableItem.contentX - headerWidth) / multitrack.scaleFactor)
if (toolbar.ripple)
timeline.insert(currentTrack, position, xml)
else
timeline.overwrite(currentTrack, position, xml)
}
function trackHeight(isAudio) {
return isAudio? Math.max(40, multitrack.trackHeight) : multitrack.trackHeight * 2
}
/*
* Copyright (c) 2013 Meltytech, LLC
* Author: Dan Dennedy <dan@dennedy.org>
*
* 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/>.
*/
var SNAP = 10
function snapClip(clip, repeater) {
// clip.x = left edge
var right = clip.x + clip.width
if (clip.x > -SNAP && clip.x < SNAP) {
// Snap around origin.
clip.x = 0
return
} else {
// Snap to other clips.
for (var i = 0; i < repeater.count; i++) {
var itemLeft = repeater.itemAt(i).x
var itemRight = itemLeft + repeater.itemAt(i).width
// Snap to blank
if (right > itemLeft - SNAP && right < itemLeft + SNAP) {
clip.x = itemLeft - clip.width
return
} else if (clip.x > itemRight - SNAP && clip.x < itemRight + SNAP) {
clip.x = itemRight
return
} else if (right > itemRight - SNAP && right < itemRight + SNAP) {
clip.x = itemRight - clip.width
return
} else if (clip.x > itemLeft - SNAP && clip.x < itemLeft + SNAP) {
clip.x = itemLeft
return
}
}
}
if (!toolbar.scrub) {
var cursorX = scrollView.flickableItem.contentX + cursor.x
if (clip.x > cursorX - SNAP && clip.x < cursorX + SNAP)
// Snap around cursor/playhead.
clip.x = cursorX
if (right > cursorX - SNAP && right < cursorX + SNAP)
clip.x = cursorX - clip.width
}
}
function snapTrimIn(clip, delta) {
var x = clip.x + delta
var cursorX = scrollView.flickableItem.contentX + cursor.x
if (false) {
// Snap to other clips.
for (var i = 0; i < repeater.count; i++) {