diff --git a/src/doc/kdenlivedoc.cpp b/src/doc/kdenlivedoc.cpp index 3e9356c78..1196a6ada 100644 --- a/src/doc/kdenlivedoc.cpp +++ b/src/doc/kdenlivedoc.cpp @@ -1,1697 +1,1696 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "kdenlivedoc.h" #include "documentchecker.h" #include "documentvalidator.h" #include "mltcontroller/clipcontroller.h" #include "mltcontroller/producerqueue.h" #include #include "kdenlivesettings.h" #include "renderer.h" #include "mainwindow.h" #include "project/clipmanager.h" #include "project/projectcommands.h" #include "bin/bincommands.h" #include "effectslist/initeffects.h" #include "dialogs/profilesdialog.h" #include "titler/titlewidget.h" #include "project/notesplugin.h" #include "project/dialogs/noteswidget.h" #include "core.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "utils/KoIconUtils.h" #include "mltcontroller/bincontroller.h" #include "mltcontroller/effectscontroller.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif DocUndoStack::DocUndoStack(QUndoGroup *parent) : QUndoStack(parent) { } //TODO: custom undostack everywhere do that void DocUndoStack::push(QUndoCommand *cmd) { if (index() < count()) emit invalidate(); QUndoStack::push(cmd); } const double DOCUMENTVERSION = 0.94; KdenliveDoc::KdenliveDoc(const QUrl &url, const QUrl &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap & properties, const QMap & metadata, const QPoint &tracks, Render *render, NotesPlugin *notes, bool *openBackup, MainWindow *parent) : QObject(parent), m_autosave(NULL), m_url(url), m_width(0), m_height(0), m_render(render), m_notesWidget(notes->widget()), -// m_commandStack(new DocUndoStack(undoGroup)), m_modified(false), m_projectFolder(projectFolder) { // init m_profile struct m_commandStack = new DocUndoStack(undoGroup); m_profile.frame_rate_num = 0; m_profile.frame_rate_den = 0; m_profile.width = 0; m_profile.height = 0; m_profile.progressive = 0; m_profile.sample_aspect_num = 0; m_profile.sample_aspect_den = 0; m_profile.display_aspect_num = 0; m_profile.display_aspect_den = 0; m_profile.colorspace = 0; m_clipManager = new ClipManager(this); connect(m_clipManager, SIGNAL(displayMessage(QString,int)), parent, SLOT(slotGotProgressInfo(QString,int))); bool success = false; connect(m_commandStack, SIGNAL(indexChanged(int)), this, SLOT(slotModified())); connect(m_commandStack, SIGNAL(invalidate()), this, SLOT(checkPreviewStack())); connect(m_render, SIGNAL(setDocumentNotes(QString)), this, SLOT(slotSetDocumentNotes(QString))); connect(pCore->producerQueue(), &ProducerQueue::switchProfile, this, &KdenliveDoc::switchProfile); //connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool))); // Init clip modification tracker m_modifiedTimer.setInterval(1500); connect(&m_fileWatcher, &KDirWatch::dirty, this, &KdenliveDoc::slotClipModified); connect(&m_fileWatcher, &KDirWatch::deleted, this, &KdenliveDoc::slotClipMissing); connect(&m_modifiedTimer, &QTimer::timeout, this, &KdenliveDoc::slotProcessModifiedClips); // init default document properties m_documentProperties[QStringLiteral("zoom")] = '7'; m_documentProperties[QStringLiteral("verticalzoom")] = '1'; m_documentProperties[QStringLiteral("zonein")] = '0'; m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("100"); m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int) KdenliveSettings::enableproxy()); m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams(); m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension(); m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int) KdenliveSettings::generateproxy()); m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize()); m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int) KdenliveSettings::generateimageproxy()); m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize()); m_documentProperties[QStringLiteral("documentid")] = QString::number(QDateTime::currentMSecsSinceEpoch()); // Load properties QMapIterator i(properties); while (i.hasNext()) { i.next(); m_documentProperties[i.key()] = i.value(); } // Load metadata QMapIterator j(metadata); while (j.hasNext()) { j.next(); m_documentMetadata[j.key()] = j.value(); } if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) { setlocale(LC_NUMERIC, ""); QLocale systemLocale = QLocale::system(); systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); // locale conversion might need to be redone initEffects::parseEffectFiles(pCore->binController()->mltRepository(), setlocale(LC_NUMERIC, NULL)); } *openBackup = false; if (url.isValid()) { QFile file(url.toLocalFile()); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { // The file cannot be opened if (KMessageBox::warningContinueCancel(parent, i18n("Cannot open the project file,\nDo you want to open a backup file?"), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } //KMessageBox::error(parent, KIO::NetAccess::lastErrorString()); } else { qDebug()<<" // / processing file open"; QString errorMsg; int line; int col; QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars); success = m_document.setContent(&file, false, &errorMsg, &line, &col); file.close(); if (!success) { // It is corrupted int answer = KMessageBox::warningYesNoCancel (parent, i18n("Cannot open the project file, error is:\n%1 (line %2, col %3)\nDo you want to open a backup file?", errorMsg, line, col), i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover"))); if (answer == KMessageBox::Yes) { *openBackup = true; } else if (answer == KMessageBox::No) { // Try to recover broken file produced by Kdenlive 0.9.4 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { int correction = 0; QString playlist = file.readAll(); while (!success && correction < 2) { int errorPos = 0; line--; col = col - 2; for (int j = 0; j < line && errorPos < playlist.length(); ++j) { errorPos = playlist.indexOf(QStringLiteral("\n"), errorPos); errorPos++; } errorPos += col; if (errorPos >= playlist.length()) break; playlist.remove(errorPos, 1); line = 0; col = 0; success = m_document.setContent(playlist, false, &errorMsg, &line, &col); correction++; } if (!success) { KMessageBox::sorry(parent, i18n("Cannot recover this project file")); } else { // Document was modified, ask for backup QDomElement mlt = m_document.documentElement(); mlt.setAttribute(QStringLiteral("modified"), 1); } } } } else { qDebug()<<" // / processing file open: validate"; parent->slotGotProgressInfo(i18n("Validating"), 0); qApp->processEvents(); DocumentValidator validator(m_document, url); success = validator.isProject(); if (!success) { // It is not a project file parent->slotGotProgressInfo(i18n("File %1 is not a Kdenlive project file", m_url.path()), 100); if (KMessageBox::warningContinueCancel(parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.path()), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) { *openBackup = true; } } else { /* * Validate the file against the current version (upgrade * and recover it if needed). It is NOT a passive operation */ // TODO: backup the document or alert the user? success = validator.validate(DOCUMENTVERSION); if (success && !KdenliveSettings::gpu_accel()) { success = validator.checkMovit(); } if (success) { // Let the validator handle error messages qDebug()<<" // / processing file validate ok"; parent->slotGotProgressInfo(i18n("Check missing clips"), 0); qApp->processEvents(); DocumentChecker d(m_url, m_document); success = !d.hasErrorInClips(); if (success) { loadDocumentProperties(); if (m_document.documentElement().attribute(QStringLiteral("modified")) == QLatin1String("1")) setModified(true); if (validator.isModified()) setModified(true); } } } } } } // Something went wrong, or a new file was requested: create a new project if (!success) { m_url.clear(); m_profile = ProfilesDialog::getVideoProfile(profileName); m_document = createEmptyDocument(tracks.x(), tracks.y()); updateProjectProfile(false); } // Ask to create the project directory if it does not exist QFileInfo checkProjectFolder(m_projectFolder.toString(QUrl::RemoveFilename | QUrl::RemoveScheme)); if (!QFile::exists(m_projectFolder.path()) && checkProjectFolder.isWritable()) { int create = KMessageBox::questionYesNo(parent, i18n("Project directory %1 does not exist. Create it?", m_projectFolder.path())); if (create == KMessageBox::Yes) { QDir projectDir(m_projectFolder.path()); bool ok = projectDir.mkpath(m_projectFolder.path()); if (!ok) { KMessageBox::sorry(parent, i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.", m_projectFolder.path())); } } } // Make sure the project folder is usable if (m_projectFolder.isEmpty() || !QFile::exists(m_projectFolder.path())) { KMessageBox::information(parent, i18n("Document project folder is invalid, setting it to the default one: %1", KdenliveSettings::defaultprojectfolder())); m_projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder()); } // Make sure that the necessary folders exist QDir dir(m_projectFolder.path()); dir.mkdir(QStringLiteral("titles")); dir.mkdir(QStringLiteral("thumbs")); dir.mkdir(QStringLiteral("proxy")); dir.mkdir(QStringLiteral(".backup")); QString documentId = m_documentProperties.value(QStringLiteral("documentid")); QDir dir2(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); dir2.mkdir(documentId); updateProjectFolderPlacesEntry(); } void KdenliveDoc::slotSetDocumentNotes(const QString ¬es) { m_notesWidget->setHtml(notes); } KdenliveDoc::~KdenliveDoc() { delete m_commandStack; //qDebug() << "// DEL CLP MAN"; delete m_clipManager; //qDebug() << "// DEL CLP MAN done"; if (m_autosave) { if (!m_autosave->fileName().isEmpty()) m_autosave->remove(); delete m_autosave; } } int KdenliveDoc::setSceneList() { //m_render->resetProfile(m_profile); pCore->bin()->isLoading = true; pCore->producerQueue()->abortOperations(); if (m_render->setSceneList(m_document.toString(), m_documentProperties.value(QStringLiteral("position")).toInt()) == -1) { // INVALID MLT Consumer, something is wrong return -1; } pCore->bin()->isLoading = false; pCore->binController()->checkThumbnails(projectFolder().path() + "/thumbs/"); m_documentProperties.remove(QStringLiteral("position")); pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor, true); return 0; } QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks) { QList tracks; // Tracks are added «backwards», so we need to reverse the track numbering // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331 // Better default names for tracks: Audio 1 etc. instead of blank numbers for (int i = 0; i < audiotracks; ++i) { TrackInfo audioTrack; audioTrack.type = AudioTrack; audioTrack.isMute = false; audioTrack.isBlind = true; audioTrack.isLocked = false; audioTrack.trackName = i18n("Audio %1", audiotracks - i); audioTrack.duration = 0; audioTrack.effectsList = EffectsList(true); tracks.append(audioTrack); } for (int i = 0; i < videotracks; ++i) { TrackInfo videoTrack; videoTrack.type = VideoTrack; videoTrack.isMute = false; videoTrack.isBlind = false; videoTrack.isLocked = false; videoTrack.trackName = i18n("Video %1", i + 1); videoTrack.duration = 0; videoTrack.effectsList = EffectsList(true); tracks.append(videoTrack); } return createEmptyDocument(tracks); } QDomDocument KdenliveDoc::createEmptyDocument(const QList &tracks) { // Creating new document QDomDocument doc; QDomElement mlt = doc.createElement(QStringLiteral("mlt")); mlt.setAttribute(QStringLiteral("LC_NUMERIC"), QLatin1String("")); doc.appendChild(mlt); QDomElement blk = doc.createElement(QStringLiteral("producer")); blk.setAttribute(QStringLiteral("in"), 0); blk.setAttribute(QStringLiteral("out"), 500); blk.setAttribute(QStringLiteral("id"), QStringLiteral("black")); QDomElement property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_type")); QDomText value = doc.createTextNode(QStringLiteral("producer")); property.appendChild(value); blk.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("aspect_ratio")); value = doc.createTextNode(QString::number(0)); property.appendChild(value); blk.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("length")); value = doc.createTextNode(QString::number(15000)); property.appendChild(value); blk.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("eof")); value = doc.createTextNode(QStringLiteral("pause")); property.appendChild(value); blk.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("resource")); value = doc.createTextNode(QStringLiteral("black")); property.appendChild(value); blk.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); value = doc.createTextNode(QStringLiteral("colour")); property.appendChild(value); blk.appendChild(property); mlt.appendChild(blk); QDomElement tractor = doc.createElement(QStringLiteral("tractor")); tractor.setAttribute(QStringLiteral("id"), QStringLiteral("maintractor")); tractor.setAttribute(QStringLiteral("global_feed"), 1); //QDomElement multitrack = doc.createElement("multitrack"); QDomElement playlist = doc.createElement(QStringLiteral("playlist")); playlist.setAttribute(QStringLiteral("id"), QStringLiteral("black_track")); mlt.appendChild(playlist); QDomElement blank0 = doc.createElement(QStringLiteral("entry")); blank0.setAttribute(QStringLiteral("in"), QStringLiteral("0")); blank0.setAttribute(QStringLiteral("out"), QStringLiteral("1")); blank0.setAttribute(QStringLiteral("producer"), QStringLiteral("black")); playlist.appendChild(blank0); // create playlists int total = tracks.count(); // The lower video track will recieve composite transitions int lowerVideoTrack = -1; for (int i = 0; i < total; ++i) { QDomElement playlist = doc.createElement(QStringLiteral("playlist")); playlist.setAttribute(QStringLiteral("id"), "playlist" + QString::number(i+1)); playlist.setAttribute(QStringLiteral("kdenlive:track_name"), tracks.at(i).trackName); if (tracks.at(i).type == AudioTrack) { playlist.setAttribute(QStringLiteral("kdenlive:audio_track"), 1); } else if (lowerVideoTrack == -1) { // Register first video track lowerVideoTrack = i + 1; } mlt.appendChild(playlist); } QDomElement track0 = doc.createElement(QStringLiteral("track")); track0.setAttribute(QStringLiteral("producer"), QStringLiteral("black_track")); tractor.appendChild(track0); // create audio and video tracks for (int i = 0; i < total; ++i) { QDomElement track = doc.createElement(QStringLiteral("track")); track.setAttribute(QStringLiteral("producer"), "playlist" + QString::number(i+1)); if (tracks.at(i).type == AudioTrack) { track.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); } else if (tracks.at(i).isBlind) { if (tracks.at(i).isMute) { track.setAttribute(QStringLiteral("hide"), QStringLiteral("all")); } else track.setAttribute(QStringLiteral("hide"), QStringLiteral("video")); } else if (tracks.at(i).isMute) track.setAttribute(QStringLiteral("hide"), QStringLiteral("audio")); tractor.appendChild(track); } for (int i = 0; i < total; i++) { QDomElement transition = doc.createElement(QStringLiteral("transition")); transition.setAttribute(QStringLiteral("always_active"), QStringLiteral("1")); QDomElement property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); value = doc.createTextNode(QStringLiteral("mix")); property.appendChild(value); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("a_track")); QDomText value = doc.createTextNode(QStringLiteral("0")); property.appendChild(value); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("b_track")); value = doc.createTextNode(QString::number(i + 1)); property.appendChild(value); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("combine")); value = doc.createTextNode(QStringLiteral("1")); property.appendChild(value); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added")); value = doc.createTextNode(QStringLiteral("237")); property.appendChild(value); transition.appendChild(property); tractor.appendChild(transition); if (i >= lowerVideoTrack && tracks.at(i).type == VideoTrack) { // Only add composite transition if both tracks are video transition = doc.createElement(QStringLiteral("transition")); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service")); property.appendChild(doc.createTextNode(KdenliveSettings::gpu_accel() ? "movit.overlay" : "frei0r.cairoblend")); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("a_track")); property.appendChild(doc.createTextNode(QString::number(lowerVideoTrack))); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("b_track")); property.appendChild(doc.createTextNode(QString::number(i+1))); transition.appendChild(property); property = doc.createElement(QStringLiteral("property")); property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added")); property.appendChild(doc.createTextNode(QStringLiteral("237"))); transition.appendChild(property); tractor.appendChild(transition); } } mlt.appendChild(tractor); return doc; } bool KdenliveDoc::useProxy() const { return m_documentProperties.value(QStringLiteral("enableproxy")).toInt(); } bool KdenliveDoc::autoGenerateProxy(int width) const { return m_documentProperties.value(QStringLiteral("generateproxy")).toInt() && width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt(); } bool KdenliveDoc::autoGenerateImageProxy(int width) const { return m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() && width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt(); } void KdenliveDoc::slotAutoSave() { if (m_render && m_autosave) { if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) { // show error: could not open the autosave file qDebug() << "ERROR; CANNOT CREATE AUTOSAVE FILE"; } //qDebug() << "// AUTOSAVE FILE: " << m_autosave->fileName(); QDomDocument sceneList = xmlSceneList(m_render->sceneList()); if (sceneList.isNull()) { //Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName())); return; } m_autosave->resize(0); m_autosave->write(sceneList.toString().toUtf8()); m_autosave->flush(); } } void KdenliveDoc::setZoom(int horizontal, int vertical) { m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal); m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical); } QPoint KdenliveDoc::zoom() const { return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt()); } void KdenliveDoc::setZone(int start, int end) { m_documentProperties[QStringLiteral("zonein")] = QString::number(start); m_documentProperties[QStringLiteral("zoneout")] = QString::number(end); } QPoint KdenliveDoc::zone() const { return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt()); } QDomDocument KdenliveDoc::xmlSceneList(const QString &scene) { QDomDocument sceneList; sceneList.setContent(scene, true); QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt")); if (mlt.isNull() || !mlt.hasChildNodes()) { //scenelist is corrupted return sceneList; } // Set playlist audio volume to 100% QDomElement tractor = mlt.firstChildElement(QStringLiteral("tractor")); if (!tractor.isNull()) { QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property")); for (int i = 0; i < props.count(); ++i) { if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) { props.at(i).firstChild().setNodeValue(QStringLiteral("1")); break; } } } QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist")); QDomElement mainPlaylist; for (int i = 0; i < pls.count(); ++i) { if (pls.at(i).toElement().attribute(QStringLiteral("id")) == pCore->binController()->binPlaylistId()) { mainPlaylist = pls.at(i).toElement(); break; } } // check if project contains custom effects to embed them in project file QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter")); int maxEffects = effects.count(); //qDebug() << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++"; QMap effectIds; for (int i = 0; i < maxEffects; ++i) { QDomNode m = effects.at(i); QDomNodeList params = m.childNodes(); QString id; QString tag; for (int j = 0; j < params.count(); ++j) { QDomElement e = params.item(j).toElement(); if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) { id = e.firstChild().nodeValue(); } if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) { tag = e.firstChild().nodeValue(); } if (!id.isEmpty() && !tag.isEmpty()) effectIds.insert(id, tag); } } //TODO: find a way to process this before rendering MLT scenelist to xml QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds); if (customeffects.documentElement().childNodes().count() > 0) { EffectsList::setProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString()); } //addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true)); //TODO: move metadata to previous step in saving process QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata")); QMapIterator j(m_documentMetadata); while (j.hasNext()) { j.next(); docmetadata.setAttribute(j.key(), j.value()); } //addedXml.appendChild(docmetadata); return sceneList; } QString KdenliveDoc::documentNotes() const { QString text = m_notesWidget->toPlainText().simplified(); if (text.isEmpty()) return QString(); return m_notesWidget->toHtml(); } bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene) { QDomDocument sceneList = xmlSceneList(scene); if (sceneList.isNull()) { //Make sure we don't save if scenelist is corrupted KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path)); return false; } // Backup current version backupLastSavedVersion(path); QFile file(path); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "////// ERROR writing to file: " << path; KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); return false; } file.write(sceneList.toString().toUtf8()); if (file.error() != QFile::NoError) { KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path)); file.close(); return false; } file.close(); cleanupBackupFiles(); QFileInfo info(file); QString fileName = QUrl::fromLocalFile(path).fileName().section('.', 0, -2); fileName.append('-' + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(".kdenlive.png"); QDir backupFolder(m_projectFolder.path() + "/.backup"); emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName)); return true; } ClipManager *KdenliveDoc::clipManager() { return m_clipManager; } QString KdenliveDoc::groupsXml() const { return m_clipManager->groupsXml(); } QUrl KdenliveDoc::projectFolder() const { //if (m_projectFolder.isEmpty()) return QUrl(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "//projects/"); return m_projectFolder; } void KdenliveDoc::setProjectFolder(QUrl url) { if (url == m_projectFolder) return; setModified(true); QDir dir(url.toLocalFile()); if (!dir.exists()) { dir.mkpath(dir.absolutePath()); } dir.mkdir(QStringLiteral("titles")); dir.mkdir(QStringLiteral("thumbs")); dir.mkdir(QStringLiteral("proxy")); dir.mkdir(QStringLiteral(".backup")); if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the new folder %2?", m_projectFolder.path(), url.path())) == KMessageBox::Yes) moveProjectData(url); m_projectFolder = url; updateProjectFolderPlacesEntry(); } void KdenliveDoc::moveProjectData(const QUrl &url) { QList list = pCore->binController()->getControllerList(); QList cacheUrls; for (int i = 0; i < list.count(); ++i) { ClipController *clip = list.at(i); if (clip->clipType() == Text) { // the image for title clip must be moved QUrl oldUrl = clip->clipUrl(); QUrl newUrl = QUrl::fromLocalFile(url.toLocalFile() + QDir::separator() + "titles/" + oldUrl.fileName()); KIO::Job *job = KIO::copy(oldUrl, newUrl); if (job->exec()) clip->setProperty(QStringLiteral("resource"), newUrl.path()); } QString hash = clip->getClipHash(); QUrl oldVideoThumbUrl = QUrl::fromLocalFile(m_projectFolder.path() + QDir::separator() + "thumbs/" + hash + ".png"); if (QFile::exists(oldVideoThumbUrl.path())) { cacheUrls << oldVideoThumbUrl; } QUrl oldAudioThumbUrl = QUrl::fromLocalFile(m_projectFolder.path() + QDir::separator() + "thumbs/" + hash + ".thumb"); if (QFile::exists(oldAudioThumbUrl.path())) { cacheUrls << oldAudioThumbUrl; } QUrl oldVideoProxyUrl = QUrl::fromLocalFile(m_projectFolder.path() + QDir::separator() + "proxy/" + hash + '.' + KdenliveSettings::proxyextension()); if (QFile::exists(oldVideoProxyUrl.path())) { cacheUrls << oldVideoProxyUrl; } } if (!cacheUrls.isEmpty()) { KIO::Job *job = KIO::copy(cacheUrls, QUrl::fromLocalFile(url.path() + QDir::separator() + "thumbs/")); KJobWidgets::setWindow(job, QApplication::activeWindow()); job->exec(); } } const QString &KdenliveDoc::profilePath() const { return m_profile.path; } MltVideoProfile KdenliveDoc::mltProfile() const { return m_profile; } bool KdenliveDoc::profileChanged(const QString &profile) const { return m_profile.toList() != ProfilesDialog::getVideoProfile(profile).toList(); } ProfileInfo KdenliveDoc::getProfileInfo() const { ProfileInfo info; info.profileSize = getRenderSize(); info.profileFps = fps(); return info; } double KdenliveDoc::dar() const { return (double) m_profile.display_aspect_num / m_profile.display_aspect_den; } DocUndoStack *KdenliveDoc::commandStack() { return m_commandStack; } Render *KdenliveDoc::renderer() { return m_render; } int KdenliveDoc::getFramePos(const QString &duration) { return m_timecode.getFrameCount(duration); } QDomDocument KdenliveDoc::toXml() { return m_document; } Timecode KdenliveDoc::timecode() const { return m_timecode; } QDomNodeList KdenliveDoc::producersList() { return m_document.elementsByTagName(QStringLiteral("producer")); } double KdenliveDoc::projectDuration() const { if (m_render) return GenTime(m_render->getLength(), m_render->fps()).ms() / 1000; else return 0; } double KdenliveDoc::fps() const { return m_render->fps(); } int KdenliveDoc::width() const { return m_width; } int KdenliveDoc::height() const { return m_height; } QUrl KdenliveDoc::url() const { return m_url; } void KdenliveDoc::setUrl(const QUrl &url) { m_url = url; } void KdenliveDoc::slotModified() { setModified(m_commandStack->isClean() == false); } void KdenliveDoc::setModified(bool mod) { // fix mantis#3160: The document may have an empty URL if not saved yet, but should have a m_autosave in any case if (m_autosave && mod && KdenliveSettings::crashrecovery()) { emit startAutoSave(); } if (mod == m_modified) return; m_modified = mod; emit docModified(m_modified); } bool KdenliveDoc::isModified() const { return m_modified; } const QString KdenliveDoc::description() const { if (!m_url.isValid()) return i18n("Untitled") + "[*] / " + m_profile.description; else return m_url.fileName() + " [*]/ " + m_profile.description; } bool KdenliveDoc::addClip(QDomElement elem, const QString &clipId) { const QString producerId = clipId.section('_', 0, 0); elem.setAttribute(QStringLiteral("id"), producerId); if (KdenliveSettings::checkfirstprojectclip() && pCore->bin()->isEmpty()) { elem.setAttribute("checkProfile", 1); } pCore->bin()->createClip(elem); pCore->producerQueue()->getFileProperties(elem, producerId, 150, true); /*QString str; QTextStream stream(&str); elem.save(stream, 4); qDebug()<<"ADDING CLIP COMMAND\n-----------\n"<getClipById(producerId); if (clip == NULL) { QString clipFolder = KRecentDirs::dir(":KdenliveClipFolder"); elem.setAttribute("id", producerId); QString path = elem.attribute("resource"); QString extension; if (elem.attribute("type").toInt() == SlideShow) { QUrl f = QUrl::fromLocalFile(path); extension = f.fileName(); path = f.adjusted(QUrl::RemoveFilename).path(); } if (elem.hasAttribute("_missingsource")) { // Clip has proxy but missing original source } else if (path.isEmpty() == false && QFile::exists(path) == false && elem.attribute("type").toInt() != Text && !elem.hasAttribute("placeholder")) { //qDebug() << "// FOUND MISSING CLIP: " << path << ", TYPE: " << elem.attribute("type").toInt(); const QString size = elem.attribute("file_size"); const QString hash = elem.attribute("file_hash"); QString newpath; int action = KMessageBox::No; if (!size.isEmpty() && !hash.isEmpty()) { if (!m_searchFolder.isEmpty()) newpath = searchFileRecursively(m_searchFolder, size, hash); else action = (KMessageBox::ButtonCode) KMessageBox::questionYesNoCancel(QApplication::activeWindow(), i18n("Clip %1
is invalid, what do you want to do?", path), i18n("File not found"), KGuiItem(i18n("Search automatically")), KGuiItem(i18n("Keep as placeholder"))); } else { if (elem.attribute("type").toInt() == SlideShow) { int res = KMessageBox::questionYesNoCancel(QApplication::activeWindow(), i18n("Clip %1
is invalid or missing, what do you want to do?", path), i18n("File not found"), KGuiItem(i18n("Search manually")), KGuiItem(i18n("Keep as placeholder"))); if (res == KMessageBox::Yes) newpath = QFileDialog::getExistingDirectory(QApplication::activeWindow(), i18n("Looking for %1", path), clipFolder); else { // Abort project loading action = res; } } else { int res = KMessageBox::questionYesNoCancel(QApplication::activeWindow(), i18n("Clip %1
is invalid or missing, what do you want to do?", path), i18n("File not found"), KGuiItem(i18n("Search manually")), KGuiItem(i18n("Keep as placeholder"))); if (res == KMessageBox::Yes) newpath = QFileDialog::getOpenFileName(QApplication::activeWindow(), i18n("Looking for %1", path), clipFolder); else { // Abort project loading action = res; } } } if (action == KMessageBox::Yes) { //qDebug() << "// ASKED FOR SRCH CLIP: " << clipId; m_searchFolder = QFileDialog::getExistingDirectory(QApplication::activeWindow(), QString(), clipFolder); if (!m_searchFolder.isEmpty()) newpath = searchFileRecursively(QDir(m_searchFolder), size, hash); } else if (action == KMessageBox::Cancel) { return false; } else if (action == KMessageBox::No) { // Keep clip as placeHolder elem.setAttribute("placeholder", '1'); } if (!newpath.isEmpty()) { //qDebug() << "// NEW CLIP PATH FOR CLIP " << clipId << " : " << newpath; if (elem.attribute("type").toInt() == SlideShow) newpath.append('/' + extension); elem.setAttribute("resource", newpath); setNewClipResource(clipId, newpath); setModified(true); } } clip = new DocClipBase(m_clipManager, elem, producerId); m_clipManager->addClip(clip); } return true; */ } QString KdenliveDoc::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const { QString foundFileName; QByteArray fileData; QByteArray fileHash; QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { QFile file(dir.absoluteFilePath(filesAndDirs.at(i))); if (file.open(QIODevice::ReadOnly)) { if (QString::number(file.size()) == matchSize) { /* * 1 MB = 1 second per 450 files (or faster) * 10 MB = 9 seconds per 450 files (or faster) */ if (file.size() > 1000000 * 2) { fileData = file.read(1000000); if (file.seek(file.size() - 1000000)) fileData.append(file.readAll()); } else fileData = file.readAll(); file.close(); fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); if (QString(fileHash.toHex()) == matchHash) return file.fileName(); else qDebug() << filesAndDirs.at(i) << "size match but not hash"; } } ////qDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex(); } filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot); for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) { foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash); if (!foundFileName.isEmpty()) break; } return foundFileName; } /* bool KdenliveDoc::addClipInfo(QDomElement elem, QDomElement orig, const QString &clipId) { DocClipBase *clip = m_clipManager->getClipById(clipId); if (clip == NULL) { if (!addClip(elem, clipId, false)) return false; } else { QMap properties; QDomNamedNodeMap attributes = elem.attributes(); for (int i = 0; i < attributes.count(); ++i) { QString attrname = attributes.item(i).nodeName(); if (attrname != "resource") properties.insert(attrname, attributes.item(i).nodeValue()); ////qDebug() << attrname << " = " << attributes.item(i).nodeValue(); } clip->setProperties(properties); emit addProjectClip(clip, false); } if (!orig.isNull()) { QMap meta; for (QDomNode m = orig.firstChild(); !m.isNull(); m = m.nextSibling()) { QString name = m.toElement().attribute("name"); if (name.startsWith(QLatin1String("meta.attr"))) { if (name.endsWith(QLatin1String(".markup"))) name = name.section('.', 0, -2); meta.insert(name.section('.', 2, -1), m.firstChild().nodeValue()); } } if (!meta.isEmpty()) { if (clip == NULL) clip = m_clipManager->getClipById(clipId); if (clip) clip->setMetadata(meta); } } return true; }*/ void KdenliveDoc::deleteClip(const QString &clipId) { ClipController *controller = pCore->binController()->getController(clipId); if (!controller) { // Clip doesn't exist, something is wrong qWarning()<<"// Document error deleting clip: "<clipType(); QString url = controller->clipUrl().toLocalFile(); // Delete clip in bin pCore->bin()->deleteClip(clipId); // Delete controller and Mlt::Producer pCore->binController()->removeBinClip(clipId); // Remove from file watch if (type != Color && type != SlideShow && type != QText && !url.isEmpty()) { m_fileWatcher.removeFile(url); } } ProjectClip *KdenliveDoc::getBinClip(const QString &clipId) { return pCore->bin()->getBinClip(clipId); } QStringList KdenliveDoc::getBinFolderClipIds(const QString &folderId) const { return pCore->bin()->getBinFolderClipIds(folderId); } ClipController *KdenliveDoc::getClipController(const QString &clipId) { return pCore->binController()->getController(clipId); } void KdenliveDoc::slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path) { QString titlesFolder = QDir::cleanPath(projectFolder().path() + QDir::separator() + "titles/"); if (path.isEmpty()) { QPointer d = new QFileDialog(QApplication::activeWindow(), i18n("Enter Template Path"), titlesFolder); d->setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlivetitle")); d->setFileMode(QFileDialog::ExistingFile); if (d->exec() == QDialog::Accepted && !d->selectedUrls().isEmpty()) { path = d->selectedUrls().first(); } delete d; } if (path.isEmpty()) return; //TODO: rewrite with new title system (just set resource) m_clipManager->slotAddTextTemplateClip(i18n("Template title clip"), path, group, groupId); emit selectLastAddedClip(QString::number(m_clipManager->lastClipId())); } void KdenliveDoc::cacheImage(const QString &fileId, const QImage &img) const { img.save(QDir::cleanPath(m_projectFolder.path() +QDir::separator() + "thumbs/" + fileId + ".png")); } void KdenliveDoc::setDocumentProperty(const QString &name, const QString &value) { if (value.isEmpty()) { m_documentProperties.remove(name); return; } m_documentProperties[name] = value; } const QString KdenliveDoc::getDocumentProperty(const QString &name, const QString &defaultValue) const { if (m_documentProperties.contains(name)) return m_documentProperties.value(name); return defaultValue; } QMap KdenliveDoc::getRenderProperties() const { QMap renderProperties; QMapIterator i(m_documentProperties); while (i.hasNext()) { i.next(); if (i.key().startsWith(QLatin1String("render"))) renderProperties.insert(i.key(), i.value()); } return renderProperties; } void KdenliveDoc::saveCustomEffects(const QDomNodeList &customeffects) { QDomElement e; QStringList importedEffects; int maxchild = customeffects.count(); for (int i = 0; i < maxchild; ++i) { e = customeffects.at(i).toElement(); const QString id = e.attribute(QStringLiteral("id")); const QString tag = e.attribute(QStringLiteral("tag")); if (!id.isEmpty()) { // Check if effect exists or save it if (MainWindow::customEffects.hasEffect(tag, id) == -1) { QDomDocument doc; doc.appendChild(doc.importNode(e, true)); QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "/effects"; path += id + ".xml"; if (!QFile::exists(path)) { importedEffects << id; QFile file(path); if (file.open(QFile::WriteOnly | QFile::Truncate)) { QTextStream out(&file); out << doc.toString(); } } } } } if (!importedEffects.isEmpty()) KMessageBox::informationList(QApplication::activeWindow(), i18n("The following effects were imported from the project:"), importedEffects); if (!importedEffects.isEmpty()) { emit reloadEffects(); } } void KdenliveDoc::updateProjectFolderPlacesEntry() { /* * For similar and more code have a look at kfileplacesmodel.cpp and the included files: * http://websvn.kde.org/trunk/KDE/kdelibs/kfile/kfileplacesmodel.cpp?view=markup */ const QString file = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/user-places.xbel"; KBookmarkManager *bookmarkManager = KBookmarkManager::managerForExternalFile(file); if (!bookmarkManager) return; KBookmarkGroup root = bookmarkManager->root(); KBookmark bookmark = root.first(); QString kdenliveName = QCoreApplication::applicationName(); QUrl documentLocation = m_projectFolder; bool exists = false; while (!bookmark.isNull()) { // UDI not empty indicates a device QString udi = bookmark.metaDataItem(QStringLiteral("UDI")); QString appName = bookmark.metaDataItem(QStringLiteral("OnlyInApp")); if (udi.isEmpty() && appName == kdenliveName && bookmark.text() == i18n("Project Folder")) { if (bookmark.url() != documentLocation) { bookmark.setUrl(documentLocation); bookmarkManager->emitChanged(root); } exists = true; break; } bookmark = root.next(bookmark); } // if entry does not exist yet (was not found), well, create it then if (!exists) { bookmark = root.addBookmark(i18n("Project Folder"), documentLocation, QStringLiteral("folder-favorites")); // Make this user selectable ? bookmark.setMetaDataItem(QStringLiteral("OnlyInApp"), kdenliveName); bookmarkManager->emitChanged(root); } } const QSize KdenliveDoc::getRenderSize() const { QSize size; if (m_render) { size.setWidth(m_render->frameRenderWidth()); size.setHeight(m_render->renderHeight()); } return size; } // static double KdenliveDoc::getDisplayRatio(const QString &path) { QFile file(path); QDomDocument doc; if (!file.open(QIODevice::ReadOnly)) { qWarning() << "ERROR, CANNOT READ: " << path; return 0; } if (!doc.setContent(&file)) { qWarning() << "ERROR, CANNOT READ: " << path; file.close(); return 0; } file.close(); QDomNodeList list = doc.elementsByTagName(QStringLiteral("profile")); if (list.isEmpty()) return 0; QDomElement profile = list.at(0).toElement(); double den = profile.attribute(QStringLiteral("display_aspect_den")).toDouble(); if (den > 0) return profile.attribute(QStringLiteral("display_aspect_num")).toDouble() / den; return 0; } void KdenliveDoc::backupLastSavedVersion(const QString &path) { // Ensure backup folder exists if (path.isEmpty()) return; QFile file(path); QDir backupFolder(m_projectFolder.path() + "/.backup"); QString fileName = QUrl::fromLocalFile(path).fileName().section('.', 0, -2); QFileInfo info(file); fileName.append('-' + m_documentProperties.value(QStringLiteral("documentid"))); fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm"))); fileName.append(".kdenlive"); QString backupFile = backupFolder.absoluteFilePath(fileName); if (file.exists()) { // delete previous backup if it was done less than 60 seconds ago QFile::remove(backupFile); if (!QFile::copy(path, backupFile)) { KMessageBox::information(QApplication::activeWindow(), i18n("Cannot create backup copy:\n%1", backupFile)); } } } void KdenliveDoc::cleanupBackupFiles() { QDir backupFolder(m_projectFolder.path() + "/.backup"); QString projectFile = url().fileName().section('.', 0, -2); projectFile.append('-' + m_documentProperties.value(QStringLiteral("documentid"))); projectFile.append("-??"); projectFile.append("??"); projectFile.append("-??"); projectFile.append("-??"); projectFile.append("-??"); projectFile.append("-??.kdenlive"); QStringList filter; filter << projectFile; backupFolder.setNameFilters(filter); QFileInfoList resultList = backupFolder.entryInfoList(QDir::Files, QDir::Time); QDateTime d = QDateTime::currentDateTime(); QStringList hourList; QStringList dayList; QStringList weekList; QStringList oldList; for (int i = 0; i < resultList.count(); ++i) { if (d.secsTo(resultList.at(i).lastModified()) < 3600) { // files created in the last hour hourList.append(resultList.at(i).absoluteFilePath()); } else if (d.secsTo(resultList.at(i).lastModified()) < 43200) { // files created in the day dayList.append(resultList.at(i).absoluteFilePath()); } else if (d.daysTo(resultList.at(i).lastModified()) < 8) { // files created in the week weekList.append(resultList.at(i).absoluteFilePath()); } else { // older files oldList.append(resultList.at(i).absoluteFilePath()); } } if (hourList.count() > 20) { int step = hourList.count() / 10; for (int i = 0; i < hourList.count(); i += step) { //qDebug()<<"REMOVE AT: "< 20) { int step = dayList.count() / 10; for (int i = 0; i < dayList.count(); i += step) { dayList.removeAt(i); --i; } } else dayList.clear(); if (weekList.count() > 20) { int step = weekList.count() / 10; for (int i = 0; i < weekList.count(); i += step) { weekList.removeAt(i); --i; } } else weekList.clear(); if (oldList.count() > 20) { int step = oldList.count() / 10; for (int i = 0; i < oldList.count(); i += step) { oldList.removeAt(i); --i; } } else oldList.clear(); QString f; while (hourList.count() > 0) { f = hourList.takeFirst(); QFile::remove(f); QFile::remove(f + ".png"); } while (dayList.count() > 0) { f = dayList.takeFirst(); QFile::remove(f); QFile::remove(f + ".png"); } while (weekList.count() > 0) { f = weekList.takeFirst(); QFile::remove(f); QFile::remove(f + ".png"); } while (oldList.count() > 0) { f = oldList.takeFirst(); QFile::remove(f); QFile::remove(f + ".png"); } } const QMap KdenliveDoc::metadata() const { return m_documentMetadata; } void KdenliveDoc::setMetadata(const QMap &meta) { setModified(true); m_documentMetadata = meta; } void KdenliveDoc::slotProxyCurrentItem(bool doProxy, QList clipList) { if (clipList.isEmpty()) clipList = pCore->bin()->selectedClips(); QUndoCommand *command = new QUndoCommand(); if (doProxy) command->setText(i18np("Add proxy clip", "Add proxy clips", clipList.count())); else command->setText(i18np("Remove proxy clip", "Remove proxy clips", clipList.count())); // Make sure the proxy folder exists QString proxydir = projectFolder().path() + QDir::separator() + "proxy/"; QDir dir(projectFolder().path()); dir.mkdir(QStringLiteral("proxy")); // Prepare updated properties QMap newProps; QMap oldProps; if (!doProxy) newProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); // Parse clips for (int i = 0; i < clipList.count(); ++i) { ProjectClip *item = clipList.at(i); ClipType t = item->clipType(); // Only allow proxy on some clip types if ((t == Video || t == AV || t == Unknown || t == Image || t == Playlist) && item->isReady()) { if ((doProxy && item->hasProxy()) || (!doProxy && !item->hasProxy() && pCore->binController()->hasClip(item->clipId()))) continue; if (pCore->producerQueue()->isProcessing(item->clipId())) { continue; } if (doProxy) { newProps.clear(); QString path = proxydir + item->hash() + '.' + (t == Image ? QStringLiteral("png") : getDocumentProperty(QStringLiteral("proxyextension"))); // insert required duration for proxy newProps.insert(QStringLiteral("proxy_out"), item->getProducerProperty(QStringLiteral("out"))); newProps.insert(QStringLiteral("kdenlive:proxy"), path); } else if (!pCore->binController()->hasClip(item->clipId())) { // Force clip reload newProps.insert(QStringLiteral("resource"), item->url().toLocalFile()); } // We need to insert empty proxy so that undo will work //TODO: how to handle clip properties //oldProps = clip->currentProperties(newProps); if (doProxy) oldProps.insert(QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); new EditClipCommand(pCore->bin(), item->clipId(), oldProps, newProps, true, command); } } if (command->childCount() > 0) { m_commandStack->push(command); } else delete command; } //TODO put all file watching stuff in own class void KdenliveDoc::watchFile(const QUrl &url) { m_fileWatcher.addFile(url.toLocalFile()); } void KdenliveDoc::slotClipModified(const QString &path) { QStringList ids = pCore->binController()->getBinIdsByResource(QUrl::fromLocalFile(path)); foreach (const QString &id, ids) { if (!m_modifiedClips.contains(id)) { pCore->bin()->setWaitingStatus(id); } m_modifiedClips[id] = QTime::currentTime(); } if (!m_modifiedTimer.isActive()) m_modifiedTimer.start(); } void KdenliveDoc::slotClipMissing(const QString &path) { qDebug() << "// CLIP: " << path << " WAS MISSING"; QStringList ids = pCore->binController()->getBinIdsByResource(QUrl::fromLocalFile(path)); //TODO handle missing clips by replacing producer with an invalid producer /*foreach (const QString &id, ids) { emit missingClip(id); }*/ } void KdenliveDoc::slotProcessModifiedClips() { if (!m_modifiedClips.isEmpty()) { QMapIterator i(m_modifiedClips); while (i.hasNext()) { i.next(); if (QTime::currentTime().msecsTo(i.value()) <= -1500) { pCore->bin()->reloadClip(i.key()); m_modifiedClips.remove(i.key()); break; } } setModified(true); } if (m_modifiedClips.isEmpty()) m_modifiedTimer.stop(); } QMap KdenliveDoc::documentProperties() { m_documentProperties.insert(QStringLiteral("version"), QString::number(DOCUMENTVERSION)); m_documentProperties.insert(QStringLiteral("kdenliveversion"), QStringLiteral(KDENLIVE_VERSION)); m_documentProperties.insert(QStringLiteral("projectfolder"), m_projectFolder.path()); m_documentProperties.insert(QStringLiteral("profile"), profilePath()); m_documentProperties.insert(QStringLiteral("position"), QString::number(m_render->seekPosition().frames(m_render->fps()))); return m_documentProperties; } void KdenliveDoc::loadDocumentProperties() { QDomNodeList list = m_document.elementsByTagName(QStringLiteral("playlist")); if (!list.isEmpty()) { QDomElement pl = list.at(0).toElement(); if (pl.isNull()) return; QDomNodeList props = pl.elementsByTagName(QStringLiteral("property")); QString name; QDomElement e; for (int i = 0; i < props.count(); i++) { e = props.at(i).toElement(); name = e.attribute(QStringLiteral("name")); if (name.startsWith(QLatin1String("kdenlive:docproperties."))) { name = name.section(QStringLiteral("."), 1); m_documentProperties.insert(name, e.firstChild().nodeValue()); } else if (name.startsWith(QLatin1String("kdenlive:docmetadata."))) { name = name.section(QStringLiteral("."), 1); m_documentMetadata.insert(name, e.firstChild().nodeValue()); } } } QString path = m_documentProperties.value(QStringLiteral("projectfolder")); if (!path.startsWith('/')) { QDir dir = QDir::home(); path = dir.absoluteFilePath(path); } m_projectFolder = QUrl::fromLocalFile(path); list = m_document.elementsByTagName(QStringLiteral("profile")); if (!list.isEmpty()) { m_profile = ProfilesDialog::getVideoProfileFromXml(list.at(0).toElement()); } updateProjectProfile(false); } void KdenliveDoc::updateProjectProfile(bool reloadProducers) { KdenliveSettings::setProject_display_ratio((double) m_profile.display_aspect_num / m_profile.display_aspect_den); double fps = (double) m_profile.frame_rate_num / m_profile.frame_rate_den; KdenliveSettings::setProject_fps(fps); m_width = m_profile.width; m_height = m_profile.height; bool fpsChanged = m_timecode.fps() != fps; m_timecode.setFormat(fps); pCore->producerQueue()->abortOperations(); KdenliveSettings::setCurrent_profile(m_profile.path); pCore->monitorManager()->resetProfiles(m_profile, m_timecode); if (!reloadProducers) return; emit updateFps(fpsChanged); if (fpsChanged) { pCore->bin()->reloadAllProducers(); } } void KdenliveDoc::resetProfile() { m_profile = ProfilesDialog::getVideoProfile(KdenliveSettings::current_profile()); updateProjectProfile(true); emit docModified(true); } void KdenliveDoc::slotSwitchProfile() { QAction *action = qobject_cast(sender()); if (!action) return; QVariantList data = action->data().toList(); QString id = data.takeFirst().toString(); if (!data.isEmpty()) { // we want a profile switch m_profile = MltVideoProfile(data); updateProjectProfile(true); emit docModified(true); } } void KdenliveDoc::switchProfile(MltVideoProfile profile, const QString &id, const QDomElement &xml) { // Request profile update QString matchingProfile = ProfilesDialog::existingProfile(profile); if (matchingProfile.isEmpty() && (profile.width % 8 != 0)) { // Make sure profile width is a multiple of 8, required by some parts of mlt profile.adjustWidth(); matchingProfile = ProfilesDialog::existingProfile(profile); } if (!matchingProfile.isEmpty()) { // We found a known matching profile, switch and inform user QMap< QString, QString > profileProperties = ProfilesDialog::getSettingsFromFile(matchingProfile); profile.path = matchingProfile; profile.description = profileProperties.value("description"); // Build actions for the info message (switch / cancel) QList list; QAction *ac = new QAction(KoIconUtils::themedIcon(QStringLiteral("dialog-ok")), i18n("Switch"), this); QVariantList params; connect(ac, SIGNAL(triggered(bool)), this, SLOT(slotSwitchProfile())); params << id << profile.toList(); ac->setData(params); QAction *ac2 = new QAction(KoIconUtils::themedIcon(QStringLiteral("dialog-cancel")), i18n("Cancel"), this); QVariantList params2; params2 << id; ac2->setData(params2); connect(ac2, SIGNAL(triggered(bool)), this, SLOT(slotSwitchProfile())); list << ac << ac2; pCore->bin()->doDisplayMessage(i18n("Switch to clip profile %1?", profile.descriptiveString()), KMessageWidget::Information, list); } else { // No known profile, ask user if he wants to use clip profile anyway if (KMessageBox::warningContinueCancel(QApplication::activeWindow(), i18n("No profile found for your clip.\nCreate and switch to new profile (%1x%2, %3fps)?", profile.width, profile.height, QString::number((double)profile.frame_rate_num / profile.frame_rate_den, 'f', 2))) == KMessageBox::Continue) { m_profile = profile; m_profile.description = QString("%1x%2 %3fps").arg(profile.width).arg(profile.height).arg(QString::number((double)profile.frame_rate_num / profile.frame_rate_den, 'f', 2)); ProfilesDialog::saveProfile(m_profile); updateProjectProfile(true); emit docModified(true); pCore->producerQueue()->getFileProperties(xml, id, 150, true); } } } void KdenliveDoc::forceProcessing(const QString &id) { pCore->producerQueue()->forceProcessing(id); } void KdenliveDoc::getFileProperties(const QDomElement &xml, const QString &clipId, int imageHeight, bool replaceProducer) { pCore->producerQueue()->getFileProperties(xml, clipId, imageHeight, replaceProducer); } void KdenliveDoc::doAddAction(const QString &name, QAction *a, QKeySequence shortcut) { pCore->window()->actionCollection()->addAction(name, a); pCore->window()->actionCollection()->setDefaultShortcut(a, shortcut); } QAction *KdenliveDoc::getAction(const QString &name) { return pCore->window()->actionCollection()->action(name); } void KdenliveDoc::previewProgress(int p) { pCore->window()->setPreviewProgress(p); } void KdenliveDoc::selectPreviewProfile() { // Read preview profiles and find the best match KConfig conf(QStringLiteral("encodingprofiles.rc"), KConfig::CascadeConfig, QStandardPaths::DataLocation); KConfigGroup group(&conf, "timelinepreview"); QMap< QString, QString > values = group.entryMap(); QMapIterator i(values); QStringList matchingProfiles; QStringList fallBackProfiles; while (i.hasNext()) { i.next(); // Check for frame rate QStringList data = i.value().split(" "); bool rateFound = false; foreach(const QString arg, data) { if (arg.startsWith(QStringLiteral("r="))) { rateFound = true; double fps = arg.section(QStringLiteral("="), 1).toDouble(); if (fps > 0) { if (qAbs((int) (m_render->fps() * 100) - (fps * 100)) <= 1) { matchingProfiles << i.value(); break; } } } } if (!rateFound) { // Profile without fps, can be used as fallBack fallBackProfiles << i.value(); } } QString bestMatch; if (matchingProfiles.count() > 1) { // several profiles with matching fps, try to decide based on resolution QString docSize = QString("s=%1x%2").arg(m_profile.width).arg(m_profile.height); foreach (const QString ¶m, matchingProfiles) { if (param.contains(docSize)) { bestMatch = param; break; } } if (bestMatch.isEmpty()) bestMatch = matchingProfiles.first(); } else if (matchingProfiles.count() == 1) { bestMatch = matchingProfiles.first(); } else if (!fallBackProfiles.isEmpty()) { bestMatch = fallBackProfiles.first(); } if (!bestMatch.isEmpty()) { setDocumentProperty(QStringLiteral("previewparameters"), bestMatch.section(";", 0, 0)); setDocumentProperty(QStringLiteral("previewextension"), bestMatch.section(";", 1, 1)); } } void KdenliveDoc::checkPreviewStack() { // A command was pushed in the middle of the stack, remove all cached data from last undos emit removeInvalidUndo(m_commandStack->count()); } void KdenliveDoc::saveMltPlaylist(const QString fileName) { m_render->preparePreviewRendering(fileName); } diff --git a/src/timeline/customruler.cpp b/src/timeline/customruler.cpp index da45a784b..37615cab4 100644 --- a/src/timeline/customruler.cpp +++ b/src/timeline/customruler.cpp @@ -1,589 +1,606 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "customruler.h" #include "kdenlivesettings.h" #include #include #include #include #include #include #include #include #include static int MAX_HEIGHT; // Width of a frame in pixels static int FRAME_SIZE; // Height of the timecode text static int LABEL_SIZE; // Width of a letter, used for cursor width static int FONT_WIDTH; static int BIG_MARK_X; static int MIDDLE_MARK_X; static int LITTLE_MARK_X; static int littleMarkDistance; static int mediumMarkDistance; static int bigMarkDistance; #define SEEK_INACTIVE (-1) #include "definitions.h" const int CustomRuler::comboScale[] = { 1, 2, 5, 10, 25, 50, 125, 250, 500, 750, 1500, 3000, 6000, 12000}; CustomRuler::CustomRuler(const Timecode &tc, const QList &rulerActions, CustomTrackView *parent) : QWidget(parent), m_timecode(tc), m_view(parent), m_duration(0), m_offset(0), m_headPosition(SEEK_INACTIVE), m_clickedGuide(-1), m_rate(-1), m_mouseMove(NO_MOVE) { setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QFontMetricsF fontMetrics(font()); // Define size variables LABEL_SIZE = fontMetrics.ascent(); FONT_WIDTH = fontMetrics.averageCharWidth(); setMinimumHeight(LABEL_SIZE * 2); setMaximumHeight(LABEL_SIZE * 2); MAX_HEIGHT = height(); BIG_MARK_X = LABEL_SIZE + 1; int mark_length = MAX_HEIGHT - BIG_MARK_X; MIDDLE_MARK_X = BIG_MARK_X + mark_length / 2; LITTLE_MARK_X = BIG_MARK_X + mark_length / 3; updateFrameSize(); m_scale = 3; m_zoneStart = 0; m_zoneEnd = 100; m_contextMenu = new QMenu(this); m_contextMenu->addActions(rulerActions); QAction *addGuide = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add Guide")); connect(addGuide, SIGNAL(triggered()), m_view, SLOT(slotAddGuide())); m_editGuide = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-properties")), i18n("Edit Guide")); connect(m_editGuide, SIGNAL(triggered()), this, SLOT(slotEditGuide())); m_deleteGuide = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Guide")); connect(m_deleteGuide , SIGNAL(triggered()), this, SLOT(slotDeleteGuide())); QAction *delAllGuides = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete All Guides")); connect(delAllGuides, SIGNAL(triggered()), m_view, SLOT(slotDeleteAllGuides())); m_goMenu = m_contextMenu->addMenu(i18n("Go To")); connect(m_goMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotGoToGuide(QAction*))); setMouseTracking(true); m_zoneBG = palette().color(QPalette::Highlight); m_zoneBG.setAlpha(KdenliveSettings::useTimelineZoneToEdit() ? 180 : 60); } void CustomRuler::updateProjectFps(const Timecode &t) { m_timecode = t; mediumMarkDistance = FRAME_SIZE * m_timecode.fps(); bigMarkDistance = FRAME_SIZE * m_timecode.fps() * 60; setPixelPerMark(m_rate); update(); } void CustomRuler::updateFrameSize() { FRAME_SIZE = m_view->getFrameWidth(); littleMarkDistance = FRAME_SIZE; mediumMarkDistance = FRAME_SIZE * m_timecode.fps(); bigMarkDistance = FRAME_SIZE * m_timecode.fps() * 60; updateProjectFps(m_timecode); if (m_rate > 0) setPixelPerMark(m_rate); } void CustomRuler::slotEditGuide() { m_view->slotEditGuide(m_clickedGuide); } void CustomRuler::slotDeleteGuide() { m_view->slotDeleteGuide(m_clickedGuide); } void CustomRuler::slotGoToGuide(QAction *act) { m_view->seekCursorPos(act->data().toInt()); m_view->initCursorPos(act->data().toInt()); } void CustomRuler::setZone(const QPoint &p) { m_zoneStart = p.x(); m_zoneEnd = p.y(); update(); } void CustomRuler::mouseReleaseEvent(QMouseEvent * /*event*/) { if (m_moveCursor == RULER_START || m_moveCursor == RULER_END || m_moveCursor == RULER_MIDDLE) { emit zoneMoved(m_zoneStart, m_zoneEnd); m_view->setDocumentModified(); } m_mouseMove = NO_MOVE; } // virtual void CustomRuler::mousePressEvent(QMouseEvent * event) { int pos = (int)((event->x() + offset())); if (event->button() == Qt::RightButton) { m_clickedGuide = m_view->hasGuide((int)(pos / m_factor), (int)(5 / m_factor + 1)); m_editGuide->setEnabled(m_clickedGuide > 0); m_deleteGuide->setEnabled(m_clickedGuide > 0); m_view->buildGuidesMenu(m_goMenu); m_contextMenu->exec(event->globalPos()); return; } setFocus(Qt::MouseFocusReason); m_view->activateMonitor(); m_moveCursor = RULER_CURSOR; if (event->y() > 10) { if (qAbs(pos - m_zoneStart * m_factor) < 4) m_moveCursor = RULER_START; else if (qAbs(pos - (m_zoneStart + (m_zoneEnd - m_zoneStart) / 2.0) * m_factor) < 4) m_moveCursor = RULER_MIDDLE; else if (qAbs(pos - (m_zoneEnd + 1)* m_factor) < 4) m_moveCursor = RULER_END; m_view->updateSnapPoints(NULL); } if (m_moveCursor == RULER_CURSOR) { m_view->seekCursorPos((int) pos / m_factor); m_clickPoint = event->pos(); m_startRate = m_rate; } } // virtual void CustomRuler::mouseMoveEvent(QMouseEvent * event) { int mappedXPos = (int)((event->x() + offset()) / m_factor); emit mousePosition(mappedXPos); if (event->buttons() == Qt::LeftButton) { int pos; if (m_moveCursor == RULER_START || m_moveCursor == RULER_END) { pos = m_view->getSnapPointForPos(mappedXPos); } else pos = mappedXPos; int zoneStart = m_zoneStart; int zoneEnd = m_zoneEnd; if (pos < 0) pos = 0; if (m_moveCursor == RULER_CURSOR) { QPoint diff = event->pos() - m_clickPoint; if (m_mouseMove == NO_MOVE) { if (qAbs(diff.x()) >= QApplication::startDragDistance()) { m_mouseMove = HORIZONTAL_MOVE; } else if (KdenliveSettings::verticalzoom() && qAbs(diff.y()) >= QApplication::startDragDistance()) { m_mouseMove = VERTICAL_MOVE; } else return; } if (m_mouseMove == HORIZONTAL_MOVE) { if (pos != m_headPosition && pos != m_view->cursorPos()) { int x = m_headPosition == SEEK_INACTIVE ? pos : m_headPosition; m_headPosition = pos; int min = qMin(x, m_headPosition); int max = qMax(x, m_headPosition); update(min * m_factor - offset() - 3, BIG_MARK_X, (max - min) * m_factor + 6, MAX_HEIGHT - BIG_MARK_X); emit seekCursorPos(pos); m_view->slotCheckPositionScrolling(); } } else { int verticalDiff = m_startRate - (diff.y()) / 7; if (verticalDiff != m_rate) emit adjustZoom(verticalDiff); } return; } else if (m_moveCursor == RULER_START) m_zoneStart = qMin(pos, m_zoneEnd); else if (m_moveCursor == RULER_END) m_zoneEnd = qMax(pos, m_zoneStart); else if (m_moveCursor == RULER_MIDDLE) { int move = pos - (m_zoneStart + (m_zoneEnd - m_zoneStart) / 2); if (move + m_zoneStart < 0) move = - m_zoneStart; m_zoneStart += move; m_zoneEnd += move; } int min = qMin(m_zoneStart, zoneStart); int max = qMax(m_zoneEnd, zoneEnd); update(min * m_factor - m_offset - 2, 0, (max - min + 1) * m_factor + 4, height()); } else { int pos = (int)((event->x() + m_offset)); if (event->y() <= 10) { setCursor(Qt::ArrowCursor); } else if (qAbs(pos - m_zoneStart * m_factor) < 4) { setCursor(QCursor(Qt::SizeHorCursor)); if (KdenliveSettings::frametimecode()) setToolTip(i18n("Zone start: %1", m_zoneStart)); else setToolTip(i18n("Zone start: %1", m_timecode.getTimecodeFromFrames(m_zoneStart))); } else if (qAbs(pos - (m_zoneEnd + 1) * m_factor) < 4) { setCursor(QCursor(Qt::SizeHorCursor)); if (KdenliveSettings::frametimecode()) setToolTip(i18n("Zone end: %1", m_zoneEnd)); else setToolTip(i18n("Zone end: %1", m_timecode.getTimecodeFromFrames(m_zoneEnd))); } else if (qAbs(pos - (m_zoneStart + (m_zoneEnd - m_zoneStart) / 2.0) * m_factor) < 4) { setCursor(Qt::SizeHorCursor); if (KdenliveSettings::frametimecode()) setToolTip(i18n("Zone duration: %1", m_zoneEnd - m_zoneStart)); else setToolTip(i18n("Zone duration: %1", m_timecode.getTimecodeFromFrames(m_zoneEnd - m_zoneStart))); } else { setCursor(Qt::ArrowCursor); if (KdenliveSettings::frametimecode()) setToolTip(i18n("Position: %1", (int)(pos / m_factor))); else setToolTip(i18n("Position: %1", m_timecode.getTimecodeFromFrames(pos / m_factor))); } } } // virtual void CustomRuler::wheelEvent(QWheelEvent * e) { int delta = 1; m_view->activateMonitor(); if (e->modifiers() == Qt::ControlModifier) delta = m_timecode.fps(); if (e->delta() < 0) delta = 0 - delta; m_view->moveCursorPos(delta); } int CustomRuler::inPoint() const { return m_zoneStart; } int CustomRuler::outPoint() const { return m_zoneEnd; } void CustomRuler::slotMoveRuler(int newPos) { if (m_offset != newPos) { m_offset = newPos; update(); } } int CustomRuler::offset() const { return m_offset; } void CustomRuler::slotCursorMoved(int oldpos, int newpos) { int min = qMin(oldpos, newpos); int max = qMax(oldpos, newpos); m_headPosition = newpos; update(min * m_factor - m_offset - FONT_WIDTH, BIG_MARK_X, (max - min) * m_factor + FONT_WIDTH * 2 + 2, MAX_HEIGHT - BIG_MARK_X); } void CustomRuler::updateRuler(int pos) { int x = m_headPosition; m_headPosition = pos; if (x == SEEK_INACTIVE) x = pos; int min = qMin(x, m_headPosition); int max = qMax(x, m_headPosition); update(min * m_factor - offset() - 3, BIG_MARK_X, (max - min) * m_factor + 6, MAX_HEIGHT - BIG_MARK_X); } void CustomRuler::setPixelPerMark(int rate, bool force) { if (rate < 0 || (rate == m_rate && !force)) return; int scale = comboScale[rate]; m_rate = rate; m_factor = 1.0 / (double) scale * FRAME_SIZE; m_scale = 1.0 / (double) scale; double fend = m_scale * littleMarkDistance; int textFactor = 1; int timeLabelSize = QWidget::fontMetrics().boundingRect(QStringLiteral("00:00:00:000")).width(); if (timeLabelSize > littleMarkDistance) { textFactor = timeLabelSize / littleMarkDistance + 1; } if (rate > 8) { mediumMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 60; bigMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 300; } else if (rate > 6) { mediumMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 10; bigMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 30; } else if (rate > 3) { mediumMarkDistance = (double) FRAME_SIZE * m_timecode.fps(); bigMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 5; } else { mediumMarkDistance = (double) FRAME_SIZE * m_timecode.fps(); bigMarkDistance = (double) FRAME_SIZE * m_timecode.fps() * 60; } m_textSpacing = fend * textFactor; if (m_textSpacing < timeLabelSize) { int roundedFps = (int) (m_timecode.fps() + 0.5); int factor = timeLabelSize / m_textSpacing; if (factor < 2) { m_textSpacing *= 2; } else if (factor < 5) { m_textSpacing *= 5; } else if (factor < 10) { m_textSpacing *= 10; } else if (factor < roundedFps) { m_textSpacing *= roundedFps; } else if (factor < 2 * roundedFps) { m_textSpacing *= 2 * roundedFps; } else if (factor < 5 * roundedFps) { m_textSpacing *= 5 * roundedFps; } else if (factor < 10 * roundedFps) { m_textSpacing *= 10 * roundedFps; } else if (factor < 20 * roundedFps) { m_textSpacing *= 20 * roundedFps; } else if (factor < 60 * roundedFps) { m_textSpacing *= 60 * roundedFps; } else if (factor < 120 * roundedFps) { m_textSpacing *= 120 * roundedFps; } else if (factor < 150 * roundedFps) { m_textSpacing *= 150 * roundedFps; } else if (factor < 300 * roundedFps) { m_textSpacing *= 300 * roundedFps; } else { factor /= (300 * roundedFps); m_textSpacing *= (factor + 1) * (300 * roundedFps); } } update(); } void CustomRuler::setDuration(int d) { int oldduration = m_duration; m_duration = d; update(qMin(oldduration, m_duration) * m_factor - 1 - offset(), 0, qAbs(oldduration - m_duration) * m_factor + 2, height()); } // virtual void CustomRuler::paintEvent(QPaintEvent *e) { QStylePainter p(this); const QRect &paintRect = e->rect(); p.setClipRect(paintRect); p.fillRect(paintRect, palette().midlight().color()); // Draw zone background const int zoneStart = (int)(m_zoneStart * m_factor); const int zoneEnd = (int)((m_zoneEnd + 1)* m_factor); p.fillRect(zoneStart - m_offset, LABEL_SIZE + 2, zoneEnd - zoneStart, MAX_HEIGHT - LABEL_SIZE - 2, m_zoneBG); double f, fend; const int offsetmax = ((paintRect.right() + m_offset) / FRAME_SIZE + 1) * FRAME_SIZE; int offsetmin; p.setPen(palette().text().color()); // draw time labels if (paintRect.y() < LABEL_SIZE) { offsetmin = (paintRect.left() + m_offset) / m_textSpacing; offsetmin = offsetmin * m_textSpacing; for (f = offsetmin; f < offsetmax; f += m_textSpacing) { QString lab; if (KdenliveSettings::frametimecode()) lab = QString::number((int)(f / m_factor + 0.5)); else lab = m_timecode.getTimecodeFromFrames((int)(f / m_factor + 0.5)); p.drawText(f - m_offset + 2, LABEL_SIZE, lab); } } p.setPen(palette().dark().color()); offsetmin = (paintRect.left() + m_offset) / littleMarkDistance; offsetmin = offsetmin * littleMarkDistance; // draw the little marks fend = m_scale * littleMarkDistance; if (fend > 5) { QLineF l(offsetmin - m_offset, LITTLE_MARK_X, offsetmin - m_offset, MAX_HEIGHT); for (f = offsetmin; f < offsetmax; f += fend) { l.translate(fend, 0); p.drawLine(l); } } offsetmin = (paintRect.left() + m_offset) / mediumMarkDistance; offsetmin = offsetmin * mediumMarkDistance; // draw medium marks fend = m_scale * mediumMarkDistance; if (fend > 5) { QLineF l(offsetmin - m_offset - fend, MIDDLE_MARK_X, offsetmin - m_offset - fend, MAX_HEIGHT); for (f = offsetmin - fend; f < offsetmax + fend; f += fend) { l.translate(fend, 0); p.drawLine(l); } } offsetmin = (paintRect.left() + m_offset) / bigMarkDistance; offsetmin = offsetmin * bigMarkDistance; // draw big marks fend = m_scale * bigMarkDistance; if (fend > 5) { QLineF l(offsetmin - m_offset, BIG_MARK_X, offsetmin - m_offset, MAX_HEIGHT); for (f = offsetmin; f < offsetmax; f += fend) { l.translate(fend, 0); p.drawLine(l); } } // draw zone cursors if (zoneStart > 0) { QPolygon pa(4); pa.setPoints(4, zoneStart - m_offset + FONT_WIDTH / 2, LABEL_SIZE + 2, zoneStart - m_offset, LABEL_SIZE + 2, zoneStart - m_offset, MAX_HEIGHT - 1, zoneStart - m_offset + FONT_WIDTH / 2, MAX_HEIGHT - 1); p.drawPolyline(pa); } if (zoneEnd > 0) { QColor center(Qt::white); center.setAlpha(150); QRect rec(zoneStart - m_offset + (zoneEnd - zoneStart) / 2 - 4, LABEL_SIZE + 2, 8, MAX_HEIGHT - LABEL_SIZE - 3); p.fillRect(rec, center); p.drawRect(rec); QPolygon pa(4); pa.setPoints(4, zoneEnd - m_offset - FONT_WIDTH / 2, LABEL_SIZE + 2, zoneEnd - m_offset, LABEL_SIZE + 2, zoneEnd - m_offset, MAX_HEIGHT - 1, zoneEnd - m_offset - FONT_WIDTH / 2, MAX_HEIGHT - 1); p.drawPolyline(pa); } // draw Rendering preview zones QColor preview(Qt::green); preview.setAlpha(120); foreach(int frame, m_renderingPreviews) { QRectF rec(frame * m_factor - m_offset, MAX_HEIGHT - 3, KdenliveSettings::timelinechunks() * m_factor, 3); p.fillRect(rec, preview); } preview = Qt::darkRed; preview.setAlpha(120); foreach(int frame, m_dirtyRenderingPreviews) { QRectF rec(frame * m_factor - m_offset, MAX_HEIGHT - 3, KdenliveSettings::timelinechunks() * m_factor, 3); p.fillRect(rec, preview); } // draw pointer const int value = m_view->cursorPos() * m_factor - m_offset; QPolygon pa(3); pa.setPoints(3, value - FONT_WIDTH, LABEL_SIZE + 3, value + FONT_WIDTH, LABEL_SIZE + 3, value, MAX_HEIGHT); p.setBrush(palette().brush(QPalette::Text)); p.setPen(Qt::NoPen); p.drawPolygon(pa); if (m_headPosition == m_view->cursorPos()) { m_headPosition = SEEK_INACTIVE; } if (m_headPosition != SEEK_INACTIVE) { p.fillRect(m_headPosition * m_factor - m_offset - 1, BIG_MARK_X + 1, 3, MAX_HEIGHT - BIG_MARK_X - 1, palette().linkVisited()); } } void CustomRuler::activateZone() { m_zoneBG.setAlpha(KdenliveSettings::useTimelineZoneToEdit() ? 180 : 60); update(); } +bool CustomRuler::isUnderPreview(int start, int end) +{ + QList allPreviews; + allPreviews << m_renderingPreviews << m_dirtyRenderingPreviews; + qSort(allPreviews); + foreach (int ix, allPreviews) { + if (ix >= start && ix <= end) { + return true; + } + } + return false; +} + bool CustomRuler::updatePreview(int frame, bool rendered, bool refresh) { bool result = false; if (rendered) { m_renderingPreviews << frame; m_dirtyRenderingPreviews.removeAll(frame); } else { if (m_renderingPreviews.removeAll(frame) > 0) { m_dirtyRenderingPreviews << frame; result = true; } } + std::sort(m_renderingPreviews.begin(), m_renderingPreviews.end()); + std::sort(m_dirtyRenderingPreviews.begin(), m_dirtyRenderingPreviews.end()); if (refresh) update(frame * m_factor - offset(), MAX_HEIGHT - 3, KdenliveSettings::timelinechunks() * m_factor + 1, 3); return result; } void CustomRuler::updatePreviewDisplay(int start, int end) { update(start * m_factor - offset(), MAX_HEIGHT - 3, (end - start) * KdenliveSettings::timelinechunks() * m_factor + 1, 3); } const QStringList CustomRuler::previewChunks() const { QStringList resultChunks; QString clean; QString dirty; foreach(int frame, m_renderingPreviews) { clean += QString::number(frame) + QStringLiteral(","); } foreach(int frame, m_dirtyRenderingPreviews) { dirty += QString::number(frame) + QStringLiteral(","); } resultChunks << clean << dirty; return resultChunks; } const QList CustomRuler::getDirtyChunks() const { return m_dirtyRenderingPreviews; } bool CustomRuler::hasPreviewRange() const { return (!m_dirtyRenderingPreviews.isEmpty() || !m_renderingPreviews.isEmpty()); } QList CustomRuler::addChunks(QList chunks, bool add) { qSort(chunks); QList toProcess; if (add) { foreach(int frame, chunks) { if (m_renderingPreviews.contains(frame)) { // already rendered, ignore continue; } if (m_dirtyRenderingPreviews.contains(frame)) { continue; } m_dirtyRenderingPreviews << frame; // This is a new dirty chunk toProcess << frame; } } else { foreach(int frame, chunks) { if (m_renderingPreviews.removeAll(frame) > 0) { // A preview file existed for this chunk, ask deletion toProcess << frame; } m_dirtyRenderingPreviews.removeAll(frame); } } + std::sort(m_renderingPreviews.begin(), m_renderingPreviews.end()); + std::sort(m_dirtyRenderingPreviews.begin(), m_dirtyRenderingPreviews.end()); update(chunks.first() * m_factor - offset(), MAX_HEIGHT - 3, (chunks.last() - chunks.first()) * KdenliveSettings::timelinechunks() * m_factor + 1, 3); return toProcess; } diff --git a/src/timeline/customruler.h b/src/timeline/customruler.h index 5602c914d..245287b96 100644 --- a/src/timeline/customruler.h +++ b/src/timeline/customruler.h @@ -1,115 +1,116 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ /** * @class CustomRuler * @author Jean-Baptiste Mardelle * @brief Manages the timeline ruler. */ #ifndef CUSTOMRULER_H #define CUSTOMRULER_H #include #include "timeline/customtrackview.h" #include "timecode.h" enum RULER_MOVE { RULER_CURSOR = 0, RULER_START = 1, RULER_MIDDLE = 2, RULER_END = 3 }; enum MOUSE_MOVE { NO_MOVE = 0, HORIZONTAL_MOVE = 1, VERTICAL_MOVE = 2 }; class CustomRuler : public QWidget { Q_OBJECT public: CustomRuler(const Timecode &tc, const QList &rulerActions, CustomTrackView *parent); void setPixelPerMark(int rate, bool force = false); static const int comboScale[]; int outPoint() const; int inPoint() const; void setDuration(int d); void setZone(const QPoint &p); int offset() const; void updateProjectFps(const Timecode &t); void updateFrameSize(); void activateZone(); bool updatePreview(int frame, bool rendered = true, bool refresh = false); /** @brief Returns a list of rendered timeline preview chunks */ const QStringList previewChunks() const; /** @brief Returns a list of dirty timeline preview chunks (that need to be generated) */ const QList getDirtyChunks() const; QList addChunks(QList chunks, bool add); /** @brief Returns true if a timeline preview zone has already be defined */ bool hasPreviewRange() const; /** @brief Refresh timeline preview range */ void updatePreviewDisplay(int start, int end); + bool isUnderPreview(int start, int end); protected: void paintEvent(QPaintEvent * /*e*/); void wheelEvent(QWheelEvent * e); void mousePressEvent(QMouseEvent * event); void mouseReleaseEvent(QMouseEvent * event); void mouseMoveEvent(QMouseEvent * event); private: Timecode m_timecode; CustomTrackView *m_view; int m_zoneStart; int m_zoneEnd; int m_duration; double m_textSpacing; double m_factor; double m_scale; int m_offset; /** @brief the position of the seek point */ int m_headPosition; QColor m_zoneBG; RULER_MOVE m_moveCursor; QMenu *m_contextMenu; QAction *m_editGuide; QAction *m_deleteGuide; int m_clickedGuide; /** Used for zooming through vertical move */ QPoint m_clickPoint; int m_rate; int m_startRate; MOUSE_MOVE m_mouseMove; QMenu *m_goMenu; QList m_renderingPreviews; QList m_dirtyRenderingPreviews; public slots: void slotMoveRuler(int newPos); void slotCursorMoved(int oldpos, int newpos); void updateRuler(int pos); private slots: void slotEditGuide(); void slotDeleteGuide(); void slotGoToGuide(QAction *act); signals: void zoneMoved(int, int); void adjustZoom(int); void mousePosition(int); void seekCursorPos(int); }; #endif diff --git a/src/timeline/managers/previewmanager.cpp b/src/timeline/managers/previewmanager.cpp index d21c519e8..600e6aa40 100644 --- a/src/timeline/managers/previewmanager.cpp +++ b/src/timeline/managers/previewmanager.cpp @@ -1,314 +1,423 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "previewmanager.h" #include "../customruler.h" #include "kdenlivesettings.h" #include "doc/kdenlivedoc.h" #include #include #include -PreviewManager::PreviewManager(KdenliveDoc *doc, CustomRuler *ruler) : QObject() +PreviewManager::PreviewManager(KdenliveDoc *doc, CustomRuler *ruler, Mlt::Tractor *tractor) : QObject() , m_doc(doc) , m_ruler(ruler) + , m_tractor(tractor) + , m_previewTrack(NULL) , m_initialized(false) , m_abortPreview(false) { + m_previewGatherTimer.setSingleShot(true); + m_previewGatherTimer.setInterval(200); } PreviewManager::~PreviewManager() { if (m_initialized) { abortRendering(); m_undoDir.removeRecursively(); if (m_cacheDir.entryList(QDir::NoDotAndDotDot).count() == 0) { m_cacheDir.removeRecursively(); } } + delete m_previewTrack; } bool PreviewManager::initialize() { QString documentId = m_doc->getDocumentProperty(QStringLiteral("documentid")); m_initialized = true; if (documentId.isEmpty() || documentId.toLong() == 0) { // Something is wrong, documentId should be a number (ms since epoch), abort return false; } QString cacheDir = m_doc->getDocumentProperty(QStringLiteral("cachedir")); if (!cacheDir.isEmpty() && QFile::exists(cacheDir)) { m_cacheDir = QDir(cacheDir); } else { m_cacheDir = QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); m_cacheDir.mkdir(documentId); if (!m_cacheDir.cd(documentId)) { return false; } } if (m_cacheDir.dirName() != documentId || (!m_cacheDir.exists("undo") && !m_cacheDir.mkdir("undo"))) { // TODO: cannot create undo folder, abort return false; } if (!loadParams()) { return false; } m_doc->setDocumentProperty(QStringLiteral("cachedir"), m_cacheDir.absolutePath()); m_undoDir = QDir(m_cacheDir.absoluteFilePath("undo")); connect(this, &PreviewManager::cleanupOldPreviews, this, &PreviewManager::doCleanupOldPreviews); - connect(m_doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo); + connect(m_doc, &KdenliveDoc::removeInvalidUndo, this, &PreviewManager::slotRemoveInvalidUndo, Qt::DirectConnection); m_previewTimer.setSingleShot(true); m_previewTimer.setInterval(3000); connect(&m_previewTimer, &QTimer::timeout, this, &PreviewManager::startPreviewRender); + connect(this, &PreviewManager::previewRender, this, &PreviewManager::gotPreviewRender); + connect(&m_previewGatherTimer, &QTimer::timeout, this, &PreviewManager::slotProcessDirtyChunks); m_initialized = true; return true; } +bool PreviewManager::buildPreviewTrack() +{ + if (m_previewTrack) + return false; + // Create overlay track + m_previewTrack = new Mlt::Playlist(*m_tractor->profile()); + int trackIndex = m_tractor->count(); + m_tractor->lock(); + m_tractor->insert_track(*m_previewTrack, trackIndex); + Mlt::Producer *tk = m_tractor->track(trackIndex); + tk->set("hide", 2); + delete tk; + m_tractor->unlock(); + return true; +} + +void PreviewManager::reconnectTrack() +{ + if (m_previewTrack) { + m_tractor->insert_track(*m_previewTrack, m_tractor->count()); + } +} + +void PreviewManager::disconnectTrack() +{ + if (m_previewTrack) { + m_tractor->remove_track(m_tractor->count() - 1); + } +} + bool PreviewManager::loadParams() { m_extension= m_doc->getDocumentProperty(QStringLiteral("previewextension")); m_consumerParams = m_doc->getDocumentProperty(QStringLiteral("previewparameters")).split(" "); if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { m_doc->selectPreviewProfile(); m_consumerParams = m_doc->getDocumentProperty(QStringLiteral("previewparameters")).split(" "); m_extension= m_doc->getDocumentProperty(QStringLiteral("previewextension")); } if (m_consumerParams.isEmpty() || m_extension.isEmpty()) { return false; } m_consumerParams << "an=1"; return true; } void PreviewManager::invalidatePreviews(QList chunks) { // We are not at the bottom of undo stack, chunks have already been archived previously QMutexLocker lock(&m_previewMutex); bool timer = false; if (m_previewTimer.isActive()) { m_previewTimer.stop(); timer = true; } int stackIx = m_doc->commandStack()->index(); int stackMax = m_doc->commandStack()->count(); - abortRendering(); if (stackIx == stackMax && !m_undoDir.exists(QString::number(stackIx - 1))) { // We just added a new command in stack, archive existing chunks int ix = stackIx - 1; m_undoDir.mkdir(QString::number(ix)); bool foundPreviews = false; foreach(int i, chunks) { QString current = QString("%1.%2").arg(i).arg(m_extension); if (m_cacheDir.rename(current, QString("undo/%1/%2").arg(ix).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { // No preview files found, remove undo folder m_undoDir.rmdir(QString::number(ix)); } else { // new chunks archived, cleanup old ones emit cleanupOldPreviews(); } } else { // Restore existing chunks, delete others // Check if we just undo the last stack action, then backup, otherwise delete bool lastUndo = false; if (stackIx == stackMax - 1) { if (!m_undoDir.exists(QString::number(stackMax))) { lastUndo = true; bool foundPreviews = false; m_undoDir.mkdir(QString::number(stackMax)); foreach(int i, chunks) { QString current = QString("%1.%2").arg(i).arg(m_extension); if (m_cacheDir.rename(current, QString("undo/%1/%2").arg(stackMax).arg(current))) { foundPreviews = true; } } if (!foundPreviews) { m_undoDir.rmdir(QString::number(stackMax)); } } } bool moveFile = true; QDir tmpDir = m_undoDir; if (!tmpDir.cd(QString::number(stackIx))) { moveFile = false; } QList foundChunks; foreach(int i, chunks) { QString cacheFileName = QString("%1.%2").arg(i).arg(m_extension); if (!lastUndo) { m_cacheDir.remove(cacheFileName); } if (moveFile) { if (QFile::copy(tmpDir.absoluteFilePath(cacheFileName), m_cacheDir.absoluteFilePath(cacheFileName))) { foundChunks << i; } } } qSort(foundChunks); - emit reloadChunks(m_cacheDir, foundChunks, m_extension); + slotReloadChunks(m_cacheDir, foundChunks, m_extension); } m_doc->setModified(true); if (timer) m_previewTimer.start(); } void PreviewManager::doCleanupOldPreviews() { QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); qSort(dirs); while (dirs.count() > 5) { QString dir = dirs.takeFirst(); QDir tmp = m_undoDir; if (tmp.cd(dir)) { tmp.removeRecursively(); } } } void PreviewManager::addPreviewRange(bool add) { QPoint p = m_doc->zone(); int chunkSize = KdenliveSettings::timelinechunks(); int startChunk = p.x() / chunkSize; int endChunk = rintl(p.y() / chunkSize); QList frames; for (int i = startChunk; i <= endChunk; i++) { frames << i * chunkSize; } QList toProcess = m_ruler->addChunks(frames, add); if (toProcess.isEmpty()) return; if (add) { - //TODO: optimize, don't abort rendering process, just add required frames - if (KdenliveSettings::autopreview()) + if (m_previewThread.isRunning()) { + // just add required frames to current rendering job + m_waitingThumbs << toProcess; + } else if (KdenliveSettings::autopreview()) m_previewTimer.start(); } else { // Remove processed chunks foreach(int ix, toProcess) { m_cacheDir.remove(QString("%1.%2").arg(ix).arg(m_extension)); } } } void PreviewManager::abortRendering() { if (!m_previewThread.isRunning()) return; m_abortPreview = true; emit abortPreview(); m_previewThread.waitForFinished(); } void PreviewManager::startPreviewRender() { if (!m_ruler->hasPreviewRange()) { addPreviewRange(true); } QList chunks = m_ruler->getDirtyChunks(); if (!chunks.isEmpty()) { // Abort any rendering abortRendering(); + m_waitingThumbs.clear(); const QString sceneList = m_cacheDir.absoluteFilePath(QStringLiteral("preview.mlt")); m_doc->saveMltPlaylist(sceneList); - m_previewThread = QtConcurrent::run(this, &PreviewManager::doPreviewRender, sceneList, chunks); + m_waitingThumbs = chunks; + m_previewThread = QtConcurrent::run(this, &PreviewManager::doPreviewRender, sceneList); } } -void PreviewManager::doPreviewRender(QString scene, QList chunks) +void PreviewManager::doPreviewRender(QString scene) { int progress; int chunkSize = KdenliveSettings::timelinechunks(); // initialize progress bar emit previewRender(0, QString(), 0); int ct = 0; - qSort(chunks); - while (!chunks.isEmpty()) { - int i = chunks.takeFirst(); + qSort(m_waitingThumbs); + while (!m_waitingThumbs.isEmpty()) { + int i = m_waitingThumbs.takeFirst(); ct++; QString fileName = QString("%1.%2").arg(i).arg(m_extension); - if (chunks.isEmpty()) { + if (m_waitingThumbs.isEmpty()) { progress = 1000; } else { - progress = (double) (ct) / (ct + chunks.count()) * 1000; + progress = (double) (ct) / (ct + m_waitingThumbs.count()) * 1000; } if (m_cacheDir.exists(fileName)) { // This chunk already exists emit previewRender(i, m_cacheDir.absoluteFilePath(fileName), progress); continue; } // Build rendering process QStringList args; args << scene; args << "in=" + QString::number(i); args << "out=" + QString::number(i + chunkSize - 1); args << "-consumer" << "avformat:" + m_cacheDir.absoluteFilePath(fileName); args << m_consumerParams; QProcess previewProcess; connect(this, SIGNAL(abortPreview()), &previewProcess, SLOT(kill()), Qt::DirectConnection); previewProcess.start(KdenliveSettings::rendererpath(), args); if (previewProcess.waitForStarted()) { previewProcess.waitForFinished(-1); if (previewProcess.exitStatus() != QProcess::NormalExit) { // Something went wrong if (m_abortPreview) { emit previewRender(0, QString(), 1000); } else { qDebug()<<"+++++++++\n++ ERROR ++\n++++++"; emit previewRender(i, QString(), -1); } QFile::remove(m_cacheDir.absoluteFilePath(fileName)); break; } else { emit previewRender(i, m_cacheDir.absoluteFilePath(fileName), progress); } } } QFile::remove(scene); m_abortPreview = false; } void PreviewManager::slotProcessDirtyChunks() { QList chunks = m_ruler->getDirtyChunks(); + if (chunks.isEmpty()) + return; invalidatePreviews(chunks); m_ruler->updatePreviewDisplay(chunks.first(), chunks.last()); if (KdenliveSettings::autopreview()) m_previewTimer.start(); } void PreviewManager::slotRemoveInvalidUndo(int ix) { + QMutexLocker lock(&m_previewMutex); QStringList dirs = m_undoDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); qSort(dirs); foreach(const QString dir, dirs) { if (dir.toInt() >= ix) { QDir tmp = m_undoDir; if (tmp.cd(dir)) { tmp.removeRecursively(); } } } } +void PreviewManager::invalidatePreview(int startFrame, int endFrame) +{ + int chunkSize = KdenliveSettings::timelinechunks(); + int start = startFrame / chunkSize; + int end = lrintf(endFrame / chunkSize); + start *= chunkSize; + end *= chunkSize; + if (!m_ruler->isUnderPreview(start, end)) { + return; + } + m_previewGatherTimer.stop(); + abortPreview(); + m_tractor->lock(); + for (int i = start; i <=end; i+= chunkSize) { + if (m_ruler->updatePreview(i, false)) { + int ix = m_previewTrack->get_clip_index_at(i); + if (m_previewTrack->is_blank(ix)) + continue; + Mlt::Producer *prod = m_previewTrack->replace_with_blank(ix); + delete prod; + } + } + m_previewTrack->consolidate_blanks(); + m_tractor->unlock(); + m_previewGatherTimer.start(); +} + +void PreviewManager::slotReloadChunks(QDir cacheDir, QList chunks, const QString ext) +{ + m_tractor->lock(); + foreach(int ix, chunks) { + if (m_previewTrack->is_blank_at(ix)) { + const QString fileName = cacheDir.absoluteFilePath(QString("%1.%2").arg(ix).arg(ext)); + Mlt::Producer prod(*m_tractor->profile(), 0, fileName.toUtf8().constData()); + if (prod.is_valid()) { + m_ruler->updatePreview(ix, true); + prod.set("mlt_service", "avformat-novalidate"); + m_previewTrack->insert_at(ix, &prod, 1); + } + } + } + m_ruler->updatePreviewDisplay(chunks.first(), chunks.last()); + m_previewTrack->consolidate_blanks(); + m_tractor->unlock(); +} + +void PreviewManager::gotPreviewRender(int frame, const QString &file, int progress) +{ + if (file.isEmpty()) { + m_doc->previewProgress(progress); + return; + } + m_tractor->lock(); + if (m_previewTrack->is_blank_at(frame)) { + Mlt::Producer prod(*m_tractor->profile(), 0, file.toUtf8().constData()); + if (prod.is_valid()) { + m_ruler->updatePreview(frame, true, true); + prod.set("mlt_service", "avformat-novalidate"); + m_previewTrack->insert_at(frame, &prod, 1); + } + } + m_previewTrack->consolidate_blanks(); + m_tractor->unlock(); + m_doc->previewProgress(progress); + m_doc->setModified(true); +} diff --git a/src/timeline/managers/previewmanager.h b/src/timeline/managers/previewmanager.h index 47f0adab9..b785a1511 100644 --- a/src/timeline/managers/previewmanager.h +++ b/src/timeline/managers/previewmanager.h @@ -1,82 +1,96 @@ /*************************************************************************** * Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef PREVIEWMANAGER_H #define PREVIEWMANAGER_H #include "definitions.h" #include #include #include #include class KdenliveDoc; class CustomRuler; +namespace Mlt { + class Tractor; + class Playlist; +} + /** * @namespace PreviewManager * @brief Handles timeline preview. */ class PreviewManager : public QObject { Q_OBJECT public: - explicit PreviewManager(KdenliveDoc *doc, CustomRuler *ruler); + explicit PreviewManager(KdenliveDoc *doc, CustomRuler *ruler, Mlt::Tractor *tractor); virtual ~PreviewManager(); /** @brief: initialize base variables, return false if error. */ bool initialize(); void invalidatePreviews(QList chunks); void addPreviewRange(bool add); void abortRendering(); bool loadParams(); + void invalidatePreview(int startFrame, int endFrame); + bool buildPreviewTrack(); + void reconnectTrack(); + void disconnectTrack(); private: KdenliveDoc *m_doc; CustomRuler *m_ruler; + Mlt::Tractor *m_tractor; + Mlt::Playlist *m_previewTrack; QDir m_cacheDir; QDir m_undoDir; QMutex m_previewMutex; QStringList m_consumerParams; QString m_extension; QTimer m_previewTimer; + QTimer m_previewGatherTimer; bool m_initialized; bool m_abortPreview; + QList m_waitingThumbs; QFuture m_previewThread; private slots: void doCleanupOldPreviews(); - void doPreviewRender(QString scene, QList chunks); + void doPreviewRender(QString scene); void slotRemoveInvalidUndo(int ix); + void slotReloadChunks(QDir cacheDir, QList chunks, const QString ext); + void slotProcessDirtyChunks(); public slots: - void slotProcessDirtyChunks(); void startPreviewRender(); + void gotPreviewRender(int frame, const QString &file, int progress); signals: void abortPreview(); void cleanupOldPreviews(); void previewRender(int frame, const QString &file, int progress); - void reloadChunks(QDir, QList , const QString ext); }; #endif diff --git a/src/timeline/timeline.cpp b/src/timeline/timeline.cpp index 737008214..89c61418d 100644 --- a/src/timeline/timeline.cpp +++ b/src/timeline/timeline.cpp @@ -1,1965 +1,1895 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * Copyright (C) 2015 by Vincent Pinon (vpinon@kde.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "timeline.h" #include "track.h" #include "clip.h" #include "renderer.h" #include "headertrack.h" #include "clipitem.h" #include "transition.h" #include "transitionhandler.h" #include "timelinecommands.h" #include "customruler.h" #include "customtrackview.h" #include "dialogs/profilesdialog.h" #include "mltcontroller/clipcontroller.h" #include "bin/projectclip.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "doc/kdenlivedoc.h" #include "utils/KoIconUtils.h" #include "project/clipmanager.h" #include "effectslist/initeffects.h" #include "mltcontroller/effectscontroller.h" #include "managers/previewmanager.h" #include #include #include #include #include #include Timeline::Timeline(KdenliveDoc *doc, const QList &actions, const QList &rulerActions, bool *ok, QWidget *parent) : QWidget(parent), multitrackView(false) , videoTarget(-1) , audioTarget(-1) , m_hasOverlayTrack(false) , m_overlayTrack(NULL) , m_scale(1.0) , m_doc(doc) , m_verticalZoom(1) , m_timelinePreview(NULL) + , m_usePreview(false) { m_trackActions << actions; setupUi(this); // ruler_frame->setMaximumHeight(); // size_frame->setMaximumHeight(); m_scene = new CustomTrackScene(this); m_trackview = new CustomTrackView(doc, this, m_scene, parent); if (m_doc->setSceneList() == -1) *ok = false; else *ok = true; Mlt::Service s(m_doc->renderer()->getProducer()->parent().get_service()); m_tractor = new Mlt::Tractor(s); m_ruler = new CustomRuler(doc->timecode(), rulerActions, m_trackview); connect(m_ruler, SIGNAL(zoneMoved(int,int)), this, SIGNAL(zoneMoved(int,int))); connect(m_ruler, SIGNAL(adjustZoom(int)), this, SIGNAL(setZoom(int))); connect(m_ruler, SIGNAL(mousePosition(int)), this, SIGNAL(mousePosition(int))); connect(m_ruler, SIGNAL(seekCursorPos(int)), m_doc->renderer(), SLOT(seek(int)), Qt::QueuedConnection); QHBoxLayout *layout = new QHBoxLayout; layout->setContentsMargins(m_trackview->frameWidth(), 0, 0, 0); layout->setSpacing(0); ruler_frame->setLayout(layout); ruler_frame->setMaximumHeight(m_ruler->height()); layout->addWidget(m_ruler); QHBoxLayout *sizeLayout = new QHBoxLayout; sizeLayout->setContentsMargins(0, 0, 0, 0); sizeLayout->setSpacing(0); size_frame->setLayout(sizeLayout); size_frame->setMaximumHeight(m_ruler->height()); QToolButton *butSmall = new QToolButton(this); butSmall->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-zoom-small"))); butSmall->setToolTip(i18n("Smaller tracks")); butSmall->setAutoRaise(true); connect(butSmall, SIGNAL(clicked()), this, SLOT(slotVerticalZoomDown())); sizeLayout->addWidget(butSmall); QToolButton *butLarge = new QToolButton(this); butLarge->setIcon(KoIconUtils::themedIcon(QStringLiteral("kdenlive-zoom-large"))); butLarge->setToolTip(i18n("Bigger tracks")); butLarge->setAutoRaise(true); connect(butLarge, SIGNAL(clicked()), this, SLOT(slotVerticalZoomUp())); sizeLayout->addWidget(butLarge); QToolButton *enableZone = new QToolButton(this); KDualAction *ac = new KDualAction(i18n("Don't Use Timeline Zone for Insert"), i18n("Use Timeline Zone for Insert"), this); ac->setActiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-on"))); ac->setInactiveIcon(KoIconUtils::themedIcon(QStringLiteral("timeline-use-zone-off"))); enableZone->setAutoRaise(true); ac->setActive(KdenliveSettings::useTimelineZoneToEdit()); enableZone->setDefaultAction(ac); connect(ac, &KDualAction::activeChangedByUser, this, &Timeline::slotEnableZone); sizeLayout->addWidget(enableZone); m_doc->doAddAction(QStringLiteral("use_timeline_zone_in_edit"), ac, Qt::Key_G); QHBoxLayout *tracksLayout = new QHBoxLayout; tracksLayout->setContentsMargins(0, 0, 0, 0); tracksLayout->setSpacing(0); tracks_frame->setLayout(tracksLayout); headers_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); headers_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); QVBoxLayout *headersLayout = new QVBoxLayout; headersLayout->setContentsMargins(0, m_trackview->frameWidth(), 0, 0); headersLayout->setSpacing(0); headers_container->setLayout(headersLayout); connect(headers_area->verticalScrollBar(), SIGNAL(valueChanged(int)), m_trackview->verticalScrollBar(), SLOT(setValue(int))); tracksLayout->addWidget(m_trackview); connect(m_trackview->verticalScrollBar(), SIGNAL(valueChanged(int)), headers_area->verticalScrollBar(), SLOT(setValue(int))); connect(m_trackview, SIGNAL(tracksChanged()), this, SLOT(slotReloadTracks())); connect(m_trackview, SIGNAL(updateTrackHeaders()), this, SLOT(slotRepaintTracks())); connect(m_trackview, SIGNAL(showTrackEffects(int,TrackInfo)), this, SIGNAL(showTrackEffects(int,TrackInfo))); connect(m_trackview, SIGNAL(updateTrackEffectState(int)), this, SLOT(slotUpdateTrackEffectState(int))); transitionHandler = new TransitionHandler(m_tractor); connect(transitionHandler, &TransitionHandler::refresh, m_doc->renderer(), &Render::doRefresh); connect(m_trackview, SIGNAL(cursorMoved(int,int)), m_ruler, SLOT(slotCursorMoved(int,int))); connect(m_trackview, SIGNAL(updateRuler(int)), m_ruler, SLOT(updateRuler(int)), Qt::DirectConnection); connect(m_trackview->horizontalScrollBar(), SIGNAL(valueChanged(int)), m_ruler, SLOT(slotMoveRuler(int))); connect(m_trackview->horizontalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(slotUpdateVerticalScroll(int,int))); connect(m_trackview, SIGNAL(mousePosition(int)), this, SIGNAL(mousePosition(int))); // Timeline preview stuff initializePreview(); - m_previewGatherTimer.setSingleShot(true); - m_previewGatherTimer.setInterval(200); } Timeline::~Timeline() { if (m_timelinePreview) delete m_timelinePreview; delete m_ruler; delete m_trackview; delete m_scene; delete transitionHandler; delete m_tractor; qDeleteAll<>(m_tracks); m_tracks.clear(); } void Timeline::loadTimeline() { parseDocument(m_doc->toXml()); m_trackview->slotUpdateAllThumbs(); m_trackview->slotSelectTrack(m_trackview->getNextVideoTrack(1)); slotChangeZoom(m_doc->zoom().x(), m_doc->zoom().y()); slotSetZone(m_doc->zone(), false); loadPreviewRender(); } QMap Timeline::documentProperties() { QMap props = m_doc->documentProperties(); props.insert(QStringLiteral("audiotargettrack"), QString::number(audioTarget)); props.insert(QStringLiteral("videotargettrack"), QString::number(videoTarget)); props.insert(QStringLiteral("previewchunks"), m_ruler->previewChunks().at(0)); props.insert(QStringLiteral("dirtypreviewchunks"), m_ruler->previewChunks().at(1)); return props; } Track* Timeline::track(int i) { if (i < 0 || i >= m_tracks.count()) return NULL; return m_tracks.at(i); } int Timeline::tracksCount() const { - return m_tractor->count() - (m_hasOverlayTrack ? 1 : 0); + return m_tractor->count() - m_hasOverlayTrack - m_usePreview; } int Timeline::visibleTracksCount() const { - return m_tractor->count() - 1 - (m_hasOverlayTrack ? 1 : 0); + return m_tractor->count() - 1 - m_hasOverlayTrack - m_usePreview; } //virtual void Timeline::keyPressEvent(QKeyEvent * event) { if (event->key() == Qt::Key_Up) { m_trackview->slotTrackUp(); event->accept(); } else if (event->key() == Qt::Key_Down) { m_trackview->slotTrackDown(); event->accept(); } else QWidget::keyPressEvent(event); } int Timeline::duration() const { return m_trackview->duration(); } bool Timeline::checkProjectAudio() { bool hasAudio = false; int max = m_tracks.count(); for (int i = 0; i < max; i++) { Track *sourceTrack = track(i); QScopedPointer track(m_tractor->track(i + 1)); int state = track->get_int("hide"); if (sourceTrack && sourceTrack->hasAudio() && !(state & 2)) { hasAudio = true; break; } } return hasAudio; } int Timeline::inPoint() const { return m_ruler->inPoint(); } int Timeline::outPoint() const { return m_ruler->outPoint(); } void Timeline::slotSetZone(const QPoint &p, bool updateDocumentProperties) { m_ruler->setZone(p); if (updateDocumentProperties) m_doc->setZone(p.x(), p.y()); } void Timeline::setDuration(int dur) { m_trackview->setDuration(dur); m_ruler->setDuration(dur); } int Timeline::getTracks() { int duration = 1; qDeleteAll<>(m_tracks); m_tracks.clear(); QVBoxLayout *headerLayout = qobject_cast< QVBoxLayout* >(headers_container->layout()); QLayoutItem *child; while ((child = headerLayout->takeAt(0)) != 0) { delete child; } int clipsCount = 0; for (int i = 0; i < m_tractor->count(); ++i) { QScopedPointer track(m_tractor->track(i)); QString playlist_name = track->get("id"); if (playlist_name == QLatin1String("black_track")) continue; clipsCount += track->count(); } emit startLoadingBin(clipsCount); emit resetUsageCount(); checkTrackHeight(false); int height = KdenliveSettings::trackheight() * m_scene->scale().y() - 1; int headerWidth = 0; int offset = 0; for (int i = 0; i < m_tractor->count(); ++i) { QScopedPointer track(m_tractor->track(i)); QString playlist_name = track->get("id"); if (playlist_name == QLatin1String("playlistmain")) continue; bool isBackgroundBlackTrack = playlist_name == QLatin1String("black_track"); // check track effects Mlt::Playlist playlist(*track); int trackduration = 0; int audio = 0; Track *tk = NULL; if (!isBackgroundBlackTrack) { audio = playlist.get_int("kdenlive:audio_track"); tk = new Track(i, m_trackActions, playlist, audio == 1 ? AudioTrack : VideoTrack, this); m_tracks.append(tk); trackduration = loadTrack(i, offset, playlist); QFrame *frame = new QFrame(headers_container); frame->setFrameStyle(QFrame::HLine); frame->setFixedHeight(1); headerLayout->insertWidget(0, frame); } else { // Black track tk = new Track(i, m_trackActions, playlist, audio == 1 ? AudioTrack : VideoTrack, this); m_tracks.append(tk); } offset += track->count(); if (audio == 0 && !isBackgroundBlackTrack) { // Check if we have a composite transition for this track QScopedPointer transition(transitionHandler->getTransition(KdenliveSettings::gpu_accel() ? "movit.overlay" : "frei0r.cairoblend", i, -1, true)); if (!transition) { tk->trackHeader->disableComposite(); } } if (!isBackgroundBlackTrack) { tk->trackHeader->setTrackHeight(height); int currentWidth = tk->trackHeader->minimumWidth(); if (currentWidth > headerWidth) headerWidth = currentWidth; headerLayout->insertWidget(0, tk->trackHeader); if (trackduration > duration) duration = trackduration; tk->trackHeader->setSelectedIndex(m_trackview->selectedTrack()); connect(tk->trackHeader, &HeaderTrack::switchTrackComposite, this, &Timeline::slotSwitchTrackComposite); connect(tk->trackHeader, SIGNAL(switchTrackVideo(int,bool)), m_trackview, SLOT(slotSwitchTrackVideo(int,bool))); connect(tk->trackHeader, SIGNAL(switchTrackAudio(int,bool)), m_trackview, SLOT(slotSwitchTrackAudio(int,bool))); connect(tk->trackHeader, SIGNAL(switchTrackLock(int,bool)), m_trackview, SLOT(slotSwitchTrackLock(int,bool))); connect(tk->trackHeader, SIGNAL(selectTrack(int,bool)), m_trackview, SLOT(slotSelectTrack(int,bool))); connect(tk->trackHeader, SIGNAL(renameTrack(int,QString)), this, SLOT(slotRenameTrack(int,QString))); connect(tk->trackHeader, SIGNAL(configTrack()), this, SIGNAL(configTrack())); connect(tk->trackHeader, SIGNAL(addTrackEffect(QDomElement,int)), m_trackview, SLOT(slotAddTrackEffect(QDomElement,int))); if (playlist.filter_count()) { getEffects(playlist, NULL, i); slotUpdateTrackEffectState(i); } connect(tk, &Track::newTrackDuration, this, &Timeline::checkDuration, Qt::DirectConnection); connect(tk, SIGNAL(storeSlowMotion(QString,Mlt::Producer *)), m_doc->renderer(), SLOT(storeSlowmotionProducer(QString,Mlt::Producer *))); } } headers_container->setFixedWidth(headerWidth); if (audioTarget > -1) { m_tracks.at(audioTarget)->trackHeader->switchTarget(true); } if (videoTarget > -1) { m_tracks.at(videoTarget)->trackHeader->switchTarget(true); } updatePalette(); refreshTrackActions(); return duration; } void Timeline::checkDuration(int duration) { Q_UNUSED(duration) m_doc->renderer()->mltCheckLength(m_tractor); return; /*FIXME for (int i = 1; i < m_tractor->count(); ++i) { QScopedPointer tk(m_tractor->track(i)); int len = tk->get_playtime() - 1; if (len > duration) duration = len; } QScopedPointer tk1(m_tractor->track(0)); Mlt::Service s(tk1->get_service()); Mlt::Playlist blackTrack(s); if (blackTrack.get_playtime() - 1 != duration) { QScopedPointer blackClip(blackTrack.get_clip(0)); if (blackClip->parent().get_length() <= duration) { blackClip->parent().set("length", duration + 1); blackClip->parent().set("out", duration); blackClip->set("length", duration + 1); } blackTrack.resize_clip(0, 0, duration); } //TODO: rewind consumer if beyond duration / emit durationChanged */ } void Timeline::getTransitions() { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); mlt_service service = mlt_service_get_producer(m_tractor->get_service()); QScopedPointer field(m_tractor->field()); while (service) { Mlt::Properties prop(MLT_SERVICE_PROPERTIES(service)); if (QString(prop.get("mlt_type")) != QLatin1String("transition")) break; //skip automatic mix if (prop.get_int("internal_added") == 237) { QString trans = prop.get("mlt_service"); if (trans == QLatin1String("movit.overlay") || trans == QLatin1String("frei0r.cairoblend")) { int ix = prop.get_int("b_track"); if (ix >= 0 && ix < m_tracks.count()) { TrackInfo info = track(ix)->info(); info.composite = !prop.get_int("disable"); track(ix)->setInfo(info); } else qWarning() << "Wrong composite track index: " << ix; } else if(trans == QLatin1String("mix")) { } service = mlt_service_producer(service); continue; } int a_track = prop.get_int("a_track"); int b_track = prop.get_int("b_track"); ItemInfo transitionInfo; transitionInfo.startPos = GenTime(prop.get_int("in"), m_doc->fps()); transitionInfo.endPos = GenTime(prop.get_int("out") + 1, m_doc->fps()); transitionInfo.track = b_track; // When adding composite transition, check if it is a wipe transition if (prop.get("kdenlive_id") == NULL && QString(prop.get("mlt_service")) == QLatin1String("composite") && isSlide(prop.get("geometry"))) prop.set("kdenlive_id", "slide"); QDomElement base = MainWindow::transitions.getEffectByTag(prop.get("mlt_service"), prop.get("kdenlive_id")).cloneNode().toElement(); //check invalid parameters if (a_track > m_tractor->count() - 1) { m_documentErrors.append(i18n("Transition %1 had an invalid track: %2 > %3", prop.get("id"), a_track, m_tractor->count() - 1) + '\n'); prop.set("a_track", m_tractor->count() - 1); } if (b_track > m_tractor->count() - 1) { m_documentErrors.append(i18n("Transition %1 had an invalid track: %2 > %3", prop.get("id"), b_track, m_tractor->count() - 1) + '\n'); prop.set("b_track", m_tractor->count() - 1); } if (a_track == b_track || b_track <= 0 || transitionInfo.startPos >= transitionInfo.endPos || base.isNull() //|| !m_trackview->canBePastedTo(transitionInfo, TransitionWidget) ) { // invalid transition, remove it m_documentErrors.append(i18n("Removed invalid transition: %1", prop.get("id")) + '\n'); mlt_service disconnect = service; service = mlt_service_producer(service); mlt_field_disconnect_service(field->get_field(), disconnect); } else { QDomNodeList params = base.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); QString paramName = e.hasAttribute(QStringLiteral("tag")) ? e.attribute(QStringLiteral("tag")) : e.attribute(QStringLiteral("name")); QString value = prop.get(paramName.toUtf8().constData()); // if transition parameter has an "optional" attribute, it means that it can contain an empty value if (value.isEmpty() && !e.hasAttribute(QStringLiteral("optional"))) continue; if (e.hasAttribute("factor") || e.hasAttribute("offset")) adjustDouble(e, value); else e.setAttribute(QStringLiteral("value"), value); } Transition *tr = new Transition(transitionInfo, a_track, m_doc->fps(), base, QString(prop.get("automatic")) == QLatin1String("1")); connect(tr, &AbstractClipItem::selectItem, m_trackview, &CustomTrackView::slotSelectItem); tr->setPos(transitionInfo.startPos.frames(m_doc->fps()), KdenliveSettings::trackheight() * (visibleTracksCount() - transitionInfo.track) + 1 + tr->itemOffset()); if (QString(prop.get("force_track")) == QLatin1String("1")) tr->setForcedTrack(true, a_track); if (isTrackLocked(b_track)) tr->setItemLocked(true); m_scene->addItem(tr); service = mlt_service_producer(service); } } } // static bool Timeline::isSlide(QString geometry) { if (geometry.count(';') != 1) return false; geometry.remove(QChar('%'), Qt::CaseInsensitive); geometry.replace(QChar('x'), QChar(':'), Qt::CaseInsensitive); geometry.replace(QChar(','), QChar(':'), Qt::CaseInsensitive); geometry.replace(QChar('/'), QChar(':'), Qt::CaseInsensitive); QString start = geometry.section('=', 0, 0).section(':', 0, -2) + ':'; start.append(geometry.section('=', 1, 1).section(':', 0, -2)); QStringList numbers = start.split(':', QString::SkipEmptyParts); for (int i = 0; i < numbers.size(); ++i) { int checkNumber = qAbs(numbers.at(i).toInt()); if (checkNumber != 0 && checkNumber != 100) { return false; } } return true; } void Timeline::adjustDouble(QDomElement &e, const QString &value) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); QString factor = e.attribute(QStringLiteral("factor"), QStringLiteral("1")); double offset = locale.toDouble(e.attribute(QStringLiteral("offset"), QStringLiteral("0"))); double fact = 1; if (factor.contains('%')) fact = EffectsController::getStringEval(m_doc->getProfileInfo(), factor); else fact = locale.toDouble(factor); QString type = e.attribute(QStringLiteral("type")); if (type == QLatin1String("double") || type == QLatin1String("constant")) { double val = locale.toDouble(value); e.setAttribute(QStringLiteral("value"), locale.toString(offset + val * fact)); } else if (type == QLatin1String("simplekeyframe")) { QStringList keys = value.split(QLatin1Char(';')); for (int j = 0; j < keys.count(); ++j) { QString pos = keys.at(j).section(QLatin1Char('='), 0, 0); double val = locale.toDouble(keys.at(j).section(QLatin1Char('='), 1, 1)) * fact + offset; keys[j] = pos + '=' + locale.toString(val); } e.setAttribute(QStringLiteral("value"), keys.join(QLatin1Char(';'))); } else { e.setAttribute(QStringLiteral("value"), value); } } void Timeline::parseDocument(const QDomDocument &doc) { //int cursorPos = 0; m_documentErrors.clear(); m_replacementProducerIds.clear(); // parse project tracks QDomElement mlt = doc.firstChildElement(QStringLiteral("mlt")); m_trackview->setDuration(getTracks()); getTransitions(); // Rebuild groups QDomDocument groupsDoc; groupsDoc.setContent(m_doc->renderer()->getBinProperty(QStringLiteral("kdenlive:clipgroups"))); QDomNodeList groups = groupsDoc.elementsByTagName(QStringLiteral("group")); m_trackview->loadGroups(groups); // Load custom effects QDomDocument effectsDoc; effectsDoc.setContent(m_doc->renderer()->getBinProperty(QStringLiteral("kdenlive:customeffects"))); QDomNodeList effects = effectsDoc.elementsByTagName(QStringLiteral("effect")); if (!effects.isEmpty()) { m_doc->saveCustomEffects(effects); } if (!m_documentErrors.isNull()) KMessageBox::sorry(this, m_documentErrors); if (mlt.hasAttribute(QStringLiteral("upgraded")) || mlt.hasAttribute(QStringLiteral("modified"))) { // Our document was upgraded, create a backup copy just in case QString baseFile = m_doc->url().path().section(QStringLiteral(".kdenlive"), 0, 0); int ct = 0; QString backupFile = baseFile + "_backup" + QString::number(ct) + ".kdenlive"; while (QFile::exists(backupFile)) { ct++; backupFile = baseFile + "_backup" + QString::number(ct) + ".kdenlive"; } QString message; if (mlt.hasAttribute(QStringLiteral("upgraded"))) message = i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile); else message = i18n("Your project file was modified by Kdenlive.\nTo make sure you don't lose data, a backup copy called %1 was created.", backupFile); KIO::FileCopyJob *copyjob = KIO::file_copy(m_doc->url(), QUrl::fromLocalFile(backupFile)); if (copyjob->exec()) KMessageBox::information(this, message); else KMessageBox::information(this, i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.", backupFile)); } } void Timeline::slotDeleteClip(const QString &clipId, QUndoCommand *deleteCommand) { m_trackview->deleteClip(clipId, deleteCommand); } void Timeline::setCursorPos(int pos) { m_trackview->setCursorPos(pos); } void Timeline::moveCursorPos(int pos) { m_trackview->setCursorPos(pos); } void Timeline::slotChangeZoom(int horizontal, int vertical) { m_ruler->setPixelPerMark(horizontal); m_scale = (double) m_trackview->getFrameWidth() / m_ruler->comboScale[horizontal]; if (vertical == -1) { // user called zoom m_doc->setZoom(horizontal, m_verticalZoom); m_trackview->setScale(m_scale, m_scene->scale().y()); } else { m_verticalZoom = vertical; if (m_verticalZoom == 0) m_trackview->setScale(m_scale, 0.5); else m_trackview->setScale(m_scale, m_verticalZoom); adjustTrackHeaders(); } } int Timeline::fitZoom() const { int zoom = (int)((duration() + 20 / m_scale) * m_trackview->getFrameWidth() / m_trackview->width()); int i; for (i = 0; i < 13; ++i) if (m_ruler->comboScale[i] > zoom) break; return i; } KdenliveDoc *Timeline::document() { return m_doc; } void Timeline::refresh() { m_trackview->viewport()->update(); } void Timeline::slotRepaintTracks() { for (int i = 1; i < m_tracks.count(); i++) { m_tracks.at(i)->trackHeader->setSelectedIndex(m_trackview->selectedTrack()); } } void Timeline::blockTrackSignals(bool block) { for (int i = 1; i < m_tracks.count(); i++) { m_tracks.at(i)->blockSignals(block); } } void Timeline::slotReloadTracks() { emit updateTracksInfo(); } TrackInfo Timeline::getTrackInfo(int ix) { if (ix < 0 || ix > m_tracks.count()) { qWarning()<<"/// ARGH, requested info for track: "<info(); } bool Timeline::isLastClip(ItemInfo info) { Track *tk = track(info.track); if (tk == NULL) { return true; } return tk->isLastClip(info.endPos.seconds()); } void Timeline::setTrackInfo(int ix, TrackInfo info) { if (ix < 0 || ix > m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return; } Track *tk = track(ix); tk->setInfo(info); } QList Timeline::getTracksInfo() { QList tracks; for (int i = 0; i < tracksCount(); i++) { tracks << track(i)->info(); } return tracks; } QStringList Timeline::getTrackNames() { QStringList trackNames; for (int i = 0; i < tracksCount(); i++) { trackNames << track(i)->info().trackName; } return trackNames; } void Timeline::lockTrack(int ix, bool lock) { Track *tk = track(ix); if (tk == NULL) { qWarning() << "Set Track effect outisde of range: "<lockTrack(lock); } bool Timeline::isTrackLocked(int ix) { Track *tk = track(ix); if (tk == NULL) { qWarning() << "Set Track effect outisde of range: "<getIntProperty(QStringLiteral("kdenlive:locked_track")); return locked == 1; } void Timeline::updateTrackState(int ix, int state) { int currentState = 0; QScopedPointer track(m_tractor->track(ix)); currentState = track->get_int("hide"); if (state == currentState) return; if (state == 0) { // Show all if (currentState & 1) { switchTrackVideo(ix, false); } if (currentState & 2) { switchTrackAudio(ix, false); } } else if (state == 1) { // Mute video if (currentState & 2) { switchTrackAudio(ix, false); } switchTrackVideo(ix, true); } else if (state == 2) { // Mute audio if (currentState & 1) { switchTrackVideo(ix, false); } switchTrackAudio(ix, true); } else { switchTrackVideo(ix, true); switchTrackAudio(ix, true); } } void Timeline::switchTrackVideo(int ix, bool hide) { Track* tk = track(ix); if (tk == NULL) { qWarning() << "Set Track effect outisde of range: "<state(); if (hide && (state & 1)) { // Video is already muted return; } int newstate = 0; if (hide) { if (state & 2) { newstate = 3; } else { newstate = 1; } } else { if (state & 2) { newstate = 2; } else { newstate = 0; } } tk->setState(newstate); invalidateTrack(ix); refreshTractor(); } void Timeline::slotSwitchTrackComposite(int trackIndex, bool enable) { if (trackIndex < 1 || trackIndex > m_tracks.count()) return; QScopedPointer transition(transitionHandler->getTransition(KdenliveSettings::gpu_accel() ? "movit.overlay" : "frei0r.cairoblend", trackIndex, -1, true)); if (transition) { transition->set("disable", enable); // When turning a track composite on/off, we need to re-plug transitions correctly updateComposites(); m_doc->renderer()->doRefresh(); m_doc->setModified(); //TODO: create undo/redo command for this } else { Track* tk = track(trackIndex); tk->trackHeader->setComposite(false); qWarning() << "Composite transition not found"; } } void Timeline::updateComposites() { int lowest = getLowestVideoTrack(); if (lowest >= 0) { transitionHandler->rebuildComposites(lowest); } } void Timeline::refreshTractor() { m_tractor->multitrack()->refresh(); m_tractor->refresh(); } void Timeline::switchTrackAudio(int ix, bool mute) { Track* tk = track(ix); if (tk == NULL) { qWarning() << "Set Track effect outisde of range: "<state(); if (mute && (state & 2)) { // audio is already muted return; } if (mute && state < 2 ) { // We mute a track with sound /*if (ix == getLowestNonMutedAudioTrack())*/ } else if (!mute && state > 1 ) { // We un-mute a previously muted track /*if (ix < getLowestNonMutedAudioTrack())*/ } int newstate; if (mute) { if (state & 1) newstate = 3; else newstate = 2; } else if (state & 1) { newstate = 1; } else { newstate = 0; } tk->setState(newstate); //if (audioMixingBroken) fixAudioMixing(); m_tractor->multitrack()->refresh(); m_tractor->refresh(); } int Timeline::getLowestVideoTrack() { for (int i = 1; i < m_tractor->count(); ++i) { QScopedPointer track(m_tractor->track(i)); Mlt::Playlist playlist(*track); if (playlist.get_int("kdenlive:audio_track") != 1) return i; } return -1; } void Timeline::fixAudioMixing() { QScopedPointer field(m_tractor->field()); field->lock(); mlt_service nextservice = mlt_service_get_producer(field->get_service()); mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); QString mlt_type = mlt_properties_get(properties, "mlt_type"); QString resource = mlt_properties_get(properties, "mlt_service"); // Delete all audio mixing transitions while (mlt_type == QLatin1String("transition")) { if (resource == QLatin1String("mix")) { Mlt::Transition transition((mlt_transition) nextservice); nextservice = mlt_service_producer(nextservice); field->disconnect_service(transition); } else nextservice = mlt_service_producer(nextservice); if (nextservice == NULL) break; properties = MLT_SERVICE_PROPERTIES(nextservice); mlt_type = mlt_properties_get(properties, "mlt_type"); resource = mlt_properties_get(properties, "mlt_service"); } // Re-add correct audio transitions for (int i = 1; i < m_tractor->count(); i++) { //bool muted = getTrackInfo(i).isMute; //if (muted) continue; /*int a_track = qMax(lowestTrack, i - 1); bool a_muted = getTrackInfo(a_track).isMute; while (a_muted && a_track > lowestTrack) { a_track = qMax(lowestTrack, a_track - 1); a_muted = getTrackInfo(a_track).isMute; } if (a_muted) continue;*/ Mlt::Transition *transition = new Mlt::Transition(*m_tractor->profile(), "mix"); transition->set("always_active", 1); transition->set("combine", 1); transition->set("a_track", 0); transition->set("b_track", i); transition->set("internal_added", 237); field->plant_transition(*transition, 0, i); } field->unlock(); } void Timeline::updatePalette() { headers_container->setStyleSheet(QLatin1String("")); setPalette(qApp->palette()); QPalette p = qApp->palette(); KColorScheme scheme(p.currentColorGroup(), KColorScheme::View, KSharedConfig::openConfig(KdenliveSettings::colortheme())); QColor col = scheme.background().color(); QColor col2 = scheme.foreground().color(); headers_container->setStyleSheet(QStringLiteral("QLineEdit { background-color: transparent;color: %1;} QLineEdit:hover{ background-color: %2;} QLineEdit:focus { background-color: %2;} ").arg(col2.name(), col.name())); m_trackview->updatePalette(); if (!m_tracks.isEmpty()) { int ix = m_trackview->selectedTrack(); for (int i = 0; i < m_tracks.count(); i++) { if (m_tracks.at(i)->trackHeader) { m_tracks.at(i)->trackHeader->refreshPalette(); if (i == ix) m_tracks.at(ix)->trackHeader->setSelectedIndex(ix); } } } m_ruler->activateZone(); } void Timeline::updateHeaders() { if (!m_tracks.isEmpty()) { for (int i = 0; i < m_tracks.count(); i++) { if (m_tracks.at(i)->trackHeader) { m_tracks.at(i)->trackHeader->updateLed(); } } } } void Timeline::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) continue; QIcon newIcon = KoIconUtils::themedIcon(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) continue; QIcon newIcon = KoIconUtils::themedIcon(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) continue; newIcon = KoIconUtils::themedIcon(ic.name()); m->setInactiveIcon(newIcon); } } void Timeline::adjustTrackHeaders() { if (m_tracks.isEmpty()) return; int height = KdenliveSettings::trackheight() * m_scene->scale().y() - 1; for (int i = 1; i < m_tracks.count(); i++) { m_tracks.at(i)->trackHeader->adjustSize(height); } } void Timeline::reloadTrack(int ix, int start, int end) { // Get playlist Mlt::Playlist pl = m_tracks.at(ix)->playlist(); if (end == -1) end = pl.get_length(); // Remove current clips int startIndex = pl.get_clip_index_at(start); int endIndex = pl.get_clip_index_at(end); double startY = m_trackview->getPositionFromTrack(ix) + 2; QRectF r(start, startY, end - start, 2); QList selection = m_scene->items(r); QList toDelete; for (int i = 0; i < selection.count(); i++) { if (selection.at(i)->type() == AVWidget) toDelete << selection.at(i); } qDeleteAll(toDelete); // Reload items loadTrack(ix, 0, pl, startIndex, endIndex, false); } int Timeline::loadTrack(int ix, int offset, Mlt::Playlist &playlist, int start, int end, bool updateReferences) { // parse track Mlt::ClipInfo *info = new Mlt::ClipInfo(); double fps = m_doc->fps(); if (end == -1) end = playlist.count(); bool locked = playlist.get_int("kdenlive:locked_track") == 1; for(int i = start; i < end; ++i) { emit loadingBin(offset + i + 1); if (playlist.is_blank(i)) { continue; } playlist.clip_info(i, info); Mlt::Producer *clip = info->cut; // Found a clip int in = info->frame_in; int out = info->frame_out; QString idString = info->producer->get("id"); if (in > out || m_invalidProducers.contains(idString)) { QString trackName = playlist.get("kdenlive:track_name"); m_documentErrors.append(i18n("Invalid clip removed from track %1 at %2\n", trackName.isEmpty() ? QString::number(ix) : trackName, info->start)); playlist.remove(i); --i; continue; } QString id = idString; Track::SlowmoInfo slowInfo; slowInfo.speed = 1.0; slowInfo.strobe = 1; slowInfo.state = PlaylistState::Original; bool hasSpeedEffect = false; if (idString.endsWith(QLatin1String("_video"))) { // Video only producer, store it in BinController m_doc->renderer()->loadExtraProducer(idString, new Mlt::Producer(clip->parent())); } if (idString.startsWith(QLatin1String("slowmotion"))) { hasSpeedEffect = true; QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); id = idString.section(':', 1, 1); slowInfo.speed = locale.toDouble(idString.section(':', 2, 2)); slowInfo.strobe = idString.section(':', 3, 3).toInt(); if (slowInfo.strobe == 0) slowInfo.strobe = 1; slowInfo.state = (PlaylistState::ClipState) idString.section(':', 4, 4).toInt(); // Slowmotion producer, store it for reuse Mlt::Producer *parentProd = new Mlt::Producer(clip->parent()); QString url = parentProd->get("warp_resource"); if (!m_doc->renderer()->storeSlowmotionProducer(slowInfo.toString(locale) + url, parentProd)) { delete parentProd; } } id = id.section('_', 0, 0); int length = out - in + 1; ProjectClip *binclip = m_doc->getBinClip(id); PlaylistState::ClipState originalState = PlaylistState::Original; if (binclip == NULL) { // Is this a disabled clip id = info->producer->get("kdenlive:binid"); binclip = m_doc->getBinClip(id); originalState = (PlaylistState::ClipState) info->producer->get_int("kdenlive:clipstate"); } if (binclip == NULL) { // Warning, unknown clip found, timeline corruption!! //TODO: fix this qDebug()<<"* * * * *UNKNOWN CLIP, WE ARE DEAD: "<addRef(); ItemInfo clipinfo; clipinfo.startPos = GenTime(info->start, fps); clipinfo.endPos = GenTime(info->start + length, fps); clipinfo.cropStart = GenTime(in, fps); clipinfo.cropDuration = GenTime(length, fps); clipinfo.track = ix; //qDebug()<<"// Loading clip: "<getFrameWidth(), true); connect(item, &AbstractClipItem::selectItem, m_trackview, &CustomTrackView::slotSelectItem); item->setPos(clipinfo.startPos.frames(fps), KdenliveSettings::trackheight() * (visibleTracksCount() - clipinfo.track) + 1 + item->itemOffset()); //qDebug()<<" * * Loaded clip on tk: "<updateState(idString, info->producer->get_int("audio_index"), info->producer->get_int("video_index"), originalState); m_scene->addItem(item); if (locked) item->setItemLocked(true); if (hasSpeedEffect) { QDomElement speedeffect = MainWindow::videoEffects.getEffectByTag(QString(), QStringLiteral("speed")).cloneNode().toElement(); EffectsList::setParameter(speedeffect, QStringLiteral("speed"), QString::number((int)(100 * slowInfo.speed + 0.5))); EffectsList::setParameter(speedeffect, QStringLiteral("strobe"), QString::number(slowInfo.strobe)); item->addEffect(m_doc->getProfileInfo(), speedeffect, false); } // parse clip effects getEffects(*clip, item); } delete info; return playlist.get_length(); } void Timeline::loadGuides(QMap guidesData) { QMapIterator i(guidesData); while (i.hasNext()) { i.next(); const GenTime pos = GenTime(i.key()); m_trackview->addGuide(pos, i.value(), true); } } void Timeline::getEffects(Mlt::Service &service, ClipItem *clip, int track) { int effectNb = clip == NULL ? 0 : clip->effectsCount(); QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); for (int ix = 0; ix < service.filter_count(); ++ix) { QScopedPointer effect(service.filter(ix)); QDomElement clipeffect = getEffectByTag(effect->get("tag"), effect->get("kdenlive_id")); if (clipeffect.isNull()) { m_documentErrors.append(i18n("Effect %1:%2 not found in MLT, it was removed from this project\n", effect->get("tag"), effect->get("kdenlive_id"))); service.detach(*effect); --ix; continue; } effectNb++; QDomElement currenteffect = clipeffect.cloneNode().toElement(); currenteffect.setAttribute(QStringLiteral("kdenlive_ix"), QString::number(effectNb)); currenteffect.setAttribute(QStringLiteral("kdenlive_info"), effect->get("kdenlive_info")); currenteffect.setAttribute(QStringLiteral("disable"), effect->get("disable")); QDomNodeList clipeffectparams = currenteffect.childNodes(); QDomNodeList params = currenteffect.elementsByTagName(QStringLiteral("parameter")); ProfileInfo info = m_doc->getProfileInfo(); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); if (e.attribute(QStringLiteral("type")) == QLatin1String("keyframe")) e.setAttribute(QStringLiteral("keyframes"), getKeyframes(service, ix, e)); else setParam(info, e, effect->get(e.attribute(QStringLiteral("name")).toUtf8().constData())); } if (effect->get_out()) { // no keyframes but in/out points //EffectsList::setParameter(currenteffect, QStringLiteral("in"), effect->get("in")); //EffectsList::setParameter(currenteffect, QStringLiteral("out"), effect->get("out")); currenteffect.setAttribute(QStringLiteral("in"), effect->get_in()); currenteffect.setAttribute(QStringLiteral("out"), effect->get_out()); } QString sync = effect->get("kdenlive:sync_in_out"); if (!sync.isEmpty()) { currenteffect.setAttribute(QStringLiteral("kdenlive:sync_in_out"), sync); } if (QString(effect->get("tag")) == QLatin1String("region")) getSubfilters(effect.data(), currenteffect); if (clip) { clip->addEffect(m_doc->getProfileInfo(), currenteffect, false); } else { addTrackEffect(track, currenteffect, false); } } } QString Timeline::getKeyframes(Mlt::Service service, int &ix, QDomElement e) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); QString starttag = e.attribute(QStringLiteral("starttag"), QStringLiteral("start")); QString endtag = e.attribute(QStringLiteral("endtag"), QStringLiteral("end")); double fact, offset = locale.toDouble(e.attribute(QStringLiteral("offset"), QStringLiteral("0"))); QString factor = e.attribute(QStringLiteral("factor"), QStringLiteral("1")); if (factor.contains('%')) fact = EffectsController::getStringEval(m_doc->getProfileInfo(), factor); else fact = locale.toDouble(factor); // retrieve keyframes QScopedPointer effect(service.filter(ix)); int effectNb = effect->get_int("kdenlive_ix"); QString keyframes = QString::number(effect->get_in()) + '=' + locale.toString(offset + fact * effect->get_double(starttag.toUtf8().constData())) + ';'; for (;ix < service.filter_count(); ++ix) { QScopedPointer eff2(service.filter(ix)); if (eff2->get_int("kdenlive_ix") != effectNb) break; if (eff2->get_in() < eff2->get_out()) { keyframes.append(QString::number(eff2->get_out()) + '=' + locale.toString(offset + fact * eff2->get_double(endtag.toUtf8().constData())) + ';'); } } --ix; return keyframes; } void Timeline::getSubfilters(Mlt::Filter *effect, QDomElement ¤teffect) { for (int i = 0; ; ++i) { QString name = "filter" + QString::number(i); if (!effect->get(name.toUtf8().constData())) break; //identify effect QString tag = effect->get(name.append(".tag").toUtf8().constData()); QString id = effect->get(name.append(".kdenlive_id").toUtf8().constData()); QDomElement subclipeffect = getEffectByTag(tag, id); if (subclipeffect.isNull()) { qWarning() << "Region sub-effect not found"; continue; } //load effect subclipeffect = subclipeffect.cloneNode().toElement(); subclipeffect.setAttribute(QStringLiteral("region_ix"), i); //get effect parameters (prefixed by subfilter name) QDomNodeList params = subclipeffect.elementsByTagName(QStringLiteral("parameter")); ProfileInfo info = m_doc->getProfileInfo(); for (int i = 0; i < params.count(); ++i) { QDomElement param = params.item(i).toElement(); setParam(info, param, effect->get((name + "." + param.attribute(QStringLiteral("name"))).toUtf8().constData())); } currenteffect.appendChild(currenteffect.ownerDocument().importNode(subclipeffect, true)); } } //static void Timeline::setParam(ProfileInfo info, QDomElement param, QString value) { QLocale locale; locale.setNumberOptions(QLocale::OmitGroupSeparator); //get Kdenlive scaling parameters double offset = locale.toDouble(param.attribute(QStringLiteral("offset"), QStringLiteral("0"))); double fact; QString factor = param.attribute(QStringLiteral("factor"), QStringLiteral("1")); if (factor.contains('%')) { fact = EffectsController::getStringEval(info, factor); } else { fact = locale.toDouble(factor); } //adjust parameter if necessary QString type = param.attribute(QStringLiteral("type")); if (type == QLatin1String("simplekeyframe")) { QStringList kfrs = value.split(';'); for (int l = 0; l < kfrs.count(); ++l) { QString fr = kfrs.at(l).section('=', 0, 0); double val = locale.toDouble(kfrs.at(l).section('=', 1, 1)); if (fact != 1) { // Add 0.5 since we are converting to integer below so that 0.8 is converted to 1 and not 0 val = val * fact + 0.5; } kfrs[l] = fr + '=' + QString::number((int) (val + offset)); } param.setAttribute(QStringLiteral("keyframes"), kfrs.join(QStringLiteral(";"))); } else if (type == QLatin1String("double") || type == QLatin1String("constant")) { param.setAttribute(QStringLiteral("value"), locale.toDouble(value) * fact + offset); } else { param.setAttribute(QStringLiteral("value"), value); } } QDomElement Timeline::getEffectByTag(const QString &effecttag, const QString &effectid) { QDomElement clipeffect = MainWindow::customEffects.getEffectByTag(QString(), effectid); if (clipeffect.isNull()) { clipeffect = MainWindow::videoEffects.getEffectByTag(effecttag, effectid); } if (clipeffect.isNull()) { clipeffect = MainWindow::audioEffects.getEffectByTag(effecttag, effectid); } return clipeffect; } QGraphicsScene *Timeline::projectScene() { return m_scene; } CustomTrackView *Timeline::projectView() { return m_trackview; } void Timeline::setEditMode(const QString & editMode) { m_editMode = editMode; } const QString & Timeline::editMode() const { return m_editMode; } void Timeline::slotVerticalZoomDown() { if (m_verticalZoom == 0) return; m_verticalZoom--; m_doc->setZoom(m_doc->zoom().x(), m_verticalZoom); if (m_verticalZoom == 0) m_trackview->setScale(m_scene->scale().x(), 0.5); else m_trackview->setScale(m_scene->scale().x(), 1); adjustTrackHeaders(); m_trackview->verticalScrollBar()->setValue(headers_area->verticalScrollBar()->value()); } void Timeline::slotVerticalZoomUp() { if (m_verticalZoom == 2) return; m_verticalZoom++; m_doc->setZoom(m_doc->zoom().x(), m_verticalZoom); if (m_verticalZoom == 2) m_trackview->setScale(m_scene->scale().x(), 2); else m_trackview->setScale(m_scene->scale().x(), 1); adjustTrackHeaders(); m_trackview->verticalScrollBar()->setValue(headers_area->verticalScrollBar()->value()); } void Timeline::slotRenameTrack(int ix, const QString &name) { QString currentName = track(ix)->getProperty(QStringLiteral("kdenlive:track_name")); if (currentName == name) return; ConfigTracksCommand *configTracks = new ConfigTracksCommand(this, ix, currentName, name); m_doc->commandStack()->push(configTracks); } void Timeline::renameTrack(int ix, const QString &name) { if (ix < 1) return; Track *tk = track(ix); if (!tk) return; tk->setProperty(QStringLiteral("kdenlive:track_name"), name); tk->trackHeader->renameTrack(name); slotReloadTracks(); } void Timeline::slotUpdateVerticalScroll(int /*min*/, int max) { int height = 0; if (max > 0) height = m_trackview->horizontalScrollBar()->height() - 1; headers_container->layout()->setContentsMargins(0, m_trackview->frameWidth(), 0, height); } void Timeline::updateRuler() { m_ruler->update(); } void Timeline::slotShowTrackEffects(int ix) { m_trackview->clearSelection(); emit showTrackEffects(ix, getTrackInfo(ix)); } void Timeline::slotUpdateTrackEffectState(int ix) { if (ix < 1) return; Track *tk = track(ix); if (!tk) return; tk->trackHeader->updateEffectLabel(tk->effectsList.effectNames()); } void Timeline::slotSaveTimelinePreview(const QString &path) { QImage img(width(), height(), QImage::Format_ARGB32_Premultiplied); img.fill(palette().base().color().rgb()); QPainter painter(&img); render(&painter); painter.end(); img = img.scaledToWidth(600, Qt::SmoothTransformation); img.save(path); } void Timeline::updateProfile(bool fpsChanged) { m_ruler->updateFrameSize(); m_ruler->updateProjectFps(m_doc->timecode()); m_ruler->setPixelPerMark(m_doc->zoom().x(), true); slotChangeZoom(m_doc->zoom().x(), m_doc->zoom().y()); slotSetZone(m_doc->zone(), false); m_trackview->updateSceneFrameWidth(fpsChanged); } void Timeline::checkTrackHeight(bool force) { if (m_trackview->checkTrackHeight(force)) { m_doc->clipManager()->clearCache(); m_ruler->updateFrameSize(); m_trackview->updateSceneFrameWidth(); slotChangeZoom(m_doc->zoom().x(), m_doc->zoom().y()); slotSetZone(m_doc->zone(), false); } } bool Timeline::moveClip(int startTrack, qreal startPos, int endTrack, qreal endPos, PlaylistState::ClipState state, TimelineMode::EditMode mode, bool duplicate) { if (startTrack == endTrack) { return track(startTrack)->move(startPos, endPos, mode); } Track *sourceTrack = track(startTrack); int pos = sourceTrack->frame(startPos); int clipIndex = sourceTrack->playlist().get_clip_index_at(pos); sourceTrack->playlist().lock(); Mlt::Producer *clipProducer = sourceTrack->playlist().replace_with_blank(clipIndex); sourceTrack->playlist().consolidate_blanks(); if (!clipProducer || clipProducer->is_blank()) { qDebug() << "// Cannot get clip at index: "<playlist().unlock(); return false; } sourceTrack->playlist().unlock(); Track *destTrack = track(endTrack); bool success = destTrack->add(endPos, clipProducer, GenTime(clipProducer->get_in(), destTrack->fps()).seconds(), GenTime(clipProducer->get_out() + 1, destTrack->fps()).seconds(), state, duplicate, mode); delete clipProducer; return success; } void Timeline::addTrackEffect(int trackIndex, QDomElement effect, bool addToPlaylist) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return; } Track *sourceTrack = track(trackIndex); effect.setAttribute(QStringLiteral("kdenlive_ix"), sourceTrack->effectsList.count() + 1); // Init parameter value & keyframes if required QDomNodeList params = effect.elementsByTagName(QStringLiteral("parameter")); for (int i = 0; i < params.count(); ++i) { QDomElement e = params.item(i).toElement(); // Check if this effect has a variable parameter if (e.attribute(QStringLiteral("default")).contains('%')) { double evaluatedValue = EffectsController::getStringEval(m_doc->getProfileInfo(), e.attribute(QStringLiteral("default"))); e.setAttribute(QStringLiteral("default"), evaluatedValue); if (e.hasAttribute(QStringLiteral("value")) && e.attribute(QStringLiteral("value")).startsWith('%')) { e.setAttribute(QStringLiteral("value"), evaluatedValue); } } if (!e.isNull() && (e.attribute(QStringLiteral("type")) == QLatin1String("keyframe") || e.attribute(QStringLiteral("type")) == QLatin1String("simplekeyframe"))) { QString def = e.attribute(QStringLiteral("default")); // Effect has a keyframe type parameter, we need to set the values if (e.attribute(QStringLiteral("keyframes")).isEmpty()) { e.setAttribute(QStringLiteral("keyframes"), "0:" + def + ';'); //qDebug() << "///// EFFECT KEYFRAMES INITED: " << e.attribute("keyframes"); //break; } } if (effect.attribute(QStringLiteral("id")) == QLatin1String("crop")) { // default use_profile to 1 for clips with proxies to avoid problems when rendering if (e.attribute(QStringLiteral("name")) == QLatin1String("use_profile") && m_doc->useProxy()) e.setAttribute(QStringLiteral("value"), QStringLiteral("1")); } } sourceTrack->effectsList.append(effect); if (addToPlaylist) { sourceTrack->addTrackEffect(EffectsController::getEffectArgs(m_doc->getProfileInfo(), effect)); if (effect.attribute(QStringLiteral("type")) != QLatin1String("audio")) { invalidateTrack(trackIndex); } } } bool Timeline::removeTrackEffect(int trackIndex, int effectIndex, const QDomElement &effect) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return false; } int toRemove = effect.attribute(QStringLiteral("kdenlive_ix")).toInt(); Track *sourceTrack = track(trackIndex); bool success = sourceTrack->removeTrackEffect(effectIndex, true); if (success) { int max = sourceTrack->effectsList.count(); for (int i = 0; i < max; ++i) { int index = sourceTrack->effectsList.at(i).attribute(QStringLiteral("kdenlive_ix")).toInt(); if (toRemove == index) { sourceTrack->effectsList.removeAt(toRemove); break; } } if (effect.attribute(QStringLiteral("type")) != QLatin1String("audio")) { invalidateTrack(trackIndex); } } return success; } void Timeline::setTrackEffect(int trackIndex, int effectIndex, QDomElement effect) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return; } Track *sourceTrack = track(trackIndex); int max = sourceTrack->effectsList.count(); if (effectIndex <= 0 || effectIndex > (max) || effect.isNull()) { //qDebug() << "Invalid effect index: " << effectIndex; return; } sourceTrack->effectsList.removeAt(effect.attribute(QStringLiteral("kdenlive_ix")).toInt()); effect.setAttribute(QStringLiteral("kdenlive_ix"), effectIndex); sourceTrack->effectsList.insert(effect); if (effect.attribute(QStringLiteral("type")) != QLatin1String("audio")) { invalidateTrack(trackIndex); } } bool Timeline::enableTrackEffects(int trackIndex, const QList &effectIndexes, bool disable) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return false; } Track *sourceTrack = track(trackIndex); EffectsList list = sourceTrack->effectsList; QDomElement effect; bool hasVideoEffect = false; for (int i = 0; i < effectIndexes.count(); ++i) { effect = list.itemFromIndex(effectIndexes.at(i)); if (!effect.isNull()) { effect.setAttribute(QStringLiteral("disable"), (int) disable); if (effect.attribute(QStringLiteral("type")) != QLatin1String("audio")) hasVideoEffect = true; } } if (hasVideoEffect) { invalidateTrack(trackIndex); } return hasVideoEffect; } const EffectsList Timeline::getTrackEffects(int trackIndex) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return EffectsList(); } Track *sourceTrack = track(trackIndex); return sourceTrack->effectsList; } QDomElement Timeline::getTrackEffect(int trackIndex, int effectIndex) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return QDomElement(); } Track *sourceTrack = track(trackIndex); EffectsList list = sourceTrack->effectsList; if (effectIndex > list.count() || effectIndex < 1 || list.itemFromIndex(effectIndex).isNull()) return QDomElement(); return list.itemFromIndex(effectIndex).cloneNode().toElement(); } int Timeline::hasTrackEffect(int trackIndex, const QString &tag, const QString &id) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return -1; } Track *sourceTrack = track(trackIndex); EffectsList list = sourceTrack->effectsList; return list.hasEffect(tag, id); } MltVideoProfile Timeline::mltProfile() const { return ProfilesDialog::getVideoProfile(*m_tractor->profile()); } double Timeline::fps() const { return m_doc->fps(); } QPoint Timeline::getTracksCount() { int audioTracks = 0; int videoTracks = 0; int max = m_tracks.count(); for (int i = 0; i < max; i++) { Track *tk = track(i); if (tk->type == AudioTrack) audioTracks++; else videoTracks++; } return QPoint(videoTracks, audioTracks); } int Timeline::getTrackSpaceLength(int trackIndex, int pos, bool fromBlankStart) { if (trackIndex < 0 || trackIndex >= m_tracks.count()) { qWarning() << "Set Track effect outisde of range"; return 0; } return track(trackIndex)->getBlankLength(pos, fromBlankStart); } void Timeline::updateClipProperties(const QString &id, QMap properties) { for (int i = 0; i< m_tracks.count(); i++) { track(i)->updateClipProperties(id, properties); } } int Timeline::changeClipSpeed(ItemInfo info, ItemInfo speedIndependantInfo, PlaylistState::ClipState state, double speed, int strobe, Mlt::Producer *originalProd, bool removeEffect) { QLocale locale; QString url = QString::fromUtf8(originalProd->get("resource")); Track::SlowmoInfo slowInfo; slowInfo.speed = speed; slowInfo.strobe = strobe; slowInfo.state = state; url.prepend(slowInfo.toString(locale)); //if (strobe > 1) url.append("&strobe=" + QString::number(strobe)); Mlt::Producer *prod; if (removeEffect) { // We want to remove framebuffer producer, so pass original prod = originalProd; } else { // Pass slowmotion producer prod = m_doc->renderer()->getSlowmotionProducer(url); } QString id = originalProd->get("id"); id = id.section(QStringLiteral("_"), 0, 0); Mlt::Properties passProperties; Mlt::Properties original(originalProd->get_properties()); passProperties.pass_list(original, ClipController::getPassPropertiesList(false)); return track(info.track)->changeClipSpeed(info, speedIndependantInfo, state, speed, strobe, prod, id, passProperties); } void Timeline::duplicateClipOnPlaylist(int tk, qreal startPos, int offset, Mlt::Producer *prod) { Track *sourceTrack = track(tk); int pos = sourceTrack->frame(startPos); int clipIndex = sourceTrack->playlist().get_clip_index_at(pos); if (sourceTrack->playlist().is_blank(clipIndex)) { qDebug()<<"// ERROR FINDING CLIP on TK: "<playlist().get_clip(clipIndex); Clip clp(clipProducer->parent()); Mlt::Producer *cln = clp.clone(); // Clip effects must be moved from clip to the playlist entry, so first delete them from parent clip Clip(*cln).deleteEffects(); cln->set_in_and_out(clipProducer->get_in(), clipProducer->get_out()); Mlt::Playlist trackPlaylist((mlt_playlist) prod->get_service()); trackPlaylist.lock(); int newIdx = trackPlaylist.insert_at(pos - offset, cln, 1); // Re-add source effects in playlist Mlt::Producer *inPlaylist = trackPlaylist.get_clip(newIdx); if (inPlaylist) { Clip(*inPlaylist).addEffects(*clipProducer); delete inPlaylist; } trackPlaylist.unlock(); delete clipProducer; delete cln; delete prod; } int Timeline::getSpaceLength(const GenTime &pos, int tk, bool fromBlankStart) { Track *sourceTrack = track(tk); if (!sourceTrack) return 0; int insertPos = pos.frames(m_doc->fps()); return sourceTrack->spaceLength(insertPos, fromBlankStart); } void Timeline::disableTimelineEffects(bool disable) { for (int i = 0; i< tracksCount(); i++) { track(i)->disableEffects(disable); } } void Timeline::importPlaylist(ItemInfo info, QMap processedUrl, QMap idMaps, QDomDocument doc, QUndoCommand *command) { projectView()->importPlaylist(info, processedUrl, idMaps, doc, command); } void Timeline::refreshTrackActions() { int tracks = tracksCount(); if (tracks > 3) { return; } foreach(QAction *action, m_trackActions) { if (action->data().toString() == "delete_track") { action->setEnabled(tracks > 2); } } } void Timeline::slotMultitrackView(bool enable) { multitrackView = enable; transitionHandler->enableMultiTrack(enable); } void Timeline::connectOverlayTrack(bool enable) { - if (!m_hasOverlayTrack) return; + if (!m_hasOverlayTrack && !m_usePreview) return; m_tractor->lock(); if (enable) { // Re-add overlaytrack - m_tractor->insert_track(*m_overlayTrack, tracksCount() + 1); - delete m_overlayTrack; - m_overlayTrack = NULL; + if (m_usePreview) + m_timelinePreview->reconnectTrack(); + if (m_hasOverlayTrack) { + m_tractor->insert_track(*m_overlayTrack, tracksCount() + 1); + delete m_overlayTrack; + m_overlayTrack = NULL; + } } else { - m_overlayTrack = m_tractor->track(tracksCount()); - m_tractor->remove_track(tracksCount()); + if (m_usePreview) + m_timelinePreview->disconnectTrack(); + if (m_hasOverlayTrack) { + m_overlayTrack = m_tractor->track(tracksCount()); + m_tractor->remove_track(tracksCount()); + } } m_tractor->unlock(); } void Timeline::removeSplitOverlay() { if (!m_hasOverlayTrack) return; m_tractor->lock(); m_tractor->remove_track(tracksCount()); m_hasOverlayTrack = false; m_tractor->unlock(); } bool Timeline::createOverlay(Mlt::Filter *filter, int tk, int startPos) { Track *sourceTrack = track(tk); if (!sourceTrack) return false; m_tractor->lock(); int clipIndex = sourceTrack->playlist().get_clip_index_at(startPos); Mlt::Producer *clipProducer = sourceTrack->playlist().get_clip(clipIndex); Clip clp(clipProducer->parent()); Mlt::Producer *cln = clp.clone(); Clip(*cln).deleteEffects(); cln->set_in_and_out(clipProducer->get_in(), clipProducer->get_out()); Mlt::Playlist overlay(*m_tractor->profile()); Mlt::Tractor trac(*m_tractor->profile()); trac.set_track(*clipProducer, 0); trac.set_track(*cln, 1); cln->attach(*filter); QString splitTransition = KdenliveSettings::gpu_accel() ? "movit.overlay" : "frei0r.cairoblend"; Mlt::Transition t(*m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete cln; delete clipProducer; overlay.insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay.insert_at(startPos, &split, 1); int trackIndex = tracksCount(); m_tractor->insert_track(overlay, trackIndex); Mlt::Producer *overlayTrack = m_tractor->track(trackIndex); overlayTrack->set("hide", 2); delete overlayTrack; m_hasOverlayTrack = true; m_tractor->unlock(); return true; } void Timeline::switchTrackTarget() { if (!KdenliveSettings::splitaudio()) { // This feature is only available on split mode return; } Track *current = m_tracks.at(m_trackview->selectedTrack()); TrackType trackType = current->info().type; if (trackType == VideoTrack) { if (m_trackview->selectedTrack() == videoTarget) { // Switch off current->trackHeader->switchTarget(false); videoTarget = -1; } else { if (videoTarget > -1) m_tracks.at(videoTarget)->trackHeader->switchTarget(false); current->trackHeader->switchTarget(true); videoTarget = m_trackview->selectedTrack(); } } else if (trackType == AudioTrack) { if (m_trackview->selectedTrack() == audioTarget) { // Switch off current->trackHeader->switchTarget(false); audioTarget = -1; } else { if (audioTarget > -1) m_tracks.at(audioTarget)->trackHeader->switchTarget(false); current->trackHeader->switchTarget(true); audioTarget = m_trackview->selectedTrack(); } } } void Timeline::slotEnableZone(bool enable) { KdenliveSettings::setUseTimelineZoneToEdit(enable); m_ruler->activateZone(); } -void Timeline::gotPreviewRender(int frame, const QString &file, int progress) -{ - if (!m_hasOverlayTrack) { - // Create overlay track - Mlt::Playlist overlay(*m_tractor->profile()); - int trackIndex = tracksCount(); - m_tractor->lock(); - m_tractor->insert_track(overlay, trackIndex); - m_tractor->unlock(); - m_hasOverlayTrack = true; - } - if (file.isEmpty()) { - m_doc->previewProgress(progress); - return; - } - Mlt::Producer *overlayTrack = m_tractor->track(tracksCount()); - m_tractor->lock(); - Mlt::Playlist trackPlaylist((mlt_playlist) overlayTrack->get_service()); - delete overlayTrack; - if (trackPlaylist.is_blank_at(frame)) { - Mlt::Producer prod(*m_tractor->profile(), 0, file.toUtf8().constData()); - if (prod.is_valid()) { - m_ruler->updatePreview(frame, true, true); - prod.set("mlt_service", "avformat-novalidate"); - trackPlaylist.insert_at(frame, &prod, 1); - } - } - trackPlaylist.consolidate_blanks(); - m_tractor->unlock(); - m_doc->previewProgress(progress); - m_doc->setModified(true); -} - - - void Timeline::stopPreviewRender() { if (m_timelinePreview) m_timelinePreview->abortRendering(); } void Timeline::invalidateRange(ItemInfo info) { - if (!m_hasOverlayTrack) + if (!m_usePreview) return; if (info.isValid()) - invalidatePreview(info.startPos.frames(m_doc->fps()), info.endPos.frames(m_doc->fps())); + m_timelinePreview->invalidatePreview(info.startPos.frames(m_doc->fps()), info.endPos.frames(m_doc->fps())); else { - invalidatePreview(0, m_trackview->duration()); - } -} - -void Timeline::invalidatePreview(int startFrame, int endFrame) -{ - if (m_previewGatherTimer.isActive()) - m_previewGatherTimer.stop(); - int chunkSize = KdenliveSettings::timelinechunks(); - int start = startFrame / chunkSize; - int end = lrintf(endFrame / chunkSize); - m_timelinePreview->abortPreview(); - Mlt::Producer *overlayTrack = m_tractor->track(tracksCount()); - m_tractor->lock(); - Mlt::Playlist trackPlaylist((mlt_playlist) overlayTrack->get_service()); - delete overlayTrack; - start *= chunkSize; - end *= chunkSize; - for (int i = start; i <=end; i+= chunkSize) { - if (m_ruler->updatePreview(i, false)) { - int ix = trackPlaylist.get_clip_index_at(i); - if (trackPlaylist.is_blank(ix)) - continue; - Mlt::Producer *prod = trackPlaylist.replace_with_blank(ix); - delete prod; - } + m_timelinePreview->invalidatePreview(0, m_trackview->duration()); } - trackPlaylist.consolidate_blanks(); - m_tractor->unlock(); - m_previewGatherTimer.start(); } void Timeline::loadPreviewRender() { + if (!m_timelinePreview) + return; QString documentId = m_doc->getDocumentProperty(QStringLiteral("documentid")); QString chunks = m_doc->getDocumentProperty(QStringLiteral("previewchunks")); QString dirty = m_doc->getDocumentProperty(QStringLiteral("dirtypreviewchunks")); QString ext = m_doc->getDocumentProperty(QStringLiteral("previewextension")); QDateTime documentDate = QFileInfo(m_doc->url().path()).lastModified(); if (!chunks.isEmpty() || !dirty.isEmpty()) { - if (!m_hasOverlayTrack) { - // Create overlay track - Mlt::Playlist overlay(*m_tractor->profile()); - int trackIndex = tracksCount(); - m_tractor->lock(); - m_tractor->insert_track(overlay, trackIndex); - m_tractor->unlock(); - m_hasOverlayTrack = true; - } + m_timelinePreview->buildPreviewTrack(); QDir dir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)); dir.cd(documentId); QStringList previewChunks = chunks.split(",", QString::SkipEmptyParts); QStringList dirtyChunks = dirty.split(",", QString::SkipEmptyParts); foreach(const QString frame, previewChunks) { int pos = frame.toInt(); const QString fileName = dir.absoluteFilePath(QString("%1.%2").arg(pos).arg(ext)); QFile file(fileName); if (file.exists()) { if (QFileInfo(file).lastModified() > documentDate) { // Timeline preview file was created after document, invalidate file.remove(); dirtyChunks << frame; } else { - gotPreviewRender(pos, fileName, 1000); + m_timelinePreview->gotPreviewRender(pos, fileName, 1000); } } else dirtyChunks << frame; } if (!dirtyChunks.isEmpty()) { foreach(const QString i, dirtyChunks) { m_ruler->updatePreview(i.toInt(), false); } m_ruler->update(); } + m_usePreview = true; } } void Timeline::updatePreviewSettings(const QString &profile) { if (profile.isEmpty()) return; QString params = profile.section(";", 0, 0); QString ext = profile.section(";", 1, 1); if (params != m_doc->getDocumentProperty(QStringLiteral("previewparameters")) || ext != m_doc->getDocumentProperty(QStringLiteral("previewextension"))) { // Timeline preview params changed, delete all existing previews. invalidateRange(ItemInfo()); m_doc->setDocumentProperty(QStringLiteral("previewparameters"), params); m_doc->setDocumentProperty(QStringLiteral("previewextension"), ext); initializePreview(); } } -void Timeline::slotReloadChunks(QDir cacheDir, QList chunks, const QString ext) -{ - Mlt::Producer *overlayTrack = m_tractor->track(tracksCount()); - m_tractor->lock(); - Mlt::Playlist trackPlaylist((mlt_playlist) overlayTrack->get_service()); - delete overlayTrack; - foreach(int ix, chunks) { - if (trackPlaylist.is_blank_at(ix)) { - const QString fileName = cacheDir.absoluteFilePath(QString("%1.%2").arg(ix).arg(ext)); - Mlt::Producer prod(*m_tractor->profile(), 0, fileName.toUtf8().constData()); - if (prod.is_valid()) { - m_ruler->updatePreview(ix, true); - prod.set("mlt_service", "avformat-novalidate"); - trackPlaylist.insert_at(ix, &prod, 1); - } - } - } - m_ruler->updatePreviewDisplay(chunks.first(), chunks.last()); - trackPlaylist.consolidate_blanks(); - m_tractor->unlock(); -} - void Timeline::invalidateTrack(int ix) { - if (!m_hasOverlayTrack) + if (!m_usePreview) return; Track* tk = track(ix); QList visibleRange = tk->visibleClips(); foreach(const QPoint p, visibleRange) { - invalidatePreview(p.x(), p.y()); + m_timelinePreview->invalidatePreview(p.x(), p.y()); } } void Timeline::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { delete m_timelinePreview; m_timelinePreview = NULL; } } else { - m_timelinePreview = new PreviewManager(m_doc, m_ruler); + m_timelinePreview = new PreviewManager(m_doc, m_ruler, m_tractor); if (!m_timelinePreview->initialize()) { //TODO warn user delete m_timelinePreview; m_timelinePreview = NULL; qDebug()<<" * * * *TL PREVIEW NOT INITIALIZED!!!"; - } else { - connect(&m_previewGatherTimer, &QTimer::timeout, m_timelinePreview, &PreviewManager::slotProcessDirtyChunks); - connect(m_timelinePreview, &PreviewManager::previewRender, this, &Timeline::gotPreviewRender); - connect(m_timelinePreview, &PreviewManager::reloadChunks, this, &Timeline::slotReloadChunks, Qt::DirectConnection); } } QAction *previewRender = m_doc->getAction(QStringLiteral("prerender_timeline_zone")); - if (previewRender) + if (previewRender) { previewRender->setEnabled(m_timelinePreview != NULL); + } + /*if (m_timelinePreview) { + if (!m_hasOverlayTrack) { + // Create overlay track + Mlt::Playlist overlay(*m_tractor->profile()); + int trackIndex = tracksCount(); + m_tractor->lock(); + m_tractor->insert_track(overlay, trackIndex); + m_tractor->unlock(); + m_hasOverlayTrack = true; + } + }*/ } void Timeline::startPreviewRender() { - if (m_timelinePreview) + if (m_timelinePreview) { + if (!m_usePreview) { + m_timelinePreview->buildPreviewTrack(); + m_usePreview = true; + } m_timelinePreview->startPreviewRender(); + } } void Timeline::addPreviewRange(bool add) { if (m_timelinePreview) m_timelinePreview->addPreviewRange(add); } diff --git a/src/timeline/timeline.h b/src/timeline/timeline.h index b10b6291e..386f2d962 100644 --- a/src/timeline/timeline.h +++ b/src/timeline/timeline.h @@ -1,280 +1,276 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.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 2 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ /** * @class Timeline * @brief Manages the timline * @author Jean-Baptiste Mardelle */ #ifndef TRACKVIEW_H #define TRACKVIEW_H #include "timeline/customtrackscene.h" #include "effectslist/effectslist.h" #include "ui_timeline_ui.h" #include "definitions.h" #include #include #include -#include #include #include class Track; class ClipItem; class CustomTrackView; class KdenliveDoc; class TransitionHandler; class CustomRuler; class QUndoCommand; class PreviewManager; class Timeline : public QWidget, public Ui::TimeLine_UI { Q_OBJECT public: explicit Timeline(KdenliveDoc *doc, const QList & actions, const QList &rulerActions, bool *ok, QWidget *parent = 0); virtual ~ Timeline(); /** @brief is multitrack view (split screen for tracks) enabled */ bool multitrackView; int videoTarget; int audioTarget; Track* track(int i); /** @brief Number of tracks in the MLT playlist. */ int tracksCount() const; /** @brief Number of visible tracks (= tracksCount() - 1 ) because black trck is not visible to user. */ int visibleTracksCount() const; void setEditMode(const QString & editMode); const QString & editMode() const; QGraphicsScene *projectScene(); CustomTrackView *projectView(); int duration() const; KdenliveDoc *document(); void refresh() ; int outPoint() const; int inPoint() const; int fitZoom() const; /** @brief This object handles all transition operation. */ TransitionHandler *transitionHandler; void lockTrack(int ix, bool lock); bool isTrackLocked(int ix); /** @brief Dis / enable video for a track. */ void switchTrackVideo(int ix, bool hide); /** @brief Dis / enable audio for a track. */ void switchTrackAudio(int ix, bool mute); /** @brief Adjust audio transitions depending on tracks muted state. */ void fixAudioMixing(); /** @brief Updates (redraws) the ruler. * * Used to change from displaying frames to timecode or vice versa. */ void updateRuler(); /** @brief Parse tracks to see if project has audio in it. * * Parses all tracks to check if there is audio data. */ bool checkProjectAudio(); /** @brief Load guides from data */ void loadGuides(QMap guidesData); void checkTrackHeight(bool force = false); void updatePalette(); void refreshIcons(); /** @brief Returns a kdenlive effect xml description from an effect tag / id */ static QDomElement getEffectByTag(const QString &effecttag, const QString &effectid); /** @brief Move a clip between tracks */ bool moveClip(int startTrack, qreal startPos, int endTrack, qreal endPos, PlaylistState::ClipState state, TimelineMode::EditMode mode, bool duplicate); void renameTrack(int ix, const QString &name); void updateTrackState(int ix, int state); /** @brief Returns info about a track. * @param ix The track number in MLT's coordinates (0 = black track, 1 = bottom audio, etc) * deprecated use string version with track name instead */ TrackInfo getTrackInfo(int ix); void setTrackInfo(int trackIndex, TrackInfo info); QList getTracksInfo(); QStringList getTrackNames(); void addTrackEffect(int trackIndex, QDomElement effect, bool addToPlaylist = true); bool removeTrackEffect(int trackIndex, int effectIndex, const QDomElement &effect); void setTrackEffect(int trackIndex, int effectIndex, QDomElement effect); bool enableTrackEffects(int trackIndex, const QList &effectIndexes, bool disable); const EffectsList getTrackEffects(int trackIndex); QDomElement getTrackEffect(int trackIndex, int effectIndex); int hasTrackEffect(int trackIndex, const QString &tag, const QString &id); MltVideoProfile mltProfile() const; double fps() const; QPoint getTracksCount(); /** @brief Check if we have a blank space on selected track. * Returns -1 if track is shorter, 0 if not blank and > 0 for blank length */ int getTrackSpaceLength(int trackIndex, int pos, bool fromBlankStart); void updateClipProperties(const QString &id, QMap properties); int changeClipSpeed(ItemInfo info, ItemInfo speedIndependantInfo, PlaylistState::ClipState state, double speed, int strobe, Mlt::Producer *originalProd, bool removeEffect = false); /** @brief Set an effect's XML accordingly to MLT::filter values. */ static void setParam(ProfileInfo info, QDomElement param, QString value); int getTracks(); void getTransitions(); void refreshTractor(); void duplicateClipOnPlaylist(int tk, qreal startPos, int offset, Mlt::Producer *prod); int getSpaceLength(const GenTime &pos, int tk, bool fromBlankStart); void blockTrackSignals(bool block); /** @brief Load document */ void loadTimeline(); /** @brief Dis/enable all effects in timeline*/ void disableTimelineEffects(bool disable); QString getKeyframes(Mlt::Service service, int &ix, QDomElement e); void getSubfilters(Mlt::Filter *effect, QDomElement ¤teffect); static bool isSlide(QString geometry); /** @brief Import amultitrack MLT playlist in timeline */ void importPlaylist(ItemInfo info, QMap processedUrl, QMap idMaps, QDomDocument doc, QUndoCommand *command); /** @brief Creates an overlay track with a filtered clip */ bool createOverlay(Mlt::Filter *filter, int tk, int startPos); void removeSplitOverlay(); /** @brief Temporarily add/remove track before saving */ void connectOverlayTrack(bool enable); /** @brief Update composite transitions's tracks */ void updateComposites(); /** @brief Switch current track target state */ void switchTrackTarget(); /** @brief Refresh Header Leds */ void updateHeaders(); /** @brief Returns true if position is on the last clip */ bool isLastClip(ItemInfo info); /** @brief find lowest video track in timeline. */ int getLowestVideoTrack(); /** @brief Returns the document properties with some added values from timeline. */ QMap documentProperties(); void reloadTrack(int ix, int start = 0, int end = -1); /** @brief Invalidate a preview rendering range. */ void invalidateRange(ItemInfo info = ItemInfo()); /** @brief Add or remove current timeline zone to preview render zone. */ void addPreviewRange(bool add); /** @brief Check if timeline preview profile changed and remove preview files if necessary. */ void updatePreviewSettings(const QString &profile); /** @brief invalidate timeline preview for visible clips in a track */ void invalidateTrack(int ix); /** @brief Start rendering preview rendering range. */ void startPreviewRender(); protected: void keyPressEvent(QKeyEvent * event); public slots: void slotDeleteClip(const QString &clipId, QUndoCommand *deleteCommand); void slotChangeZoom(int horizontal, int vertical = -1); void setDuration(int dur); void slotSetZone(const QPoint &p, bool updateDocumentProperties = true); /** @brief Save a snapshot image of current timeline view */ void slotSaveTimelinePreview(const QString &path); void checkDuration(int duration); void slotShowTrackEffects(int); void updateProfile(bool fpsChanged); /** @brief Enable/disable multitrack view (split monitor in 4) */ void slotMultitrackView(bool enable); /** @brief Stop rendering preview. */ void stopPreviewRender(); private: Mlt::Tractor *m_tractor; QList m_tracks; /** @brief number of special overlay tracks to preview effects */ bool m_hasOverlayTrack; Mlt::Producer *m_overlayTrack; CustomRuler *m_ruler; CustomTrackView *m_trackview; QList m_invalidProducers; double m_scale; QString m_editMode; CustomTrackScene *m_scene; /** @brief A list of producer ids to be replaced when opening a corrupted document*/ QMap m_replacementProducerIds; KdenliveDoc *m_doc; int m_verticalZoom; QString m_documentErrors; QList m_trackActions; /** @brief sometimes grouped commands quickly send invalidate commands, so wait a little bit before processing*/ - QTimer m_previewGatherTimer; PreviewManager *m_timelinePreview; + bool m_usePreview; void adjustTrackHeaders(); void parseDocument(const QDomDocument &doc); int loadTrack(int ix, int offset, Mlt::Playlist &playlist, int start = 0, int end = -1, bool updateReferences = true); void getEffects(Mlt::Service &service, ClipItem *clip, int track = 0); void adjustDouble(QDomElement &e, const QString &value); /** @brief Adjust kdenlive effect xml parameters to the MLT value*/ void adjustparameterValue(QDomNodeList clipeffectparams, const QString ¶mname, const QString ¶mvalue); /** @brief Enable/disable track actions depending on number of tracks */ void refreshTrackActions(); /** @brief load existing timeline previews */ void loadPreviewRender(); void initializePreview(); private slots: void slotSwitchTrackComposite(int trackIndex, bool enable); void setCursorPos(int pos); void moveCursorPos(int pos); /** @brief The tracks count or a track name changed, rebuild and notify */ void slotReloadTracks(); void slotVerticalZoomDown(); void slotVerticalZoomUp(); /** @brief Changes the name of a track. * @param ix Number of the track * @param name New name */ void slotRenameTrack(int ix, const QString &name); void slotRepaintTracks(); /** @brief Adjusts the margins of the header area. * * Avoid a shift between header area and trackview if * the horizontal scrollbar is visible and the position * of the vertical scrollbar is maximal */ void slotUpdateVerticalScroll(int min, int max); /** @brief Update the track label showing applied effects.*/ void slotUpdateTrackEffectState(int); /** @brief Toggle use of timeline zone for editing.*/ void slotEnableZone(bool enable); - void gotPreviewRender(int frame, const QString &file, int progress); - void invalidatePreview(int startFrame, int endFrame); - void slotReloadChunks(QDir cacheDir, QList chunks, const QString ext); signals: void mousePosition(int); void cursorMoved(); void zoneMoved(int, int); void configTrack(); void updateTracksInfo(); void setZoom(int); void showTrackEffects(int, const TrackInfo&); /** @brief Indicate how many clips we are going to load */ void startLoadingBin(int); /** @brief Indicate which clip we are currently loading */ void loadingBin(int); /** @brief We are about to reload timeline, reset bin clip usage */ void resetUsageCount(); }; #endif