diff --git a/data/effects/frei0r_iirblur.xml b/data/effects/frei0r_iirblur.xml
index f190444a4..6ff3ee63d 100644
--- a/data/effects/frei0r_iirblur.xml
+++ b/data/effects/frei0r_iirblur.xml
@@ -1,23 +1,21 @@
Blur
Blur using 2D IIR filters (Exponential, Lowpass, Gaussian)
Marko Cebokli
-
-
+
Amount
Amount of blur
-
-
+
Exponential,Lowpass,Gaussian
Type
Select blurring algorithm
Edge
Enable edge compensation
-
\ No newline at end of file
+
diff --git a/src/assets/abstractassetsrepository.ipp b/src/assets/abstractassetsrepository.ipp
index edf168d97..50f16d7fb 100644
--- a/src/assets/abstractassetsrepository.ipp
+++ b/src/assets/abstractassetsrepository.ipp
@@ -1,324 +1,315 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "xml/xml.hpp"
#include
#include
#include
#include
#include
#include
#ifdef Q_OS_MAC
#include
#endif
template AbstractAssetsRepository::AbstractAssetsRepository() {}
template void AbstractAssetsRepository::init()
{
// Warning: Mlt::Factory::init() resets the locale to the default system value, make sure we keep correct locale
#ifndef Q_OS_MAC
setlocale(LC_NUMERIC, nullptr);
#else
setlocale(LC_NUMERIC_MASK, nullptr);
#endif
// Parse effects blacklist
parseBlackList(assetBlackListPath());
parseFavorites();
// Retrieve the list of MLT's available assets.
QScopedPointer assets(retrieveListFromMlt());
int max = assets->count();
QString sox = QStringLiteral("sox.");
for (int i = 0; i < max; ++i) {
Info info;
QString name = assets->get_name(i);
info.id = name;
if (name.startsWith(sox)) {
// sox effects are not usage directly (parameters not available)
continue;
}
// qDebug() << "trying to parse " < customAssets;
for (const auto &dir : asset_dirs) {
QDir current_dir(dir);
QStringList filter;
filter << QStringLiteral("*.xml");
QStringList fileList = current_dir.entryList(filter, QDir::Files);
for (const auto &file : fileList) {
QString path = current_dir.absoluteFilePath(file);
parseCustomAssetFile(path, customAssets);
}
}
// We add the custom assets
for (const auto &custom : customAssets) {
// Custom assets should override default ones
m_assets[custom.first] = custom.second;
/*if (m_assets.count(custom.second.mltId) > 0) {
m_assets.erase(custom.second.mltId);
}
if (m_assets.count(custom.first) == 0) {
m_assets[custom.first] = custom.second;
} else {
qDebug() << "Error: conflicting asset name " << custom.first;
}*/
}
}
template void AbstractAssetsRepository::parseBlackList(const QString &path)
{
QFile blacklist_file(path);
if (blacklist_file.open(QIODevice::ReadOnly)) {
QTextStream stream(&blacklist_file);
QString line;
while (stream.readLineInto(&line)) {
line = line.simplified();
if (!line.isEmpty() && !line.startsWith('#')) {
m_blacklist.insert(line);
}
}
blacklist_file.close();
}
}
template bool AbstractAssetsRepository::parseInfoFromMlt(const QString &assetId, Info &res)
{
QScopedPointer metadata(getMetadata(assetId));
if (metadata && metadata->is_valid()) {
if (metadata->get("title") && metadata->get("identifier") && strlen(metadata->get("title")) > 0) {
res.name = metadata->get("title");
res.name[0] = res.name[0].toUpper();
res.description = metadata->get("description");
res.author = metadata->get("creator");
res.version_str = metadata->get("version");
res.version = ceil(100 * metadata->get_double("version"));
res.id = res.mltId = assetId;
parseType(metadata, res);
// Create params
QDomDocument doc;
QDomElement eff = doc.createElement(QStringLiteral("effect"));
QString id = metadata->get("identifier");
eff.setAttribute(QStringLiteral("tag"), id);
eff.setAttribute(QStringLiteral("id"), id);
////qCDebug(KDENLIVE_LOG)<<"Effect: "<get_data("parameters"));
for (int j = 0; param_props.is_valid() && j < param_props.count(); ++j) {
QDomElement params = doc.createElement(QStringLiteral("parameter"));
Mlt::Properties paramdesc((mlt_properties) param_props.get_data(param_props.get_name(j)));
params.setAttribute(QStringLiteral("name"), paramdesc.get("identifier"));
if (params.attribute(QStringLiteral("name")) == QLatin1String("argument")) {
// This parameter has to be given as attribute when using command line, do not show it in Kdenlive
continue;
}
if (paramdesc.get("readonly") && !strcmp(paramdesc.get("readonly"), "yes")) {
// Do not expose readonly parameters
continue;
}
if (paramdesc.get("maximum")) {
params.setAttribute(QStringLiteral("max"), paramdesc.get("maximum"));
}
if (paramdesc.get("minimum")) {
params.setAttribute(QStringLiteral("min"), paramdesc.get("minimum"));
}
QString paramType = paramdesc.get("type");
if (paramType == QLatin1String("integer")) {
if (params.attribute(QStringLiteral("min")) == QLatin1String("0") && params.attribute(QStringLiteral("max")) == QLatin1String("1")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("bool"));
} else {
params.setAttribute(QStringLiteral("type"), QStringLiteral("constant"));
}
} else if (paramType == QLatin1String("float")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("constant"));
// param type is float, set default decimals to 3
params.setAttribute(QStringLiteral("decimals"), QStringLiteral("3"));
} else if (paramType == QLatin1String("boolean")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("bool"));
} else if (paramType == QLatin1String("geometry")) {
params.setAttribute(QStringLiteral("type"), QStringLiteral("geometry"));
} else if (paramType == QLatin1String("string")) {
// string parameter are not really supported, so if we have a default value, enforce it
params.setAttribute(QStringLiteral("type"), QStringLiteral("fixed"));
if (paramdesc.get("default")) {
QString stringDefault = paramdesc.get("default");
stringDefault.remove(QLatin1Char('\''));
params.setAttribute(QStringLiteral("value"), stringDefault);
} else {
// String parameter without default, skip it completely
continue;
}
} else {
params.setAttribute(QStringLiteral("type"), paramType);
if (!QString(paramdesc.get("format")).isEmpty()) {
params.setAttribute(QStringLiteral("format"), paramdesc.get("format"));
}
}
if (!params.hasAttribute(QStringLiteral("value"))) {
if (paramdesc.get("default")) {
params.setAttribute(QStringLiteral("default"), paramdesc.get("default"));
}
if (paramdesc.get("value")) {
params.setAttribute(QStringLiteral("value"), paramdesc.get("value"));
} else {
params.setAttribute(QStringLiteral("value"), paramdesc.get("default"));
}
}
QString paramName = paramdesc.get("title");
if (!paramName.isEmpty()) {
QDomElement pname = doc.createElement(QStringLiteral("name"));
pname.appendChild(doc.createTextNode(paramName));
params.appendChild(pname);
}
if (paramdesc.get("description")) {
QDomElement comment = doc.createElement(QStringLiteral("comment"));
comment.appendChild(doc.createTextNode(paramdesc.get("description")));
params.appendChild(comment);
}
eff.appendChild(params);
}
doc.appendChild(eff);
res.xml = eff;
return true;
}
}
return false;
}
template bool AbstractAssetsRepository::exists(const QString &assetId) const
{
return m_assets.count(assetId) > 0;
}
template QVector> AbstractAssetsRepository::getNames() const
{
QVector> res;
res.reserve((int)m_assets.size());
for (const auto &asset : m_assets) {
res.push_back({asset.first, asset.second.name});
}
std::sort(res.begin(), res.end(), [](const QPair &a, const QPair &b) { return a.second < b.second; });
return res;
}
template AssetType AbstractAssetsRepository::getType(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).type;
}
template QString AbstractAssetsRepository::getName(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).name;
}
template QString AbstractAssetsRepository::getDescription(const QString &assetId) const
{
Q_ASSERT(m_assets.count(assetId) > 0);
return m_assets.at(assetId).description;
}
template bool AbstractAssetsRepository::parseInfoFromXml(const QDomElement ¤tAsset, Info &res) const
{
- QLocale locale;
-
- // We first deal with locale
- if (currentAsset.hasAttribute(QStringLiteral("LC_NUMERIC"))) {
- // set a locale for that effect
- locale = QLocale(currentAsset.attribute(QStringLiteral("LC_NUMERIC")));
- }
- locale.setNumberOptions(QLocale::OmitGroupSeparator);
-
QString tag = currentAsset.attribute(QStringLiteral("tag"), QString());
QString id = currentAsset.attribute(QStringLiteral("id"), QString());
if (id.isEmpty()) {
id = tag;
}
if (!exists(tag)) {
qDebug() << "++++++ Unknown asset : " << tag;
return false;
}
// Check if there is a maximal version set
if (currentAsset.hasAttribute(QStringLiteral("version"))) {
// a specific version of the filter is required
if (m_assets.at(tag).version < (int)(100 * currentAsset.attribute(QStringLiteral("version")).toDouble())) {
return false;
}
}
res = m_assets.at(tag);
res.id = id;
res.mltId = tag;
// Update description if the xml provide one
QString description = Xml::getSubTagContent(currentAsset, QStringLiteral("description"));
if (!description.isEmpty()) {
res.description = description;
}
// Update name if the xml provide one
QString name = Xml::getSubTagContent(currentAsset, QStringLiteral("name"));
if (!name.isEmpty()) {
res.name = name;
}
return true;
}
template QDomElement AbstractAssetsRepository::getXml(const QString &assetId) const
{
if (m_assets.count(assetId) == 0) {
qDebug() << "Error : Requesting info on unknown transition " << assetId;
return QDomElement();
}
return m_assets.at(assetId).xml.cloneNode().toElement();
}
diff --git a/src/assets/assetlist/view/qml/assetList.qml b/src/assets/assetlist/view/qml/assetList.qml
index 32ff45c96..d5d45b1a9 100644
--- a/src/assets/assetlist/view/qml/assetList.qml
+++ b/src/assets/assetlist/view/qml/assetList.qml
@@ -1,378 +1,378 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQuick.Controls 1.5
import QtQuick.Controls.Styles 1.4
import QtQuick.Window 2.2
import QtQml.Models 2.2
Rectangle {
id: listRoot
SystemPalette { id: activePalette }
color: activePalette.window
function assetType(){
return isEffectList ? i18n("effects") : i18n("transitions");
}
function expandNodes(indexes) {
for(var i = 0; i < indexes.length; i++) {
if (indexes[i].valid) {
treeView.expand(indexes[i]);
}
}
}
function rowPosition(model, index) {
var pos = 0;
for(var i = 0; i < index.parent.row; i++) {
var catIndex = model.getCategory(i);
if (treeView.isExpanded(catIndex)) {
pos += model.rowCount(catIndex);
}
pos ++;
}
pos += index.row + 2;
return pos;
}
ColumnLayout {
anchors.fill: parent
spacing: 0
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: false
spacing: 4
ExclusiveGroup { id: filterGroup}
ToolButton {
id: searchList
iconName: "edit-find"
checkable: true
tooltip: isEffectList ? i18n('Find effect') : i18n('Find composition')
onCheckedChanged: {
searchInput.visible = searchList.checked
searchInput.focus = searchList.checked
if (!searchList.checked) {
searchInput.text = ''
treeView.focus = true
}
}
}
ToolButton {
id: showAll
iconName: "show-all-effects"
checkable:true
exclusiveGroup: filterGroup
tooltip: i18n('Show all ')+assetType()
onClicked: {
assetlist.setFilterType("")
}
}
ToolButton {
id: showVideo
visible: isEffectList
iconName: "kdenlive-show-video"
iconSource: 'qrc:///pics/kdenlive-show-video.svgz'
checkable:true
exclusiveGroup: filterGroup
tooltip: i18n('Show all video effects')
onClicked: {
assetlist.setFilterType("video")
}
}
ToolButton {
id: showAudio
visible: isEffectList
iconName: "kdenlive-show-audio"
iconSource: 'qrc:///pics/kdenlive-show-audio.svgz'
checkable:true
exclusiveGroup: filterGroup
tooltip: i18n('Show all audio effects')
onClicked: {
assetlist.setFilterType("audio")
}
}
ToolButton {
id: showCustom
visible: isEffectList
iconName: "kdenlive-custom-effect"
checkable:true
exclusiveGroup: filterGroup
tooltip: i18n('Show all custom effects')
onClicked: {
assetlist.setFilterType("custom")
}
}
ToolButton {
id: showFavorites
iconName: "favorite"
checkable:true
exclusiveGroup: filterGroup
tooltip: i18n('Show favorite items')
onClicked: {
assetlist.setFilterType("favorites")
}
}
ToolButton {
id: downloadTransitions
visible: !isEffectList
iconName: "edit-download"
tooltip: i18n('Download New Wipes...')
onClicked: {
assetlist.downloadNewLumas()
}
}
Rectangle {
//This is a spacer
Layout.fillHeight: false
Layout.fillWidth: true
color: "transparent"
}
ToolButton {
id: showDescription
iconName: "help-about"
checkable:true
tooltip: i18n('Show/hide description of the ') + assetType()
onCheckedChanged:{
assetlist.showDescription = checked
}
Component.onCompleted: checked = assetlist.showDescription
}
}
TextField {
id: searchInput
Layout.fillWidth:true
visible: false
Image {
id: clear
source: 'image://icon/edit-clear'
width: parent.height * 0.8
height: width
anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter }
opacity: 0
MouseArea {
anchors.fill: parent
onClicked: { searchInput.text = ''; searchInput.focus = true; searchList.checked = false; }
}
}
states: State {
name: "hasText"; when: searchInput.text != ''
PropertyChanges { target: clear; opacity: 1 }
}
transitions: [
Transition {
from: ""; to: "hasText"
NumberAnimation { properties: "opacity" }
},
Transition {
from: "hasText"; to: ""
NumberAnimation { properties: "opacity" }
}
]
onTextChanged: {
var current = sel.currentIndex
var rowModelIndex = assetListModel.getModelIndex(sel.currentIndex);
assetlist.setFilterName(text)
if (text.length > 0) {
sel.setCurrentIndex(assetListModel.firstVisibleItem(current), ItemSelectionModel.ClearAndSelect)
} else {
sel.clearCurrentIndex()
sel.setCurrentIndex(assetListModel.getProxyIndex(rowModelIndex), ItemSelectionModel.ClearAndSelect)
}
treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible)
}
onEditingFinished: {
if (!assetContextMenu.isDisplayed) {
searchList.checked = false
}
}
Keys.onDownPressed: {
sel.setCurrentIndex(assetListModel.getNextChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect)
treeView.expand(sel.currentIndex.parent)
treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible)
}
Keys.onUpPressed: {
sel.setCurrentIndex(assetListModel.getPreviousChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect)
treeView.expand(sel.currentIndex.parent)
treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible)
}
Keys.onReturnPressed: {
if (sel.hasSelection) {
assetlist.activate(sel.currentIndex)
searchList.checked = false
}
}
}
ItemSelectionModel {
id: sel
model: assetListModel
onSelectionChanged: {
assetDescription.text = assetlist.getDescription(sel.currentIndex)
}
}
SplitView {
orientation: Qt.Vertical
Layout.fillHeight: true
Layout.fillWidth: true
TreeView {
id: treeView
Layout.fillHeight: true
Layout.fillWidth: true
alternatingRowColors: false
headerVisible: false
selection: sel
selectionMode: SelectionMode.SingleSelection
itemDelegate: Rectangle {
id: assetDelegate
// These anchors are important to allow "copy" dragging
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
anchors.right: parent ? parent.right : undefined
- property bool isItem : styleData.value != "root" && styleData.value != ""
+ property bool isItem : styleData.value !== "root" && styleData.value !== ""
property string mimeType : isItem ? assetlist.getMimeType(styleData.value) : ""
height: assetText.implicitHeight + 8
color: "transparent"
Drag.active: isItem ? dragArea.drag.active : false
Drag.dragType: Drag.Automatic
Drag.supportedActions: Qt.CopyAction
Drag.mimeData: isItem ? assetlist.getMimeData(styleData.value) : {}
Drag.keys:[
isItem ? assetlist.getMimeType(styleData.value) : ""
]
Row {
anchors.fill:parent
anchors.leftMargin: 2
anchors.topMargin: 2
anchors.bottomMargin: 2
spacing: 2
Image{
id: assetThumb
visible: assetDelegate.isItem
- property bool isFavorite: model == undefined || model.favorite == undefined ? false : model.favorite
+ property bool isFavorite: model == undefined || model.favorite === undefined ? false : model.favorite
height: parent.height
width: height
source: 'image://asseticon/' + styleData.value
}
Label {
id: assetText
font.bold : assetThumb.isFavorite
text: assetlist.getName(styleData.index)
}
}
MouseArea {
id: dragArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
drag.target: undefined
onReleased: {
drag.target = undefined
}
onPressed: {
if (isItem) {
sel.setCurrentIndex(styleData.index, ItemSelectionModel.ClearAndSelect)
if (mouse.button === Qt.LeftButton) {
drag.target = parent
parent.grabToImage(function(result) {
parent.Drag.imageSource = result.url
})
} else {
drag.target = undefined
assetContextMenu.isItemFavorite = assetThumb.isFavorite
assetContextMenu.popup()
mouse.accepted = false
}
console.log(parent.Drag.keys)
} else {
if (treeView.isExpanded(styleData.index)) {
treeView.collapse(styleData.index)
} else {
treeView.expand(styleData.index)
}
}
}
onDoubleClicked: {
if (isItem) {
assetlist.activate(styleData.index)
}
}
}
}
Menu {
id: assetContextMenu
property bool isItemFavorite
property bool isDisplayed: false
MenuItem {
id: favMenu
text: assetContextMenu.isItemFavorite ? "Remove from favorites" : "Add to favorites"
property url thumbSource
onTriggered: {
assetlist.setFavorite(sel.currentIndex, !assetContextMenu.isItemFavorite)
}
}
onAboutToShow: {
isDisplayed = true;
}
onAboutToHide: {
isDisplayed = false;
}
}
TableViewColumn { role: "identifier"; title: "Name"; }
model: assetListModel
Keys.onDownPressed: {
sel.setCurrentIndex(assetListModel.getNextChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect)
treeView.expand(sel.currentIndex.parent)
treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible)
}
Keys.onUpPressed: {
sel.setCurrentIndex(assetListModel.getPreviousChild(sel.currentIndex), ItemSelectionModel.ClearAndSelect)
treeView.expand(sel.currentIndex.parent)
treeView.__listView.positionViewAtIndex(rowPosition(assetListModel, sel.currentIndex), ListView.Visible)
}
Keys.onReturnPressed: {
if (sel.hasSelection) {
assetlist.activate(sel.currentIndex)
}
}
}
TextArea {
id: assetDescription
text: ""
visible: showDescription.checked
readOnly: true
Layout.fillWidth: true
states: State {
name: "hasDescription"; when: assetDescription.text != '' && showDescription.checked
PropertyChanges { target: assetDescription; visible: true}
}
}
}
}
}
diff --git a/src/assets/model/assetparametermodel.cpp b/src/assets/model/assetparametermodel.cpp
index e75300173..e592d9241 100644
--- a/src/assets/model/assetparametermodel.cpp
+++ b/src/assets/model/assetparametermodel.cpp
@@ -1,804 +1,817 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "assetparametermodel.hpp"
#include "assets/keyframes/model/keyframemodellist.hpp"
#include "core.h"
#include "kdenlivesettings.h"
#include "klocalizedstring.h"
#include "profiles/profilemodel.hpp"
#include
#include
#include
#include
#include
AssetParameterModel::AssetParameterModel(std::unique_ptr asset, const QDomElement &assetXml, const QString &assetId, ObjectId ownerId,
QObject *parent)
: QAbstractListModel(parent)
, monitorId(ownerId.first == ObjectType::BinClip ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor)
, m_assetId(assetId)
, m_ownerId(ownerId)
, m_asset(std::move(asset))
, m_keyframes(nullptr)
{
Q_ASSERT(m_asset->is_valid());
QDomNodeList nodeList = assetXml.elementsByTagName(QStringLiteral("parameter"));
m_hideKeyframesByDefault = assetXml.hasAttribute(QStringLiteral("hideKeyframes"));
bool needsLocaleConversion = false;
QChar separator, oldSeparator;
- // Check locale
+ // Check locale, default effects xml has no LC_NUMERIC defined and always uses the C locale
+ QLocale locale;
if (assetXml.hasAttribute(QStringLiteral("LC_NUMERIC"))) {
- QLocale locale = QLocale(assetXml.attribute(QStringLiteral("LC_NUMERIC")));
- if (locale.decimalPoint() != QLocale().decimalPoint()) {
+ QLocale effectLocale = QLocale(assetXml.attribute(QStringLiteral("LC_NUMERIC")));
+ if (QLocale::c().decimalPoint() != effectLocale.decimalPoint()) {
needsLocaleConversion = true;
- separator = QLocale().decimalPoint();
- oldSeparator = locale.decimalPoint();
+ separator = QLocale::c().decimalPoint();
+ oldSeparator = effectLocale.decimalPoint();
}
}
qDebug() << "XML parsing of " << assetId << ". found : " << nodeList.count();
for (int i = 0; i < nodeList.count(); ++i) {
QDomElement currentParameter = nodeList.item(i).toElement();
// Convert parameters if we need to
if (needsLocaleConversion) {
QDomNamedNodeMap attrs = currentParameter.attributes();
for (int k = 0; k < attrs.count(); ++k) {
QString nodeName = attrs.item(k).nodeName();
if (nodeName != QLatin1String("type") && nodeName != QLatin1String("name")) {
QString val = attrs.item(k).nodeValue();
if (val.contains(oldSeparator)) {
QString newVal = val.replace(oldSeparator, separator);
attrs.item(k).setNodeValue(newVal);
}
}
}
}
// Parse the basic attributes of the parameter
QString name = currentParameter.attribute(QStringLiteral("name"));
QString type = currentParameter.attribute(QStringLiteral("type"));
QString value = currentParameter.attribute(QStringLiteral("value"));
ParamRow currentRow;
currentRow.type = paramTypeFromStr(type);
currentRow.xml = currentParameter;
- QLocale locale;
if (value.isNull()) {
QVariant defaultValue = parseAttribute(m_ownerId, QStringLiteral("default"), currentParameter);
value = defaultValue.type() == QVariant::Double ? locale.toString(defaultValue.toDouble()) : defaultValue.toString();
}
bool isFixed = (type == QLatin1String("fixed"));
if (isFixed) {
m_fixedParams[name] = value;
} else if (currentRow.type == ParamType::Position) {
int val = value.toInt();
if (val < 0) {
int in = pCore->getItemIn(m_ownerId);
int out = in + pCore->getItemDuration(m_ownerId);
val += out;
value = QString::number(val);
}
} else if (currentRow.type == ParamType::KeyframeParam || currentRow.type == ParamType::AnimatedRect) {
if (!value.contains(QLatin1Char('='))) {
value.prepend(QStringLiteral("%1=").arg(pCore->getItemIn(m_ownerId)));
}
}
if (!name.isEmpty()) {
setParameter(name, value, false);
// Keep track of param order
m_paramOrder.push_back(name);
}
if (isFixed) {
// fixed parameters are not displayed so we don't store them.
continue;
}
currentRow.value = value;
QString title = currentParameter.firstChildElement(QStringLiteral("name")).text();
currentRow.name = title.isEmpty() ? name : title;
m_params[name] = currentRow;
m_rows.push_back(name);
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Sox effects need to have a special "Effect" value set
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
}
qDebug() << "END parsing of " << assetId << ". Number of found parameters" << m_rows.size();
emit modelChanged();
}
void AssetParameterModel::prepareKeyframes()
{
if (m_keyframes) return;
int ix = 0;
for (const auto &name : m_rows) {
if (m_params[name].type == ParamType::KeyframeParam || m_params[name].type == ParamType::AnimatedRect ||
m_params[name].type == ParamType::Roto_spline) {
addKeyframeParam(index(ix, 0));
}
ix++;
}
}
void AssetParameterModel::setParameter(const QString &name, const int value, bool update)
{
Q_ASSERT(m_asset->is_valid());
m_asset->set(name.toLatin1().constData(), value);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = value;
} else {
m_fixedParams[name] = value;
}
if (update) {
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
emit modelChanged();
emit dataChanged(index(0, 0), index(m_rows.count() - 1, 0), {});
}
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
void AssetParameterModel::setParameter(const QString &name, const QString ¶mValue, bool update, const QModelIndex ¶mIndex)
{
Q_ASSERT(m_asset->is_valid());
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
- qDebug() << "// PROCESSING PARAM CHANGE: " << name << ", UPDATE: "<() == ParamType::Curve) {
QStringList vals = paramValue.split(QLatin1Char(';'), QString::SkipEmptyParts);
int points = vals.size();
m_asset->set("3", points / 10.);
// for the curve, inpoints are numbered: 6, 8, 10, 12, 14
// outpoints, 7, 9, 11, 13,15 so we need to deduce these enums
for (int i = 0; i < points; i++) {
QString pointVal = vals.at(i);
int idx = 2 * i + 6;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 0, 0).toDouble());
idx++;
m_asset->set(QString::number(idx).toLatin1().constData(), pointVal.section(QLatin1Char('/'), 1, 1).toDouble());
}
}
bool conversionSuccess;
double doubleValue = locale.toDouble(paramValue, &conversionSuccess);
if (conversionSuccess) {
m_asset->set(name.toLatin1().constData(), doubleValue);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = doubleValue;
} else {
m_fixedParams[name] = doubleValue;
}
} else {
m_asset->set(name.toLatin1().constData(), paramValue.toUtf8().constData());
qDebug() << " = = SET EFFECT PARAM: " << name << " = " << paramValue;
if (m_fixedParams.count(name) == 0) {
m_params[name].value = paramValue;
} else {
m_fixedParams[name] = paramValue;
}
}
if (update) {
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
qDebug() << "// SENDING DATA CHANGE....";
if (paramIndex.isValid()) {
emit dataChanged(paramIndex, paramIndex);
} else {
QModelIndex ix = index(m_rows.indexOf(name), 0);
emit dataChanged(ix, ix);
}
emit modelChanged();
}
}
emit updateChildren(name);
// Update timeline view if necessary
if (m_ownerId.first == ObjectType::NoItem) {
// Used for generator clips
if (!update) emit modelChanged();
} else {
// Update fades in timeline
pCore->updateItemModel(m_ownerId, m_assetId);
// Trigger monitor refresh
pCore->refreshProjectItem(m_ownerId);
// Invalidate timeline preview
pCore->invalidateItem(m_ownerId);
}
}
void AssetParameterModel::setParameter(const QString &name, double &value)
{
Q_ASSERT(m_asset->is_valid());
m_asset->set(name.toLatin1().constData(), value);
if (m_fixedParams.count(name) == 0) {
m_params[name].value = value;
} else {
m_fixedParams[name] = value;
}
if (m_assetId.startsWith(QStringLiteral("sox_"))) {
// Warning, SOX effect, need unplug/replug
qDebug() << "// Warning, SOX effect, need unplug/replug";
QStringList effectParam = {m_assetId.section(QLatin1Char('_'), 1)};
for (const QString &pName : m_paramOrder) {
effectParam << m_asset->get(pName.toUtf8().constData());
}
m_asset->set("effect", effectParam.join(QLatin1Char(' ')).toUtf8().constData());
emit replugEffect(shared_from_this());
} else if (m_assetId == QLatin1String("autotrack_rectangle") || m_assetId.startsWith(QStringLiteral("ladspa"))) {
// these effects don't understand param change and need to be rebuild
emit replugEffect(shared_from_this());
} else {
emit modelChanged();
}
pCore->refreshProjectItem(m_ownerId);
pCore->invalidateItem(m_ownerId);
}
AssetParameterModel::~AssetParameterModel() = default;
QVariant AssetParameterModel::data(const QModelIndex &index, int role) const
{
if (index.row() < 0 || index.row() >= m_rows.size() || !index.isValid()) {
return QVariant();
}
QString paramName = m_rows[index.row()];
Q_ASSERT(m_params.count(paramName) > 0);
const QDomElement &element = m_params.at(paramName).xml;
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
return m_params.at(paramName).name;
case NameRole:
return paramName;
case TypeRole:
return QVariant::fromValue(m_params.at(paramName).type);
case CommentRole: {
QDomElement commentElem = element.firstChildElement(QStringLiteral("comment"));
QString comment;
if (!commentElem.isNull()) {
comment = i18n(commentElem.text().toUtf8().data());
}
return comment;
}
case InRole:
return m_asset->get_int("in");
case OutRole:
return m_asset->get_int("out");
case ParentInRole:
return pCore->getItemIn(m_ownerId);
case ParentDurationRole:
return pCore->getItemDuration(m_ownerId);
case ParentPositionRole:
return pCore->getItemPosition(m_ownerId);
case HideKeyframesFirstRole:
return m_hideKeyframesByDefault;
case MinRole:
return parseAttribute(m_ownerId, QStringLiteral("min"), element);
case MaxRole:
return parseAttribute(m_ownerId, QStringLiteral("max"), element);
case FactorRole:
return parseAttribute(m_ownerId, QStringLiteral("factor"), element, 1);
case ScaleRole:
return parseAttribute(m_ownerId, QStringLiteral("scale"), element, 0);
case DecimalsRole:
return parseAttribute(m_ownerId, QStringLiteral("decimals"), element);
case DefaultRole:
return parseAttribute(m_ownerId, QStringLiteral("default"), element);
case FilterRole:
return parseAttribute(m_ownerId, QStringLiteral("filter"), element);
case SuffixRole:
return element.attribute(QStringLiteral("suffix"));
case OpacityRole:
return element.attribute(QStringLiteral("opacity")) != QLatin1String("false");
case RelativePosRole:
return element.attribute(QStringLiteral("relative")) == QLatin1String("true");
case ShowInTimelineRole:
return !element.hasAttribute(QStringLiteral("notintimeline"));
case AlphaRole:
return element.attribute(QStringLiteral("alpha")) == QLatin1String("1");
case ValueRole: {
QString value(m_asset->get(paramName.toUtf8().constData()));
return value.isEmpty() ? (element.attribute(QStringLiteral("value")).isNull() ? parseAttribute(m_ownerId, QStringLiteral("default"), element)
: element.attribute(QStringLiteral("value")))
: value;
}
case ListValuesRole:
return element.attribute(QStringLiteral("paramlist")).split(QLatin1Char(';'));
case ListNamesRole: {
QDomElement namesElem = element.firstChildElement(QStringLiteral("paramlistdisplay"));
return i18n(namesElem.text().toUtf8().data()).split(QLatin1Char(','));
}
case List1Role:
return parseAttribute(m_ownerId, QStringLiteral("list1"), element);
case List2Role:
return parseAttribute(m_ownerId, QStringLiteral("list2"), element);
case Enum1Role:
return m_asset->get_double("1");
case Enum2Role:
return m_asset->get_double("2");
case Enum3Role:
return m_asset->get_double("3");
case Enum4Role:
return m_asset->get_double("4");
case Enum5Role:
return m_asset->get_double("5");
case Enum6Role:
return m_asset->get_double("6");
case Enum7Role:
return m_asset->get_double("7");
case Enum8Role:
return m_asset->get_double("8");
case Enum9Role:
return m_asset->get_double("9");
case Enum10Role:
return m_asset->get_double("10");
case Enum11Role:
return m_asset->get_double("11");
case Enum12Role:
return m_asset->get_double("12");
case Enum13Role:
return m_asset->get_double("13");
case Enum14Role:
return m_asset->get_double("14");
case Enum15Role:
return m_asset->get_double("15");
}
return QVariant();
}
int AssetParameterModel::rowCount(const QModelIndex &parent) const
{
qDebug() << "===================================================== Requested rowCount" << parent << m_rows.size();
if (parent.isValid()) return 0;
return m_rows.size();
}
// static
ParamType AssetParameterModel::paramTypeFromStr(const QString &type)
{
if (type == QLatin1String("double") || type == QLatin1String("float") || type == QLatin1String("constant")) {
return ParamType::Double;
}
if (type == QLatin1String("list")) {
return ParamType::List;
}
if (type == QLatin1String("bool")) {
return ParamType::Bool;
}
if (type == QLatin1String("switch")) {
return ParamType::Switch;
} else if (type == QLatin1String("simplekeyframe")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("animatedrect")) {
return ParamType::AnimatedRect;
} else if (type == QLatin1String("geometry")) {
return ParamType::Geometry;
} else if (type == QLatin1String("addedgeometry")) {
return ParamType::Addedgeometry;
} else if (type == QLatin1String("keyframe") || type == QLatin1String("animated")) {
return ParamType::KeyframeParam;
} else if (type == QLatin1String("color")) {
return ParamType::Color;
} else if (type == QLatin1String("colorwheel")) {
return ParamType::ColorWheel;
} else if (type == QLatin1String("position")) {
return ParamType::Position;
} else if (type == QLatin1String("curve")) {
return ParamType::Curve;
} else if (type == QLatin1String("bezier_spline")) {
return ParamType::Bezier_spline;
} else if (type == QLatin1String("roto-spline")) {
return ParamType::Roto_spline;
} else if (type == QLatin1String("wipe")) {
return ParamType::Wipe;
} else if (type == QLatin1String("url")) {
return ParamType::Url;
} else if (type == QLatin1String("keywords")) {
return ParamType::Keywords;
} else if (type == QLatin1String("fontfamily")) {
return ParamType::Fontfamily;
} else if (type == QLatin1String("filterjob")) {
return ParamType::Filterjob;
} else if (type == QLatin1String("readonly")) {
return ParamType::Readonly;
} else if (type == QLatin1String("hidden")) {
return ParamType::Hidden;
}
qDebug() << "WARNING: Unknown type :" << type;
return ParamType::Double;
}
// static
QString AssetParameterModel::getDefaultKeyframes(int start, const QString &defaultValue, bool linearOnly)
{
QString keyframes = QString::number(start);
if (linearOnly) {
keyframes.append(QLatin1Char('='));
} else {
switch (KdenliveSettings::defaultkeyframeinterp()) {
case mlt_keyframe_discrete:
keyframes.append(QStringLiteral("|="));
break;
case mlt_keyframe_smooth:
keyframes.append(QStringLiteral("~="));
break;
default:
keyframes.append(QLatin1Char('='));
break;
}
}
keyframes.append(defaultValue);
return keyframes;
}
// static
QVariant AssetParameterModel::parseAttribute(const ObjectId owner, const QString &attribute, const QDomElement &element, QVariant defaultValue)
{
if (!element.hasAttribute(attribute) && !defaultValue.isNull()) {
return defaultValue;
}
ParamType type = paramTypeFromStr(element.attribute(QStringLiteral("type")));
QString content = element.attribute(attribute);
if (content.contains(QLatin1Char('%'))) {
std::unique_ptr &profile = pCore->getCurrentProfile();
int width = profile->width();
int height = profile->height();
int in = pCore->getItemIn(owner);
int out = in + pCore->getItemDuration(owner);
// replace symbols in the double parameter
content.replace(QLatin1String("%maxWidth"), QString::number(width))
.replace(QLatin1String("%maxHeight"), QString::number(height))
.replace(QLatin1String("%width"), QString::number(width))
.replace(QLatin1String("%height"), QString::number(height))
.replace(QLatin1String("%out"), QString::number(out));
if (type == ParamType::Double) {
// Use a Mlt::Properties to parse mathematical operators
Mlt::Properties p;
p.set("eval", content.toLatin1().constData());
return p.get_double("eval");
}
- } else if (type == ParamType::Double) {
+ } else if (type == ParamType::Double || type == ParamType::Hidden) {
QLocale locale;
locale.setNumberOptions(QLocale::OmitGroupSeparator);
if (attribute == QLatin1String("default")) {
int factor = element.attribute(QStringLiteral("factor"), QStringLiteral("1")).toInt();
if (factor > 0) {
- return locale.toDouble(content) / factor;
+ return content.toDouble() / factor;
}
}
return locale.toDouble(content);
}
if (attribute == QLatin1String("default")) {
if (type == ParamType::RestrictedAnim) {
content = getDefaultKeyframes(0, content, true);
+ } else if (type == ParamType::KeyframeParam) {
+ return content.toDouble();
+ } else if (type == ParamType::List) {
+ bool ok;
+ double res = content.toDouble(&ok);
+ if (ok) {
+ return res;
+ }
+ } else if (type == ParamType::Bezier_spline) {
+ QLocale locale;
+ if (locale.decimalPoint() != QLocale::c().decimalPoint()) {
+ return content.replace(QLocale::c().decimalPoint(), locale.decimalPoint());
+ }
}
}
return content;
}
QString AssetParameterModel::getAssetId() const
{
return m_assetId;
}
QVector> AssetParameterModel::getAllParameters() const
{
QVector> res;
res.reserve((int)m_fixedParams.size() + (int)m_params.size());
for (const auto &fixed : m_fixedParams) {
res.push_back(QPair(fixed.first, fixed.second));
}
for (const auto ¶m : m_params) {
res.push_back(QPair(param.first, param.second.value));
}
return res;
}
QJsonDocument AssetParameterModel::toJson() const
{
QJsonArray list;
QLocale locale;
for (const auto &fixed : m_fixedParams) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(fixed.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(fixed.first));
currentParam.insert(QLatin1String("value"), fixed.second.toString());
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
for (const auto ¶m : m_params) {
QJsonObject currentParam;
QModelIndex ix = index(m_rows.indexOf(param.first), 0);
currentParam.insert(QLatin1String("name"), QJsonValue(param.first));
currentParam.insert(QLatin1String("value"), QJsonValue(param.second.value.toString()));
int type = data(ix, AssetParameterModel::TypeRole).toInt();
double min = data(ix, AssetParameterModel::MinRole).toDouble();
double max = data(ix, AssetParameterModel::MaxRole).toDouble();
double factor = data(ix, AssetParameterModel::FactorRole).toDouble();
if (factor > 0) {
min /= factor;
max /= factor;
}
currentParam.insert(QLatin1String("type"), QJsonValue(type));
currentParam.insert(QLatin1String("min"), QJsonValue(min));
currentParam.insert(QLatin1String("max"), QJsonValue(max));
list.push_back(currentParam);
}
return QJsonDocument(list);
}
void AssetParameterModel::deletePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
qDebug() << " * * ** JSON IS AN ARRAY, DELETING: " << presetName;
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
qDebug() << " * * ** JSON IS AN OBJECT, DELETING: " << presetName;
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
} else {
qDebug() << " * * ** JSON DOES NOT CONTAIN: " << obj.keys();
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
// TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
// TODO: error message
}
loadFile.write(QJsonDocument(array).toJson());
}
void AssetParameterModel::savePreset(const QString &presetFile, const QString &presetName)
{
QJsonObject object;
QJsonArray array;
QJsonDocument doc = toJson();
QFile loadFile(presetFile);
if (loadFile.exists()) {
if (loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isArray()) {
array = loadDoc.array();
QList toDelete;
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().keys().contains(presetName)) {
toDelete << i;
}
}
for (int i : toDelete) {
array.removeAt(i);
}
} else if (loadDoc.isObject()) {
QJsonObject obj = loadDoc.object();
if (obj.keys().contains(presetName)) {
obj.remove(presetName);
}
array.append(obj);
}
loadFile.close();
} else if (!loadFile.open(QIODevice::ReadWrite)) {
// TODO: error message
}
}
if (!loadFile.open(QIODevice::WriteOnly)) {
// TODO: error message
}
object[presetName] = doc.array();
array.append(object);
loadFile.write(QJsonDocument(array).toJson());
}
const QStringList AssetParameterModel::getPresetList(const QString &presetFile) const
{
QFile loadFile(presetFile);
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject()) {
qDebug() << "// PRESET LIST IS AN OBJECT!!!";
return loadDoc.object().keys();
} else if (loadDoc.isArray()) {
qDebug() << "// PRESET LIST IS AN ARRAY!!!";
QStringList result;
QJsonArray array = loadDoc.array();
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject()) {
result << val.toObject().keys();
}
}
return result;
}
}
return QStringList();
}
const QVector> AssetParameterModel::loadPreset(const QString &presetFile, const QString &presetName)
{
QFile loadFile(presetFile);
QVector> params;
if (loadFile.exists() && loadFile.open(QIODevice::ReadOnly)) {
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(QJsonDocument::fromJson(saveData));
if (loadDoc.isObject() && loadDoc.object().contains(presetName)) {
qDebug() << "..........\n..........\nLOADING OBJECT JSON";
QJsonValue val = loadDoc.object().value(presetName);
if (val.isObject()) {
QVariantMap map = val.toObject().toVariantMap();
QMap::const_iterator i = map.constBegin();
while (i != map.constEnd()) {
params.append({i.key(), i.value()});
++i;
}
}
} else if (loadDoc.isArray()) {
QJsonArray array = loadDoc.array();
for (int i = 0; i < array.size(); i++) {
QJsonValue val = array.at(i);
if (val.isObject() && val.toObject().contains(presetName)) {
QJsonValue preset = val.toObject().value(presetName);
if (preset.isArray()) {
QJsonArray paramArray = preset.toArray();
for (int j = 0; j < paramArray.size(); j++) {
QJsonValue v1 = paramArray.at(j);
if (v1.isObject()) {
QJsonObject ob = v1.toObject();
params.append({ob.value("name").toString(), ob.value("value").toVariant()});
}
}
}
qDebug() << "// LOADED PRESET: " << presetName << "\n" << params;
break;
}
}
}
}
return params;
}
void AssetParameterModel::setParameters(const QVector> ¶ms)
{
QLocale locale;
for (const auto ¶m : params) {
if (param.second.type() == QVariant::Double) {
setParameter(param.first, locale.toString(param.second.toDouble()), false);
} else {
setParameter(param.first, param.second.toString(), false);
}
}
if (m_keyframes) {
m_keyframes->refresh();
}
// emit modelChanged();
emit dataChanged(index(0), index(m_rows.count()), {});
}
ObjectId AssetParameterModel::getOwnerId() const
{
return m_ownerId;
}
void AssetParameterModel::addKeyframeParam(const QModelIndex index)
{
if (m_keyframes) {
m_keyframes->addParameter(index);
} else {
m_keyframes.reset(new KeyframeModelList(shared_from_this(), index, pCore->undoStack()));
}
}
std::shared_ptr AssetParameterModel::getKeyframeModel()
{
return m_keyframes;
}
void AssetParameterModel::resetAsset(std::unique_ptr asset)
{
m_asset = std::move(asset);
}
bool AssetParameterModel::hasMoreThanOneKeyframe() const
{
if (m_keyframes) {
return (!m_keyframes->isEmpty() && !m_keyframes->singleKeyframe());
}
return false;
}
int AssetParameterModel::time_to_frames(const QString time)
{
return m_asset->time_to_frames(time.toUtf8().constData());
}
void AssetParameterModel::passProperties(Mlt::Properties &target)
{
target.set("_profile", pCore->getCurrentProfile()->get_profile(), 0);
target.set_lcnumeric(m_asset->get_lcnumeric());
}
diff --git a/src/assets/view/assetparameterview.cpp b/src/assets/view/assetparameterview.cpp
index 070ae4093..adf435985 100644
--- a/src/assets/view/assetparameterview.cpp
+++ b/src/assets/view/assetparameterview.cpp
@@ -1,333 +1,334 @@
/***************************************************************************
* Copyright (C) 2017 by Nicolas Carion *
* This file is part of Kdenlive. See www.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) version 3 or any later version accepted by the *
* membership of KDE e.V. (or its successor approved by the membership *
* of KDE e.V.), which shall act as a proxy defined in Section 14 of *
* version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "assetparameterview.hpp"
#include "assets/model/assetcommand.hpp"
#include "assets/model/assetparametermodel.hpp"
#include "assets/view/widgets/abstractparamwidget.hpp"
#include "assets/view/widgets/keyframewidget.hpp"
#include "core.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
AssetParameterView::AssetParameterView(QWidget *parent)
: QWidget(parent)
, m_mainKeyframeWidget(nullptr)
{
m_lay = new QVBoxLayout(this);
m_lay->setContentsMargins(0, 0, 0, 2);
m_lay->setSpacing(0);
setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
// Presets Combo
m_presetMenu = new QMenu(this);
m_presetMenu->setToolTip(i18n("Presets"));
}
void AssetParameterView::setModel(const std::shared_ptr &model, QSize frameSize, bool addSpacer)
{
unsetModel();
QMutexLocker lock(&m_lock);
m_model = model;
const QString paramTag = model->getAssetId();
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(paramTag));
connect(this, &AssetParameterView::updatePresets, [this, presetFile](const QString &presetName) {
m_presetMenu->clear();
m_presetGroup.reset(new QActionGroup(this));
m_presetGroup->setExclusive(true);
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reset Effect"), this, SLOT(resetValues()));
// Save preset
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Save preset"), this, SLOT(slotSavePreset()));
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as-template")), i18n("Update current preset"), this, SLOT(slotUpdatePreset()));
m_presetMenu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete preset"), this, SLOT(slotDeletePreset()));
m_presetMenu->addSeparator();
QStringList presets = m_model->getPresetList(presetFile);
for (const QString &pName : presets) {
QAction *ac = m_presetMenu->addAction(pName, this, SLOT(slotLoadPreset()));
m_presetGroup->addAction(ac);
ac->setData(pName);
ac->setCheckable(true);
if (pName == presetName) {
ac->setChecked(true);
}
}
});
emit updatePresets();
connect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh);
if (paramTag.endsWith(QStringLiteral("lift_gamma_gain"))) {
// Special case, the colorwheel widget manages several parameters
QModelIndex index = model->index(0, 0);
auto w = AbstractParamWidget::construct(model, index, frameSize, this);
connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges);
m_lay->addWidget(w);
m_widgets.push_back(w);
} else {
for (int i = 0; i < model->rowCount(); ++i) {
QModelIndex index = model->index(i, 0);
auto type = model->data(index, AssetParameterModel::TypeRole).value();
if (m_mainKeyframeWidget &&
(type == ParamType::Geometry || type == ParamType::Animated || type == ParamType::RestrictedAnim || type == ParamType::KeyframeParam)) {
// Keyframe widget can have some extra params that shouldn't build a new widget
qDebug() << "// FOUND ADDED PARAM";
m_mainKeyframeWidget->addParameter(index);
} else {
auto w = AbstractParamWidget::construct(model, index, frameSize, this);
connect(this, &AssetParameterView::initKeyframeView, w, &AbstractParamWidget::slotInitMonitor);
if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect || type == ParamType::Roto_spline) {
m_mainKeyframeWidget = static_cast(w);
}
connect(w, &AbstractParamWidget::valueChanged, this, &AssetParameterView::commitChanges);
connect(w, &AbstractParamWidget::seekToPos, this, &AssetParameterView::seekToPos);
m_lay->addWidget(w);
m_widgets.push_back(w);
}
}
}
if (addSpacer) {
m_lay->addStretch();
}
}
QVector> AssetParameterView::getDefaultValues() const
{
+ QLocale locale;
QVector> values;
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex index = m_model->index(i, 0);
QString name = m_model->data(index, AssetParameterModel::NameRole).toString();
ParamType type = m_model->data(index, AssetParameterModel::TypeRole).value();
QVariant defaultValue = m_model->data(index, AssetParameterModel::DefaultRole);
if (type == ParamType::KeyframeParam || type == ParamType::AnimatedRect) {
- QString val = defaultValue.toString();
+ QString val = type == ParamType::KeyframeParam ? locale.toString(defaultValue.toDouble()) : defaultValue.toString();
if (!val.contains(QLatin1Char('='))) {
val.prepend(QStringLiteral("%1=").arg(m_model->data(index, AssetParameterModel::ParentInRole).toInt()));
defaultValue = QVariant(val);
}
}
values.append({name, defaultValue});
}
return values;
}
void AssetParameterView::resetValues()
{
const QVector> values = getDefaultValues();
AssetUpdateCommand *command = new AssetUpdateCommand(m_model, values);
pCore->pushUndo(command);
/*if (m_mainKeyframeWidget) {
m_mainKeyframeWidget->resetKeyframes();
}*/
}
void AssetParameterView::commitChanges(const QModelIndex &index, const QString &value, bool storeUndo)
{
// Warning: please note that some widgets (for example keyframes) do NOT send the valueChanged signal and do modifications on their own
AssetCommand *command = new AssetCommand(m_model, index, value);
if (storeUndo) {
pCore->pushUndo(command);
} else {
command->redo();
delete command;
}
}
void AssetParameterView::unsetModel()
{
QMutexLocker lock(&m_lock);
if (m_model) {
// if a model is already there, we have to disconnect signals first
disconnect(m_model.get(), &AssetParameterModel::dataChanged, this, &AssetParameterView::refresh);
}
m_mainKeyframeWidget = nullptr;
// clear layout
m_widgets.clear();
QLayoutItem *child;
while ((child = m_lay->takeAt(0)) != nullptr) {
if (child->layout()) {
QLayoutItem *subchild;
while ((subchild = child->layout()->takeAt(0)) != nullptr) {
delete subchild->widget();
delete subchild->spacerItem();
}
}
delete child->widget();
delete child->spacerItem();
}
// Release ownership of smart pointer
m_model.reset();
}
void AssetParameterView::refresh(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles)
{
QMutexLocker lock(&m_lock);
if (m_widgets.size() == 0) {
// no visible param for this asset, abort
return;
}
Q_UNUSED(roles);
// We are expecting indexes that are children of the root index, which is "invalid"
Q_ASSERT(!topLeft.parent().isValid());
// We make sure the range is valid
if (m_mainKeyframeWidget) {
m_mainKeyframeWidget->slotRefresh();
} else {
auto type = m_model->data(m_model->index(topLeft.row(), 0), AssetParameterModel::TypeRole).value();
if (type == ParamType::ColorWheel) {
// Some special widgets, like colorwheel handle multiple params so we can have cases where param index row is greater than the number of widgets.
// Should be better managed
m_widgets[0]->slotRefresh();
return;
}
Q_ASSERT(bottomRight.row() < (int)m_widgets.size());
for (size_t i = (size_t)topLeft.row(); i <= (size_t)bottomRight.row(); ++i) {
if (m_widgets.size() > i) {
m_widgets[i]->slotRefresh();
}
}
}
}
int AssetParameterView::contentHeight() const
{
return m_lay->minimumSize().height();
}
MonitorSceneType AssetParameterView::needsMonitorEffectScene() const
{
if (m_mainKeyframeWidget) {
return m_mainKeyframeWidget->requiredScene();
}
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex index = m_model->index(i, 0);
auto type = m_model->data(index, AssetParameterModel::TypeRole).value();
if (type == ParamType::Geometry) {
return MonitorSceneGeometry;
}
}
return MonitorSceneDefault;
}
/*void AssetParameterView::initKeyframeView()
{
if (m_mainKeyframeWidget) {
m_mainKeyframeWidget->initMonitor();
} else {
for (int i = 0; i < m_model->rowCount(); ++i) {
QModelIndex index = m_model->index(i, 0);
auto type = m_model->data(index, AssetParameterModel::TypeRole).value();
if (type == ParamType::Geometry) {
return MonitorSceneGeometry;
}
}
}
}*/
void AssetParameterView::slotRefresh()
{
refresh(m_model->index(0, 0), m_model->index(m_model->rowCount() - 1, 0), {});
}
bool AssetParameterView::keyframesAllowed() const
{
return m_mainKeyframeWidget != nullptr;
}
bool AssetParameterView::modelHideKeyframes() const
{
return m_mainKeyframeWidget != nullptr && !m_mainKeyframeWidget->keyframesVisible();
}
void AssetParameterView::toggleKeyframes(bool enable)
{
if (m_mainKeyframeWidget) {
m_mainKeyframeWidget->showKeyframes(enable);
}
}
void AssetParameterView::slotDeletePreset()
{
QAction *ac = m_presetGroup->checkedAction();
if (!ac) {
return;
}
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
if (!dir.exists()) {
dir.mkpath(QStringLiteral("."));
}
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
m_model->deletePreset(presetFile, ac->data().toString());
emit updatePresets();
}
void AssetParameterView::slotUpdatePreset()
{
QAction *ac = m_presetGroup->checkedAction();
if (!ac) {
return;
}
slotSavePreset(ac->data().toString());
}
void AssetParameterView::slotSavePreset(QString presetName)
{
if (presetName.isEmpty()) {
bool ok;
presetName = QInputDialog::getText(this, i18n("Enter preset name"), i18n("Enter the name of this preset"), QLineEdit::Normal, QString(), &ok);
if (!ok) return;
}
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
if (!dir.exists()) {
dir.mkpath(QStringLiteral("."));
}
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
m_model->savePreset(presetFile, presetName);
emit updatePresets(presetName);
}
void AssetParameterView::slotLoadPreset()
{
QAction *action = qobject_cast(sender());
if (!action) {
return;
}
const QString presetName = action->data().toString();
QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/effects/presets/"));
const QString presetFile = dir.absoluteFilePath(QString("%1.json").arg(m_model->getAssetId()));
const QVector> params = m_model->loadPreset(presetFile, presetName);
AssetUpdateCommand *command = new AssetUpdateCommand(m_model, params);
pCore->pushUndo(command);
}
QMenu *AssetParameterView::presetMenu()
{
return m_presetMenu;
}
diff --git a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp
index 881c87a0d..8300f9f77 100644
--- a/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp
+++ b/src/assets/view/widgets/curves/cubic/kis_cubic_curve.cpp
@@ -1,450 +1,446 @@
/*
* Copyright (c) 2005 Casper Boemann
* Copyright (c) 2009 Dmitry Kazakov
* Copyright (c) 2010 Cyrille Berger
*
* 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 "kis_cubic_curve.h"
#include
-#include
#include
#include
#include
template class KisTridiagonalSystem
{
/*
* e.g.
* |b0 c0 0 0 0| |x0| |f0|
* |a0 b1 c1 0 0| |x1| |f1|
* |0 a1 b2 c2 0|*|x2|=|f2|
* |0 0 a2 b3 c3| |x3| |f3|
* |0 0 0 a3 b4| |x4| |f4|
*/
public:
/**
* @return - vector that is storing x[]
*/
static QVector calculate(QList &a, QList &b, QList &c, QList &f)
{
QVector x;
QVector alpha;
QVector beta;
int i;
int size = b.size();
Q_ASSERT(a.size() == size - 1 && c.size() == size - 1 && f.size() == size);
x.resize(size);
/**
* Check for special case when
* order of the matrix is equal to 1
*/
if (size == 1) {
x[0] = f[0] / b[0];
return x;
}
/**
* Common case
*/
alpha.resize(size);
beta.resize(size);
alpha[1] = -c[0] / b[0];
beta[1] = f[0] / b[0];
for (i = 1; i < size - 1; ++i) {
alpha[i + 1] = -c[i] / (a[i - 1] * alpha[i] + b[i]);
beta[i + 1] = (f[i] - a[i - 1] * beta[i]) / (a[i - 1] * alpha[i] + b[i]);
}
x.last() = (f.last() - a.last() * beta.last()) / (b.last() + a.last() * alpha.last());
for (i = size - 2; i >= 0; --i) {
x[i] = alpha[i + 1] * x[i + 1] + beta[i + 1];
}
return x;
}
};
template class KisCubicSpline
{
/**
* s[i](x)=a[i] +
* b[i] * (x-x[i]) +
* 1/2 * c[i] * (x-x[i])^2 +
* 1/6 * d[i] * (x-x[i])^3
*
* h[i]=x[i+1]-x[i]
*
*/
protected:
QList m_a;
QVector m_b;
QVector m_c;
QVector m_d;
QVector m_h;
T m_begin;
T m_end;
int m_intervals;
public:
KisCubicSpline()
: m_begin(0)
, m_end(0)
, m_intervals(0)
{
}
explicit KisCubicSpline(const QList &a)
: m_begin(0)
, m_end(0)
, m_intervals(0)
{
createSpline(a);
}
/**
* Create new spline and precalculate some values
* for future
*
* @a - base points of the spline
*/
void createSpline(const QList &a)
{
int intervals = m_intervals = a.size() - 1;
int i;
m_begin = a.constFirst().x();
m_end = a.last().x();
m_a.clear();
m_b.resize(intervals);
m_c.clear();
m_d.resize(intervals);
m_h.resize(intervals);
for (i = 0; i < intervals; ++i) {
m_h[i] = a[i + 1].x() - a[i].x();
m_a.append(a[i].y());
}
m_a.append(a.last().y());
QList tri_b;
QList tri_f;
QList tri_a; /* equals to @tri_c */
for (i = 0; i < intervals - 1; ++i) {
tri_b.append(2. * (m_h[i] + m_h[i + 1]));
tri_f.append(6. * ((m_a[i + 2] - m_a[i + 1]) / m_h[i + 1] - (m_a[i + 1] - m_a[i]) / m_h[i]));
}
for (i = 1; i < intervals - 1; ++i) {
tri_a.append(m_h[i]);
}
if (intervals > 1) {
KisTridiagonalSystem tridia;
m_c = tridia.calculate(tri_a, tri_b, tri_a, tri_f);
}
m_c.prepend(0);
m_c.append(0);
for (i = 0; i < intervals; ++i) {
m_d[i] = (m_c[i + 1] - m_c[i]) / m_h[i];
}
for (i = 0; i < intervals; ++i) {
m_b[i] = -0.5 * (m_c[i] * m_h[i]) - (1 / 6.0) * (m_d[i] * m_h[i] * m_h[i]) + (m_a[i + 1] - m_a[i]) / m_h[i];
}
}
/**
* Get value of precalculated spline in the point @x
*/
T getValue(T x) const
{
T x0;
int i = findRegion(x, x0);
/* TODO: check for asm equivalent */
return m_a[i] + m_b[i] * (x - x0) + 0.5 * m_c[i] * (x - x0) * (x - x0) + (1 / 6.0) * m_d[i] * (x - x0) * (x - x0) * (x - x0);
}
T begin() const { return m_begin; }
T end() const { return m_end; }
protected:
/**
* findRegion - Searches for the region containing @x
* @x0 - out parameter, containing beginning of the region
* @return - index of the region
*/
int findRegion(T x, T &x0) const
{
int i;
x0 = m_begin;
for (i = 0; i < m_intervals; ++i) {
if (x >= x0 && x < x0 + m_h[i]) {
return i;
}
x0 += m_h[i];
}
if (x >= x0) {
x0 -= m_h[m_intervals - 1];
return m_intervals - 1;
}
qDebug("X value: %f\n", x);
qDebug("m_begin: %f\n", m_begin);
qDebug("m_end : %f\n", m_end);
Q_ASSERT_X(0, "findRegion", "X value is outside regions");
/* **never reached** */
return -1;
}
};
static bool pointLessThan(const QPointF &a, const QPointF &b)
{
return a.x() < b.x();
}
struct KisCubicCurve::Data : public QSharedData
{
Data() { init(); }
Data(const Data &data)
: QSharedData()
{
init();
points = data.points;
}
void init()
{
validSpline = false;
validU16Transfer = false;
validFTransfer = false;
}
~Data() = default;
mutable KisCubicSpline spline;
QList points;
mutable bool validSpline;
mutable QVector u16Transfer;
mutable bool validU16Transfer;
mutable QVector fTransfer;
mutable bool validFTransfer;
void updateSpline();
void keepSorted();
qreal value(qreal x);
void invalidate();
template void updateTransfer(QVector<_T_> *transfer, bool &valid, _T2_ min, _T2_ max, int size);
};
void KisCubicCurve::Data::updateSpline()
{
if (validSpline) {
return;
}
validSpline = true;
spline.createSpline(points);
}
void KisCubicCurve::Data::invalidate()
{
validSpline = false;
validFTransfer = false;
validU16Transfer = false;
}
void KisCubicCurve::Data::keepSorted()
{
qSort(points.begin(), points.end(), pointLessThan);
}
qreal KisCubicCurve::Data::value(qreal x)
{
updateSpline();
/* Automatically extend non-existing parts of the curve
* (e.g. before the first point) and cut off big y-values
*/
x = qBound(spline.begin(), x, spline.end());
qreal y = spline.getValue(x);
return qBound((qreal)0.0, y, (qreal)1.0);
}
template void KisCubicCurve::Data::updateTransfer(QVector<_T_> *transfer, bool &valid, _T2_ min, _T2_ max, int size)
{
if (!valid || transfer->size() != size) {
if (transfer->size() != size) {
transfer->resize(size);
}
qreal end = 1.0 / (size - 1);
for (int i = 0; i < size; ++i) {
/* Direct uncached version */
_T2_ val = value(i * end) * max;
val = qBound(min, val, max);
(*transfer)[i] = val;
}
valid = true;
}
}
struct KisCubicCurve::Private
{
QSharedDataPointer data;
};
KisCubicCurve::KisCubicCurve()
: d(new Private)
{
d->data = new Data;
QPointF p;
p.rx() = 0.0;
p.ry() = 0.0;
d->data->points.append(p);
p.rx() = 1.0;
p.ry() = 1.0;
d->data->points.append(p);
}
KisCubicCurve::KisCubicCurve(const QList &points)
: d(new Private)
{
d->data = new Data;
d->data->points = points;
d->data->keepSorted();
}
KisCubicCurve::KisCubicCurve(const KisCubicCurve &curve)
: d(new Private(*curve.d))
{
}
KisCubicCurve::~KisCubicCurve()
{
delete d;
}
KisCubicCurve &KisCubicCurve::operator=(const KisCubicCurve &curve)
{
*d = *curve.d;
return *this;
}
bool KisCubicCurve::operator==(const KisCubicCurve &curve) const
{
if (d->data == curve.d->data) {
return true;
}
return d->data->points == curve.d->data->points;
}
qreal KisCubicCurve::value(qreal x) const
{
return d->data->value(x);
}
QList KisCubicCurve::points() const
{
return d->data->points;
}
void KisCubicCurve::setPoints(const QList &points)
{
d->data.detach();
d->data->points = points;
d->data->invalidate();
}
int KisCubicCurve::setPoint(int idx, const QPointF &point)
{
d->data.detach();
d->data->points[idx] = point;
d->data->keepSorted();
d->data->invalidate();
return idx;
}
int KisCubicCurve::addPoint(const QPointF &point)
{
d->data.detach();
d->data->points.append(point);
d->data->keepSorted();
d->data->invalidate();
return d->data->points.indexOf(point);
}
void KisCubicCurve::removePoint(int idx)
{
d->data.detach();
d->data->points.removeAt(idx);
d->data->invalidate();
}
const QString KisCubicCurve::toString() const
{
QString sCurve;
- QLocale locale;
- locale.setNumberOptions(QLocale::OmitGroupSeparator);
for (const QPointF &pair : d->data->points) {
- sCurve += locale.toString(pair.x());
+ sCurve += QString::number(pair.x());
sCurve += QStringLiteral("/");
- sCurve += locale.toString(pair.y());
+ sCurve += QString::number(pair.y());
sCurve += QStringLiteral(";");
}
return sCurve;
}
void KisCubicCurve::fromString(const QString &string)
{
const QStringList data = string.split(QLatin1Char(';'));
QList points;
- QLocale locale;
for (const QString &pair : data) {
if (pair.indexOf('/') > -1) {
QPointF p;
- p.rx() = locale.toDouble(pair.section(QLatin1Char('/'), 0, 0));
- p.ry() = locale.toDouble(pair.section(QLatin1Char('/'), 1, 1));
+ p.rx() = pair.section(QLatin1Char('/'), 0, 0).toDouble();
+ p.ry() = pair.section(QLatin1Char('/'), 1, 1).toDouble();
points.append(p);
}
}
setPoints(points);
}
int KisCubicCurve::count() const
{
return d->data->points.size();
}
QPointF KisCubicCurve::getPoint(int ix, int normalisedWidth, int normalisedHeight, bool invertHeight)
{
QPointF p = d->data->points.at(ix);
p.rx() *= normalisedWidth;
p.ry() *= normalisedHeight;
if (invertHeight) {
p.ry() = normalisedHeight - p.y();
}
return p;
}
diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp
index d60d2ba8e..53848713c 100644
--- a/src/monitor/monitor.cpp
+++ b/src/monitor/monitor.cpp
@@ -1,2141 +1,2142 @@
/***************************************************************************
* 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 "monitor.h"
#include "bin/bin.h"
#include "bin/projectclip.h"
#include "core.h"
#include "dialogs/profilesdialog.h"
#include "doc/kdenlivedoc.h"
#include "doc/kthumb.h"
#include "glwidget.h"
#include "kdenlivesettings.h"
#include "lib/audio/audioStreamInfo.h"
#include "mainwindow.h"
#include "mltcontroller/clipcontroller.h"
#include "monitorproxy.h"
#include "project/projectmanager.h"
#include "qmlmanager.h"
#include "recmanager.h"
#include "scopes/monitoraudiolevel.h"
#include "timeline2/model/snapmodel.hpp"
#include "transitions/transitionsrepository.hpp"
#include "klocalizedstring.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include "kdenlive_debug.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SEEK_INACTIVE (-1)
QuickEventEater::QuickEventEater(QObject *parent)
: QObject(parent)
{
}
bool QuickEventEater::eventFilter(QObject *obj, QEvent *event)
{
switch (event->type()) {
case QEvent::DragEnter: {
QDragEnterEvent *ev = reinterpret_cast(event);
if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) {
ev->acceptProposedAction();
return true;
}
break;
}
case QEvent::DragMove: {
QDragEnterEvent *ev = reinterpret_cast(event);
if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) {
ev->acceptProposedAction();
return true;
}
break;
}
case QEvent::Drop: {
QDropEvent *ev = static_cast(event);
if (ev) {
QStringList effectData;
effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect")));
QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-'));
effectData << source;
emit addEffect(effectData);
ev->accept();
return true;
}
break;
}
default:
break;
}
return QObject::eventFilter(obj, event);
}
QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent)
: QObject(parent)
{
}
bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *ev = static_cast(event);
if (ev) {
emit doKeyPressEvent(ev);
return true;
}
}
return QObject::eventFilter(obj, event);
}
Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent)
: AbstractMonitor(id, manager, parent)
, m_controller(nullptr)
, m_glMonitor(nullptr)
, m_snaps(new SnapModel())
, m_splitEffect(nullptr)
, m_splitProducer(nullptr)
, m_dragStarted(false)
, m_recManager(nullptr)
, m_loopClipAction(nullptr)
, m_sceneVisibilityAction(nullptr)
, m_multitrackView(nullptr)
, m_contextMenu(nullptr)
, m_loopClipTransition(true)
, m_editMarker(nullptr)
, m_forceSizeFactor(0)
, m_lastMonitorSceneType(MonitorSceneDefault)
{
auto *layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
// Create container widget
m_glWidget = new QWidget;
auto *glayout = new QGridLayout(m_glWidget);
glayout->setSpacing(0);
glayout->setContentsMargins(0, 0, 0, 0);
// Create QML OpenGL widget
m_glMonitor = new GLWidget((int)id);
connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent);
connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView);
connect(m_glMonitor, &GLWidget::seekPosition, this, &Monitor::seekPosition, Qt::DirectConnection);
connect(m_glMonitor, &GLWidget::consumerPosition, this, &Monitor::slotSeekPosition, Qt::DirectConnection);
connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection);
m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor));
m_videoWidget->setAcceptDrops(true);
auto *leventEater = new QuickEventEater(this);
m_videoWidget->installEventFilter(leventEater);
connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect);
m_qmlManager = new QmlManager(m_glMonitor);
connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged);
connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged);
auto *monitorEventEater = new QuickMonitorEventEater(this);
m_glWidget->installEventFilter(monitorEventEater);
connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent);
glayout->addWidget(m_videoWidget, 0, 0);
m_verticalScroll = new QScrollBar(Qt::Vertical);
glayout->addWidget(m_verticalScroll, 0, 1);
m_verticalScroll->hide();
m_horizontalScroll = new QScrollBar(Qt::Horizontal);
glayout->addWidget(m_horizontalScroll, 1, 0);
m_horizontalScroll->hide();
connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX);
connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY);
connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed);
connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek);
connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay);
connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag);
connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen);
connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom);
connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection);
connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu);
connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError);
m_glWidget->setMinimumSize(QSize(320, 180));
layout->addWidget(m_glWidget, 10);
layout->addStretch();
// Tool bar buttons
m_toolbar = new QToolBar(this);
QWidget *sp1 = new QWidget(this);
sp1->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_toolbar->addWidget(sp1);
if (id == Kdenlive::ClipMonitor) {
// Add options for recording
m_recManager = new RecManager(this);
connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage);
connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject);
m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree")));
m_toolbar->setToolTip(i18n("Insert Zone to Project Bin"));
m_toolbar->addSeparator();
}
if (id != Kdenlive::DvdMonitor) {
m_toolbar->addAction(manager->getAction(QStringLiteral("mark_in")));
m_toolbar->addAction(manager->getAction(QStringLiteral("mark_out")));
}
m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_backward")));
auto *playButton = new QToolButton(m_toolbar);
m_playMenu = new QMenu(i18n("Play..."), this);
QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play")));
m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this);
m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start")));
m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause")));
QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)")));
// append shortcut if it exists for action
if (originalPlayAction->shortcut() == QKeySequence(0)) {
m_playAction->setToolTip(strippedTooltip);
} else {
m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')'));
}
m_playMenu->addAction(m_playAction);
connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay);
playButton->setMenu(m_playMenu);
playButton->setPopupMode(QToolButton::MenuButtonPopup);
m_toolbar->addWidget(playButton);
m_toolbar->addAction(manager->getAction(QStringLiteral("monitor_seek_forward")));
playButton->setDefaultAction(m_playAction);
m_configMenu = new QMenu(i18n("Misc..."), this);
if (id != Kdenlive::DvdMonitor) {
if (id == Kdenlive::ClipMonitor) {
m_markerMenu = new QMenu(i18n("Go to marker..."), this);
} else {
m_markerMenu = new QMenu(i18n("Go to guide..."), this);
}
m_markerMenu->setEnabled(false);
m_configMenu->addMenu(m_markerMenu);
connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker);
m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this);
QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%"));
fullAction->setData(100);
QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%"));
halfAction->setData(50);
QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize"));
freeAction->setData(0);
m_configMenu->addAction(m_forceSize);
m_forceSize->setCurrentAction(freeAction);
connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize);
}
// Create Volume slider popup
m_audioSlider = new QSlider(Qt::Vertical);
m_audioSlider->setRange(0, 100);
m_audioSlider->setValue(100);
connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume);
auto *widgetslider = new QWidgetAction(this);
widgetslider->setText(i18n("Audio volume"));
widgetslider->setDefaultWidget(m_audioSlider);
auto *menu = new QMenu(this);
menu->addAction(widgetslider);
m_audioButton = new QToolButton(this);
m_audioButton->setMenu(menu);
m_audioButton->setToolTip(i18n("Volume"));
m_audioButton->setPopupMode(QToolButton::InstantPopup);
QIcon icon;
if (KdenliveSettings::volume() == 0) {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted"));
} else {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
}
m_audioButton->setIcon(icon);
m_toolbar->addWidget(m_audioButton);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setLayout(layout);
setMinimumHeight(200);
connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection);
connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated);
connect(m_glMonitor, &GLWidget::audioSamplesSignal, this, &Monitor::audioSamplesSignal);
if (id != Kdenlive::ClipMonitor) {
// TODO: reimplement
// connect(render, &Render::durationChanged, this, &Monitor::durationChanged);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateTimelineClipZone);
} else {
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone);
}
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame);
m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this);
m_sceneVisibilityAction->setCheckable(true);
m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene());
connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene);
m_toolbar->addAction(m_sceneVisibilityAction);
m_toolbar->addSeparator();
m_timePos = new TimecodeDisplay(m_monitorManager->timecode(), this);
m_toolbar->addWidget(m_timePos);
auto *configButton = new QToolButton(m_toolbar);
configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
configButton->setToolTip(i18n("Options"));
configButton->setMenu(m_configMenu);
configButton->setPopupMode(QToolButton::InstantPopup);
m_toolbar->addWidget(configButton);
if (m_recManager) {
m_toolbar->addAction(m_recManager->switchAction());
}
/*QWidget *spacer = new QWidget(this);
spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_toolbar->addWidget(spacer);*/
m_toolbar->addSeparator();
int tm = 0;
int bm = 0;
m_toolbar->getContentsMargins(nullptr, &tm, nullptr, &bm);
m_audioMeterWidget = new MonitorAudioLevel(m_glMonitor->profile(), m_toolbar->height() - tm - bm, this);
m_toolbar->addWidget(m_audioMeterWidget);
if (!m_audioMeterWidget->isValid) {
KdenliveSettings::setMonitoraudio(0x01);
m_audioMeterWidget->setVisibility(false);
} else {
m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0);
}
connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek()));
layout->addWidget(m_toolbar);
if (m_recManager) {
layout->addWidget(m_recManager->toolbar());
}
// Load monitor overlay qml
loadQmlScene(MonitorSceneDefault);
// Info message widget
m_infoMessage = new KMessageWidget(this);
layout->addWidget(m_infoMessage);
m_infoMessage->hide();
}
Monitor::~Monitor()
{
delete m_splitEffect;
delete m_splitProducer;
delete m_audioMeterWidget;
delete m_glMonitor;
delete m_videoWidget;
delete m_glWidget;
delete m_timePos;
}
void Monitor::setOffsetX(int x)
{
m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum());
}
void Monitor::setOffsetY(int y)
{
m_glMonitor->setOffsetY(y, m_verticalScroll->maximum());
}
void Monitor::slotGetCurrentImage(bool request)
{
m_glMonitor->sendFrameForAnalysis = request;
m_monitorManager->activateMonitor(m_id);
refreshMonitorIfActive();
if (request) {
// Update analysis state
QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes);
} else {
m_glMonitor->releaseAnalyse();
}
}
void Monitor::slotAddEffect(const QStringList &effect)
{
if (m_id == Kdenlive::ClipMonitor) {
if (m_controller) {
emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect);
}
} else {
emit addEffect(effect);
}
}
void Monitor::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 = QIcon::fromTheme(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 = QIcon::fromTheme(ic.name());
m->setActiveIcon(newIcon);
ic = m->inactiveIcon();
if (ic.isNull() || ic.name().isEmpty()) {
continue;
}
newIcon = QIcon::fromTheme(ic.name());
m->setInactiveIcon(newIcon);
}
}
QAction *Monitor::recAction()
{
if (m_recManager) {
return m_recManager->switchAction();
}
return nullptr;
}
void Monitor::slotLockMonitor(bool lock)
{
m_monitorManager->lockMonitor(m_id, lock);
}
void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip)
{
delete m_contextMenu;
m_contextMenu = new QMenu(this);
m_contextMenu->addMenu(m_playMenu);
if (goMenu) {
m_contextMenu->addMenu(goMenu);
}
if (markerMenu) {
m_contextMenu->addMenu(markerMenu);
QList list = markerMenu->actions();
for (int i = 0; i < list.count(); ++i) {
if (list.at(i)->data().toString() == QLatin1String("edit_marker")) {
m_editMarker = list.at(i);
break;
}
}
}
m_playMenu->addAction(playZone);
m_playMenu->addAction(loopZone);
if (loopClip) {
m_loopClipAction = loopClip;
m_playMenu->addAction(loopClip);
}
// TODO: add save zone to timeline monitor when fixed
m_contextMenu->addMenu(m_markerMenu);
if (m_id == Kdenlive::ClipMonitor) {
m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone()));
QAction *extractZone =
m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone()));
m_contextMenu->addAction(extractZone);
}
m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame")));
m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project")));
if (m_id == Kdenlive::ProjectMonitor) {
m_multitrackView =
m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("view-split-left-right")), i18n("Multitrack view"), this, SIGNAL(multitrackView(bool)));
m_multitrackView->setCheckable(true);
m_configMenu->addAction(m_multitrackView);
} else if (m_id == Kdenlive::ClipMonitor) {
QAction *setThumbFrame =
m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame()));
m_configMenu->addAction(setThumbFrame);
}
if (overlayMenu) {
m_contextMenu->addMenu(overlayMenu);
}
QAction *overlayAudio = m_contextMenu->addAction(QIcon(), i18n("Overlay audio waveform"));
overlayAudio->setCheckable(true);
connect(overlayAudio, &QAction::toggled, m_glMonitor, &GLWidget::slotSwitchAudioOverlay);
overlayAudio->setChecked(KdenliveSettings::displayAudioOverlay());
QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor()));
switchAudioMonitor->setCheckable(true);
switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0);
m_configMenu->addAction(overlayAudio);
// For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden
// or it will never appear (supposed to appear on hover).
m_timePos->setFrame(false);
}
void Monitor::slotGoToMarker(QAction *action)
{
int pos = action->data().toInt();
slotSeek(pos);
}
void Monitor::slotForceSize(QAction *a)
{
int resizeType = a->data().toInt();
int profileWidth = 320;
int profileHeight = 200;
if (resizeType > 0) {
// calculate size
QRect r = QApplication::desktop()->screenGeometry();
profileHeight = m_glMonitor->profileSize().height() * resizeType / 100;
profileWidth = m_glMonitor->profile()->dar() * profileHeight;
if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) {
// reset action to free resize
const QList list = m_forceSize->actions();
for (QAction *ac : list) {
if (ac->data().toInt() == m_forceSizeFactor) {
m_forceSize->setCurrentAction(ac);
break;
}
}
warningMessage(i18n("Your screen resolution is not sufficient for this action"));
return;
}
}
switch (resizeType) {
case 100:
case 50:
// resize full size
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
m_videoWidget->setMinimumSize(profileWidth, profileHeight);
m_videoWidget->setMaximumSize(profileWidth, profileHeight);
setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight()));
break;
default:
// Free resize
m_videoWidget->setMinimumSize(profileWidth, profileHeight);
m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight()));
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
break;
}
m_forceSizeFactor = resizeType;
updateGeometry();
}
QString Monitor::getTimecodeFromFrames(int pos)
{
return m_monitorManager->timecode().getTimecodeFromFrames(pos);
}
double Monitor::fps() const
{
return m_monitorManager->timecode().fps();
}
Timecode Monitor::timecode() const
{
return m_monitorManager->timecode();
}
void Monitor::updateMarkers()
{
if (m_controller) {
m_markerMenu->clear();
QList markers = m_controller->getMarkerModel()->getAllMarkers();
if (!markers.isEmpty()) {
for (int i = 0; i < markers.count(); ++i) {
int pos = (int)markers.at(i).time().frames(m_monitorManager->timecode().fps());
QString position = m_monitorManager->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment();
QAction *go = m_markerMenu->addAction(position);
go->setData(pos);
}
}
m_markerMenu->setEnabled(!m_markerMenu->isEmpty());
}
}
void Monitor::setGuides(const QMap &guides)
{
// TODO: load guides model
m_markerMenu->clear();
QMapIterator i(guides);
QList guidesList;
while (i.hasNext()) {
i.next();
CommentedTime timeGuide(GenTime(i.key()), i.value());
guidesList << timeGuide;
int pos = (int)timeGuide.time().frames(m_monitorManager->timecode().fps());
QString position = m_monitorManager->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment();
QAction *go = m_markerMenu->addAction(position);
go->setData(pos);
}
// m_ruler->setMarkers(guidesList);
m_markerMenu->setEnabled(!m_markerMenu->isEmpty());
checkOverlay();
}
void Monitor::slotSeekToPreviousSnap()
{
if (m_controller) {
m_glMonitor->seek(getSnapForPos(true).frames(m_monitorManager->timecode().fps()));
}
}
void Monitor::slotSeekToNextSnap()
{
if (m_controller) {
m_glMonitor->seek(getSnapForPos(false).frames(m_monitorManager->timecode().fps()));
}
}
int Monitor::position()
{
return m_glMonitor->getCurrentPos();
}
GenTime Monitor::getSnapForPos(bool previous)
{
int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos());
return GenTime(frame, pCore->getCurrentFps());
}
void Monitor::slotLoadClipZone(const QPoint &zone)
{
m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y());
checkOverlay();
}
void Monitor::slotSetZoneStart()
{
m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos());
if (m_controller) {
m_controller->setZone(m_glMonitor->getControllerProxy()->zone());
} else {
// timeline
emit timelineZoneChanged();
}
checkOverlay();
}
void Monitor::slotSetZoneEnd(bool discardLastFrame)
{
Q_UNUSED(discardLastFrame);
int pos = m_glMonitor->getCurrentPos();
if (m_controller) {
if (pos < (int)m_controller->frameDuration() - 1) {
pos++;
}
} else
pos++;
m_glMonitor->getControllerProxy()->setZoneOut(pos);
if (m_controller) {
m_controller->setZone(m_glMonitor->getControllerProxy()->zone());
}
checkOverlay();
}
// virtual
void Monitor::mousePressEvent(QMouseEvent *event)
{
m_monitorManager->activateMonitor(m_id);
if ((event->button() & Qt::RightButton) == 0u) {
if (m_glWidget->geometry().contains(event->pos())) {
m_DragStartPosition = event->pos();
event->accept();
}
} else if (m_contextMenu) {
slotActivateMonitor();
m_contextMenu->popup(event->globalPos());
event->accept();
}
QWidget::mousePressEvent(event);
}
void Monitor::slotShowMenu(const QPoint pos)
{
slotActivateMonitor();
if (m_contextMenu) {
m_contextMenu->popup(pos);
}
}
void Monitor::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event)
if (m_glMonitor->zoom() > 0.0f) {
float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum());
float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum());
adjustScrollBars(horizontal, vertical);
} else {
m_horizontalScroll->hide();
m_verticalScroll->hide();
}
}
void Monitor::adjustScrollBars(float horizontal, float vertical)
{
if (m_glMonitor->zoom() > 1.0f) {
m_horizontalScroll->setPageStep(m_glWidget->width());
m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep());
m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum())));
emit m_horizontalScroll->valueChanged(m_horizontalScroll->value());
m_horizontalScroll->show();
} else {
int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width();
emit m_horizontalScroll->valueChanged(qRound(0.5 * max));
m_horizontalScroll->hide();
}
if (m_glMonitor->zoom() > 1.0f) {
m_verticalScroll->setPageStep(m_glWidget->height());
m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep());
m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical));
emit m_verticalScroll->valueChanged(m_verticalScroll->value());
m_verticalScroll->show();
} else {
int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height();
emit m_verticalScroll->valueChanged(qRound(0.5 * max));
m_verticalScroll->hide();
}
}
void Monitor::setZoom()
{
if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) {
m_horizontalScroll->hide();
m_verticalScroll->hide();
m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum());
m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum());
} else {
adjustScrollBars(0.5f, 0.5f);
}
}
void Monitor::slotSwitchFullScreen(bool minimizeOnly)
{
// TODO: disable screensaver?
if (!m_glWidget->isFullScreen() && !minimizeOnly) {
// Check if we have a multiple monitor setup
int monitors = QApplication::desktop()->screenCount();
int screen = -1;
if (monitors > 1) {
QRect screenres;
// Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget
// int currentScreen = QApplication::desktop()->screenNumber(this);
for (int i = 0; screen == -1 && i < QApplication::desktop()->screenCount(); i++) {
if (i != QApplication::desktop()->screenNumber(this->parentWidget()->parentWidget())) {
screen = i;
}
}
}
m_qmlManager->enableAudioThumbs(false);
m_glWidget->setParent(QApplication::desktop()->screen(screen));
m_glWidget->move(QApplication::desktop()->screenGeometry(screen).bottomLeft());
m_glWidget->showFullScreen();
} else {
m_glWidget->showNormal();
m_qmlManager->enableAudioThumbs(true);
QVBoxLayout *lay = (QVBoxLayout *)layout();
lay->insertWidget(0, m_glWidget, 10);
}
}
void Monitor::reparent()
{
m_glWidget->setParent(nullptr);
m_glWidget->showMinimized();
m_glWidget->showNormal();
QVBoxLayout *lay = (QVBoxLayout *)layout();
lay->insertWidget(0, m_glWidget, 10);
}
// virtual
void Monitor::mouseReleaseEvent(QMouseEvent *event)
{
if (m_dragStarted) {
event->ignore();
return;
}
if (event->button() != Qt::RightButton) {
if (m_glMonitor->geometry().contains(event->pos())) {
if (isActive()) {
slotPlay();
} else {
slotActivateMonitor();
}
} // else event->ignore(); //QWidget::mouseReleaseEvent(event);
}
m_dragStarted = false;
event->accept();
QWidget::mouseReleaseEvent(event);
}
void Monitor::slotStartDrag()
{
if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) {
// dragging is only allowed for clip monitor
return;
}
auto *drag = new QDrag(this);
auto *mimeData = new QMimeData;
// Get drag state
QQuickItem *root = m_glMonitor->rootObject();
int dragType = 0;
if (root) {
dragType = root->property("dragType").toInt();
root->setProperty("dragType", 0);
}
QByteArray prodData;
QPoint p = m_glMonitor->getControllerProxy()->zone();
if (p.x() == -1 || p.y() == -1) {
prodData = m_controller->AbstractProjectItem::clipId().toUtf8();
} else {
QStringList list;
list.append(m_controller->AbstractProjectItem::clipId());
list.append(QString::number(p.x()));
list.append(QString::number(p.y() - 1));
prodData.append(list.join(QLatin1Char('/')).toUtf8());
}
switch (dragType) {
case 1:
// Audio only drag
prodData.prepend('A');
break;
case 2:
// Audio only drag
prodData.prepend('V');
break;
default:
break;
}
mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData);
drag->setMimeData(mimeData);
/*QPixmap pix = m_currentClip->thumbnail();
drag->setPixmap(pix);
drag->setHotSpot(QPoint(0, 50));*/
drag->start(Qt::MoveAction);
}
void Monitor::enterEvent(QEvent *event)
{
m_qmlManager->enableAudioThumbs(true);
QWidget::enterEvent(event);
}
void Monitor::leaveEvent(QEvent *event)
{
m_qmlManager->enableAudioThumbs(false);
QWidget::leaveEvent(event);
}
// virtual
void Monitor::mouseMoveEvent(QMouseEvent *event)
{
if (m_dragStarted || m_controller == nullptr) {
return;
}
if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) {
return;
}
{
auto *drag = new QDrag(this);
auto *mimeData = new QMimeData;
m_dragStarted = true;
QStringList list;
list.append(m_controller->AbstractProjectItem::clipId());
QPoint p = m_glMonitor->getControllerProxy()->zone();
list.append(QString::number(p.x()));
list.append(QString::number(p.y()));
QByteArray clipData;
clipData.append(list.join(QLatin1Char(';')).toUtf8());
mimeData->setData(QStringLiteral("kdenlive/clip"), clipData);
drag->setMimeData(mimeData);
drag->start(Qt::MoveAction);
}
event->accept();
}
/*void Monitor::dragMoveEvent(QDragMoveEvent * event) {
event->setDropAction(Qt::IgnoreAction);
event->setDropAction(Qt::MoveAction);
if (event->mimeData()->hasText()) {
event->acceptProposedAction();
}
}
Qt::DropActions Monitor::supportedDropActions() const {
// returns what actions are supported when dropping
return Qt::MoveAction;
}*/
QStringList Monitor::mimeTypes() const
{
QStringList qstrList;
// list of accepted MIME types for drop
qstrList.append(QStringLiteral("kdenlive/clip"));
return qstrList;
}
// virtual
void Monitor::wheelEvent(QWheelEvent *event)
{
slotMouseSeek(event->delta(), event->modifiers());
event->accept();
}
void Monitor::mouseDoubleClickEvent(QMouseEvent *event)
{
slotSwitchFullScreen();
event->accept();
}
void Monitor::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Escape) {
slotSwitchFullScreen();
event->accept();
return;
}
if (m_glWidget->isFullScreen()) {
event->ignore();
emit passKeyPress(event);
return;
}
QWidget::keyPressEvent(event);
}
void Monitor::slotMouseSeek(int eventDelta, uint modifiers)
{
if ((modifiers & Qt::ControlModifier) != 0u) {
int delta = m_monitorManager->timecode().fps();
if (eventDelta > 0) {
delta = 0 - delta;
}
m_glMonitor->seek(m_glMonitor->getCurrentPos() - delta);
} else if ((modifiers & Qt::AltModifier) != 0u) {
if (eventDelta >= 0) {
emit seekToPreviousSnap();
} else {
emit seekToNextSnap();
}
} else {
if (eventDelta >= 0) {
slotRewindOneFrame();
} else {
slotForwardOneFrame();
}
}
}
void Monitor::slotSetThumbFrame()
{
if (m_controller == nullptr) {
return;
}
m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos());
emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId());
}
void Monitor::slotExtractCurrentZone()
{
if (m_controller == nullptr) {
return;
}
emit extractZone(m_controller->AbstractProjectItem::clipId());
}
std::shared_ptr Monitor::currentController() const
{
return m_controller;
}
void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject)
{
if (QFileInfo(frameName).fileName().isEmpty()) {
// convenience: when extracting an image to be added to the project,
// suggest a suitable image file name. In the project monitor, this
// suggestion bases on the project file name; in the clip monitor,
// the suggestion bases on the clip file name currently shown.
// Finally, the frame number is added to this suggestion, prefixed
// with "-f", so we get something like clip-f#.png.
QString suggestedImageName =
QFileInfo(currentController() ? currentController()->clipName()
: pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled"))
.completeBaseName() +
QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png");
frameName = QFileInfo(frameName, suggestedImageName).fileName();
}
QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder"));
if (framesFolder.isEmpty()) {
framesFolder = QDir::homePath();
}
QScopedPointer dlg(new QDialog(this));
QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data()));
dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project"));
auto *layout = new QVBoxLayout;
layout->addWidget(fileWidget.data());
QCheckBox *b = nullptr;
if (m_id == Kdenlive::ClipMonitor) {
b = new QCheckBox(i18n("Export image using source resolution"), dlg.data());
b->setChecked(KdenliveSettings::exportframe_usingsourceres());
fileWidget->setCustomWidget(b);
}
fileWidget->setConfirmOverwrite(true);
fileWidget->okButton()->show();
fileWidget->cancelButton()->show();
QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk);
QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept);
QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept);
QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject);
dlg->setLayout(layout);
fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png"));
fileWidget->setMode(KFile::File | KFile::LocalOnly);
fileWidget->setOperationMode(KFileWidget::Saving);
QUrl relativeUrl;
relativeUrl.setPath(frameName);
#if KIO_VERSION >= QT_VERSION_CHECK(5, 33, 0)
fileWidget->setSelectedUrl(relativeUrl);
#else
fileWidget->setSelection(relativeUrl.toString());
#endif
KSharedConfig::Ptr conf = KSharedConfig::openConfig();
QWindow *handle = dlg->windowHandle();
if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) {
KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize"));
dlg->resize(handle->size());
}
if (dlg->exec() == QDialog::Accepted) {
QString selectedFile = fileWidget->selectedFile();
if (!selectedFile.isEmpty()) {
// Create Qimage with frame
QImage frame;
// check if we are using a proxy
if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() &&
m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) {
// using proxy, use original clip url to get frame
frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(),
m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1,
b != nullptr ? b->isChecked() : false);
} else {
frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false);
}
frame.save(selectedFile);
if (b != nullptr) {
KdenliveSettings::setExportframe_usingsourceres(b->isChecked());
}
KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile());
if (addToProject) {
QStringList folderInfo = pCore->bin()->getFolderInfo();
pCore->bin()->droppedUrls(QList() << QUrl::fromLocalFile(selectedFile), folderInfo);
}
}
}
}
void Monitor::setTimePos(const QString &pos)
{
m_timePos->setValue(pos);
slotSeek();
}
void Monitor::slotSeek()
{
slotSeek(m_timePos->getValue());
}
void Monitor::slotSeek(int pos)
{
slotActivateMonitor();
m_glMonitor->seek(pos);
}
void Monitor::checkOverlay(int pos)
{
if (m_qmlManager->sceneType() != MonitorSceneDefault) {
// we are not in main view, ignore
return;
}
QString overlayText;
if (pos == -1) {
pos = m_timePos->getValue();
}
QPoint zone = m_glMonitor->getControllerProxy()->zone();
std::shared_ptr model;
if (m_id == Kdenlive::ClipMonitor && m_controller) {
model = m_controller->getMarkerModel();
} else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) {
model = pCore->currentDoc()->getGuideModel();
}
if (model) {
bool found = false;
CommentedTime marker = model->getMarker(GenTime(pos, m_monitorManager->timecode().fps()), &found);
if (!found) {
if (pos == zone.x()) {
overlayText = i18n("In Point");
} else if (pos == zone.y() - 1) {
overlayText = i18n("Out Point");
}
} else {
overlayText = marker.comment();
}
}
m_glMonitor->getControllerProxy()->setMarkerComment(overlayText);
}
int Monitor::getZoneStart()
{
return m_glMonitor->getControllerProxy()->zoneIn();
}
int Monitor::getZoneEnd()
{
return m_glMonitor->getControllerProxy()->zoneOut();
}
void Monitor::slotZoneStart()
{
slotActivateMonitor();
m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneIn());
}
void Monitor::slotZoneEnd()
{
slotActivateMonitor();
m_glMonitor->getControllerProxy()->pauseAndSeek(m_glMonitor->getControllerProxy()->zoneOut() - 1);
}
void Monitor::slotRewind(double speed)
{
slotActivateMonitor();
if (qFuzzyIsNull(speed)) {
double currentspeed = m_glMonitor->playSpeed();
if (currentspeed > -1) {
speed = -1;
} else {
speed = currentspeed * 1.5;
}
}
m_glMonitor->switchPlay(true, speed);
m_playAction->setActive(true);
}
void Monitor::slotForward(double speed)
{
slotActivateMonitor();
if (qFuzzyIsNull(speed)) {
double currentspeed = m_glMonitor->playSpeed();
if (currentspeed < 1) {
speed = 1;
} else {
speed = currentspeed * 1.2;
}
}
m_glMonitor->switchPlay(true, speed);
m_playAction->setActive(true);
}
void Monitor::slotRewindOneFrame(int diff)
{
slotActivateMonitor();
m_glMonitor->seek(m_glMonitor->getCurrentPos() - diff);
}
void Monitor::slotForwardOneFrame(int diff)
{
slotActivateMonitor();
m_glMonitor->seek(m_glMonitor->getCurrentPos() + diff);
}
void Monitor::seekCursor(int pos)
{
Q_UNUSED(pos)
// Deprecated should not be used, instead requestSeek
/*if (m_ruler->slotNewValue(pos)) {
m_timePos->setValue(pos);
checkOverlay(pos);
if (m_id != Kdenlive::ClipMonitor) {
emit renderPosition(pos);
}
}*/
}
void Monitor::adjustRulerSize(int length, std::shared_ptr markerModel)
{
if (m_controller != nullptr) {
m_glMonitor->setRulerInfo(length);
} else {
m_glMonitor->setRulerInfo(length, markerModel);
}
m_timePos->setRange(0, length);
if (markerModel) {
connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay()));
connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
}
}
void Monitor::stop()
{
m_playAction->setActive(false);
m_glMonitor->stop();
}
void Monitor::mute(bool mute, bool updateIconOnly)
{
// TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume
QIcon icon;
if (mute || KdenliveSettings::volume() == 0) {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted"));
} else {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
}
m_audioButton->setIcon(icon);
if (!updateIconOnly) {
m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0);
}
}
void Monitor::start()
{
if (!isVisible() || !isActive()) {
return;
}
m_glMonitor->startConsumer();
}
void Monitor::slotRefreshMonitor(bool visible)
{
if (visible) {
if (slotActivateMonitor()) {
start();
}
}
}
void Monitor::refreshMonitorIfActive(bool directUpdate)
{
if (isActive()) {
if (directUpdate) {
m_glMonitor->refresh();
} else {
m_glMonitor->requestRefresh();
}
}
}
void Monitor::pause()
{
if (!m_playAction->isActive()) {
return;
}
slotActivateMonitor();
m_glMonitor->switchPlay(false);
m_playAction->setActive(false);
}
void Monitor::switchPlay(bool play)
{
m_playAction->setActive(play);
m_glMonitor->switchPlay(play);
}
void Monitor::slotSwitchPlay()
{
slotActivateMonitor();
m_glMonitor->switchPlay(m_playAction->isActive());
}
void Monitor::slotPlay()
{
m_playAction->trigger();
}
void Monitor::slotPlayZone()
{
slotActivateMonitor();
bool ok = m_glMonitor->playZone();
if (ok) {
m_playAction->setActive(true);
}
}
void Monitor::slotLoopZone()
{
slotActivateMonitor();
bool ok = m_glMonitor->playZone(true);
if (ok) {
m_playAction->setActive(true);
}
}
void Monitor::slotLoopClip()
{
slotActivateMonitor();
bool ok = m_glMonitor->loopClip();
if (ok) {
m_playAction->setActive(true);
}
}
void Monitor::updateClipProducer(Mlt::Producer *prod)
{
if (m_glMonitor->setProducer(prod, isActive(), -1)) {
prod->set_speed(1.0);
}
}
void Monitor::updateClipProducer(const QString &playlist)
{
Q_UNUSED(playlist)
// TODO
// Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData());
// m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition());
m_glMonitor->switchPlay(true);
}
void Monitor::slotOpenClip(std::shared_ptr controller, int in, int out)
{
if (m_controller) {
disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this,
SLOT(checkOverlay()));
disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
}
m_controller = controller;
loadQmlScene(MonitorSceneDefault);
m_snaps.reset(new SnapModel());
m_glMonitor->getControllerProxy()->resetZone();
if (controller) {
connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this,
SLOT(checkOverlay()));
connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay()));
if (m_recManager->toolbar()->isVisible()) {
// we are in record mode, don't display clip
return;
}
m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), controller->getMarkerModel());
m_timePos->setRange(0, (int)m_controller->frameDuration());
updateMarkers();
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection);
connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection);
if (out == -1) {
m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false);
qDebug() << m_controller->zone();
} else {
m_glMonitor->getControllerProxy()->setZone(in, out, false);
}
m_snaps->addPoint((int)m_controller->frameDuration());
// Loading new clip / zone, stop if playing
if (m_playAction->isActive()) {
m_playAction->setActive(false);
}
m_glMonitor->setProducer(m_controller->originalProducer().get(), isActive(), in);
m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0;
m_glMonitor->setAudioThumb(controller->audioChannels(), controller->audioFrameCache);
m_controller->getMarkerModel()->registerSnapModel(m_snaps);
// hasEffects = controller->hasEffects();
} else {
m_glMonitor->setProducer(nullptr, isActive());
m_glMonitor->setAudioThumb();
m_audioMeterWidget->audioChannels = 0;
}
if (slotActivateMonitor()) {
start();
}
checkOverlay();
}
const QString Monitor::activeClipId()
{
if (m_controller) {
return m_controller->AbstractProjectItem::clipId();
}
return QString();
}
void Monitor::slotOpenDvdFile(const QString &file)
{
// TODO
Q_UNUSED(file)
m_glMonitor->initializeGL();
// render->loadUrl(file);
}
void Monitor::slotSaveZone()
{
// TODO? or deprecate
// render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone());
}
void Monitor::setCustomProfile(const QString &profile, const Timecode &tc)
{
// TODO or deprecate
Q_UNUSED(profile)
m_timePos->updateTimeCode(tc);
if (true) {
return;
}
slotActivateMonitor();
// render->prepareProfileReset(tc.fps());
if (m_multitrackView) {
m_multitrackView->setChecked(false);
}
// TODO: this is a temporary profile for DVD preview, it should not alter project profile
// pCore->setCurrentProfile(profile);
m_glMonitor->reloadProfile();
}
void Monitor::resetProfile()
{
m_timePos->updateTimeCode(m_monitorManager->timecode());
m_glMonitor->reloadProfile();
m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height()));
double fps = m_monitorManager->timecode().fps();
// Update drop frame info
m_qmlManager->setProperty(QStringLiteral("dropped"), false);
m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2));
}
void Monitor::resetConsumer(bool fullReset)
{
m_glMonitor->resetConsumer(fullReset);
}
const QString Monitor::sceneList(const QString &root, const QString &fullPath)
{
return m_glMonitor->sceneList(root, fullPath);
}
void Monitor::updateClipZone()
{
if (m_controller == nullptr) {
return;
}
m_controller->setZone(m_glMonitor->getControllerProxy()->zone());
}
void Monitor::updateTimelineClipZone()
{
emit zoneUpdated(m_glMonitor->getControllerProxy()->zone());
}
void Monitor::switchDropFrames(bool drop)
{
m_glMonitor->setDropFrames(drop);
}
void Monitor::switchMonitorInfo(int code)
{
int currentOverlay;
if (m_id == Kdenlive::ClipMonitor) {
currentOverlay = KdenliveSettings::displayClipMonitorInfo();
currentOverlay ^= code;
KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay);
} else {
currentOverlay = KdenliveSettings::displayProjectMonitorInfo();
currentOverlay ^= code;
KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay);
}
updateQmlDisplay(currentOverlay);
}
void Monitor::updateMonitorGamma()
{
if (isActive()) {
stop();
m_glMonitor->updateGamma();
start();
} else {
m_glMonitor->updateGamma();
}
}
void Monitor::slotEditMarker()
{
if (m_editMarker) {
m_editMarker->trigger();
}
}
void Monitor::updateTimecodeFormat()
{
m_timePos->slotUpdateTimeCodeFormat();
m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText());
}
QPoint Monitor::getZoneInfo() const
{
if (m_controller == nullptr) {
return QPoint();
}
return m_controller->zone();
}
void Monitor::slotEnableEffectScene(bool enable)
{
KdenliveSettings::setShowOnMonitorScene(enable);
MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault;
slotShowEffectScene(sceneType, true);
if (enable) {
emit seekPosition(m_glMonitor->getCurrentPos());
}
}
void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary)
{
if (sceneType == MonitorSceneNone) {
// We just want to revert to normal scene
if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) {
// Ok, nothing to do
return;
}
sceneType = MonitorSceneDefault;
}
if (!temporary) {
m_lastMonitorSceneType = sceneType;
}
loadQmlScene(sceneType);
}
void Monitor::slotSeekToKeyFrame()
{
if (m_qmlManager->sceneType() == MonitorSceneGeometry) {
// Adjust splitter pos
int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt();
emit seekToKeyframe(kfr);
}
}
void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types)
{
QQuickItem *root = m_glMonitor->rootObject();
if (!root) {
return;
}
if (!list.isEmpty()) {
root->setProperty("centerPointsTypes", types);
root->setProperty("centerPoints", list);
}
if (!r.isEmpty()) {
root->setProperty("framesize", r);
}
}
void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value)
{
QQuickItem *root = m_glMonitor->rootObject();
if (!root) {
return;
}
root->setProperty(name.toUtf8().constData(), value);
}
QRect Monitor::effectRect() const
{
QQuickItem *root = m_glMonitor->rootObject();
if (!root) {
return QRect();
}
return root->property("framesize").toRect();
}
QVariantList Monitor::effectPolygon() const
{
QQuickItem *root = m_glMonitor->rootObject();
if (!root) {
return QVariantList();
}
return root->property("centerPoints").toList();
}
QVariantList Monitor::effectRoto() const
{
QQuickItem *root = m_glMonitor->rootObject();
if (!root) {
return QVariantList();
}
QVariantList points = root->property("centerPoints").toList();
QVariantList controlPoints = root->property("centerPointsTypes").toList();
// rotoscoping effect needs a list of
QVariantList mix;
mix.reserve(points.count() * 3);
for (int i = 0; i < points.count(); i++) {
mix << controlPoints.at(2 * i);
mix << points.at(i);
mix << controlPoints.at(2 * i + 1);
}
return mix;
}
void Monitor::setEffectKeyframe(bool enable)
{
QQuickItem *root = m_glMonitor->rootObject();
if (root) {
root->setProperty("iskeyframe", enable);
}
}
bool Monitor::effectSceneDisplayed(MonitorSceneType effectType)
{
return m_qmlManager->sceneType() == effectType;
}
void Monitor::slotSetVolume(int volume)
{
KdenliveSettings::setVolume(volume);
QIcon icon;
double renderVolume = m_glMonitor->volume();
m_glMonitor->setVolume((double)volume / 100.0);
if (renderVolume > 0 && volume > 0) {
return;
}
if (volume == 0) {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted"));
} else {
icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
}
m_audioButton->setIcon(icon);
}
void Monitor::sendFrameForAnalysis(bool analyse)
{
m_glMonitor->sendFrameForAnalysis = analyse;
}
void Monitor::updateAudioForAnalysis()
{
m_glMonitor->updateAudioForAnalysis();
}
void Monitor::onFrameDisplayed(const SharedFrame &frame)
{
m_monitorManager->frameDisplayed(frame);
int position = frame.get_position();
if (!m_glMonitor->checkFrameNumber(position, m_id == Kdenlive::ClipMonitor ? 0 : TimelineModel::seekDuration + 1)) {
m_playAction->setActive(false);
}
checkDrops(m_glMonitor->droppedFrames());
}
void Monitor::checkDrops(int dropped)
{
if (m_droppedTimer.isValid()) {
if (m_droppedTimer.hasExpired(1000)) {
m_droppedTimer.invalidate();
double fps = m_monitorManager->timecode().fps();
if (dropped == 0) {
// No dropped frames since last check
m_qmlManager->setProperty(QStringLiteral("dropped"), false);
m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2));
} else {
m_glMonitor->resetDrops();
fps -= dropped;
m_qmlManager->setProperty(QStringLiteral("dropped"), true);
m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2));
m_droppedTimer.start();
}
}
} else if (dropped > 0) {
// Start m_dropTimer
m_glMonitor->resetDrops();
m_droppedTimer.start();
}
}
void Monitor::reloadProducer(const QString &id)
{
if (!m_controller) {
return;
}
if (m_controller->AbstractProjectItem::clipId() == id) {
slotOpenClip(m_controller);
}
}
QString Monitor::getMarkerThumb(GenTime pos)
{
if (!m_controller) {
return QString();
}
if (!m_controller->getClipHash().isEmpty()) {
QString url = m_monitorManager->getCacheFolder(CacheThumbs)
.absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') +
QString::number((int)pos.frames(m_monitorManager->timecode().fps())) + QStringLiteral(".png"));
if (QFile::exists(url)) {
return url;
}
}
return QString();
}
const QString Monitor::projectFolder() const
{
return m_monitorManager->getProjectFolder();
}
void Monitor::setPalette(const QPalette &p)
{
QWidget::setPalette(p);
QList allButtons = this->findChildren();
for (int i = 0; i < allButtons.count(); i++) {
QToolButton *m = allButtons.at(i);
QIcon ic = m->icon();
if (ic.isNull() || ic.name().isEmpty()) {
continue;
}
QIcon newIcon = QIcon::fromTheme(ic.name());
m->setIcon(newIcon);
}
QQuickItem *root = m_glMonitor->rootObject();
if (root) {
QMetaObject::invokeMethod(root, "updatePalette");
}
m_audioMeterWidget->refreshPixmap();
}
void Monitor::gpuError()
{
qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager";
warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1);
}
void Monitor::warningMessage(const QString &text, int timeout, const QList &actions)
{
m_infoMessage->setMessageType(KMessageWidget::Warning);
m_infoMessage->setText(text);
for (QAction *action : actions) {
m_infoMessage->addAction(action);
}
m_infoMessage->setCloseButtonVisible(true);
m_infoMessage->animatedShow();
if (timeout > 0) {
QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide);
}
}
void Monitor::activateSplit()
{
loadQmlScene(MonitorSceneSplit);
if (isActive()) {
m_glMonitor->requestRefresh();
} else if (slotActivateMonitor()) {
start();
}
}
void Monitor::slotSwitchCompare(bool enable)
{
if (m_id == Kdenlive::ProjectMonitor) {
if (enable) {
if (m_qmlManager->sceneType() == MonitorSceneSplit) {
// Split scene is already active
return;
}
m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad");
if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) {
m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter
m_splitEffect->set("1", 0); // 1 is gradient width
m_splitEffect->set("2", -0.747); // 2 is tilt
} else {
// frei0r.scal0tilt is not available
warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"));
return;
}
emit createSplitOverlay(m_splitEffect);
return;
}
// Delete temp track
emit removeSplitOverlay();
delete m_splitEffect;
m_splitEffect = nullptr;
loadQmlScene(MonitorSceneDefault);
if (isActive()) {
m_glMonitor->requestRefresh();
} else if (slotActivateMonitor()) {
start();
}
return;
}
if (m_controller == nullptr || !m_controller->hasEffects()) {
// disable split effect
if (m_controller) {
pCore->displayMessage(i18n("Clip has no effects"), InformationMessage);
} else {
pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage);
}
return;
}
if (enable) {
if (m_qmlManager->sceneType() == MonitorSceneSplit) {
// Split scene is already active
qDebug() << " . . . .. ALREADY ACTIVE";
return;
}
buildSplitEffect(m_controller->masterProducer());
} else if (m_splitEffect) {
// TODO
m_glMonitor->setProducer(m_controller->originalProducer().get(), isActive(), position());
delete m_splitEffect;
m_splitProducer = nullptr;
m_splitEffect = nullptr;
loadQmlScene(MonitorSceneDefault);
}
slotActivateMonitor();
}
void Monitor::buildSplitEffect(Mlt::Producer *original)
{
m_splitEffect = new Mlt::Filter(*profile(), "frei0r.alphagrad");
if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) {
m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter
m_splitEffect->set("1", 0); // 1 is gradient width
m_splitEffect->set("2", -0.747); // 2 is tilt
} else {
// frei0r.scal0tilt is not available
pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage);
return;
}
QString splitTransition = TransitionsRepository::get()->getCompositingTransition();
Mlt::Transition t(*profile(), splitTransition.toUtf8().constData());
if (!t.is_valid()) {
delete m_splitEffect;
pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage);
return;
}
Mlt::Tractor trac(*profile());
std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original));
// Delete all effects
int ct = 0;
Mlt::Filter *filter = clone->filter(ct);
while (filter != nullptr) {
QString ix = QString::fromLatin1(filter->get("kdenlive_id"));
if (!ix.isEmpty()) {
if (clone->detach(*filter) == 0) {
} else {
ct++;
}
} else {
ct++;
}
delete filter;
filter = clone->filter(ct);
}
trac.set_track(*original, 0);
trac.set_track(*clone.get(), 1);
clone.get()->attach(*m_splitEffect);
t.set("always_active", 1);
trac.plant_transition(t, 0, 1);
delete original;
m_splitProducer = new Mlt::Producer(trac.get_producer());
m_glMonitor->setProducer(m_splitProducer, isActive(), position());
m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel());
loadQmlScene(MonitorSceneSplit);
}
QSize Monitor::profileSize() const
{
return m_glMonitor->profileSize();
}
void Monitor::loadQmlScene(MonitorSceneType type)
{
if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) {
return;
}
bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto;
if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) {
// User doesn't want effect scenes
+ pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500);
type = MonitorSceneDefault;
}
double ratio = (double)m_glMonitor->profileSize().width() / (int)(m_glMonitor->profileSize().height() * m_glMonitor->profile()->dar() + 0.5);
m_qmlManager->setScene(m_id, type, m_glMonitor->profileSize(), ratio, m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum());
QQuickItem *root = m_glMonitor->rootObject();
switch (type) {
case MonitorSceneSplit:
QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection);
break;
case MonitorSceneGeometry:
case MonitorSceneCorners:
case MonitorSceneRoto:
break;
case MonitorSceneRipple:
QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection);
QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection);
break;
case MonitorSceneDefault:
QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection);
m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText());
if (m_id == Kdenlive::ClipMonitor) {
updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo());
} else if (m_id == Kdenlive::ProjectMonitor) {
updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo());
}
break;
default:
break;
}
m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(m_monitorManager->timecode().fps(), 'g', 2));
}
void Monitor::setQmlProperty(const QString &name, const QVariant &value)
{
m_qmlManager->setProperty(name, value);
}
void Monitor::slotAdjustEffectCompare()
{
QRect r = m_glMonitor->rect();
double percent = 0.5;
if (m_qmlManager->sceneType() == MonitorSceneSplit) {
// Adjust splitter pos
QQuickItem *root = m_glMonitor->rootObject();
percent = 0.5 - ((root->property("splitterPos").toInt() - r.left() - r.width() / 2.0) / (double)r.width() / 2.0) / 0.75;
// Store real frame percentage for resize events
root->setProperty("realpercent", percent);
}
if (m_splitEffect) {
m_splitEffect->set("0", percent);
}
m_glMonitor->refresh();
}
Mlt::Profile *Monitor::profile()
{
return m_glMonitor->profile();
}
void Monitor::slotSwitchRec(bool enable)
{
if (!m_recManager) {
return;
}
if (enable) {
m_toolbar->setVisible(false);
m_recManager->toolbar()->setVisible(true);
} else if (m_recManager->toolbar()->isVisible()) {
m_recManager->stop();
m_toolbar->setVisible(true);
emit refreshCurrentClip();
}
}
bool Monitor::startCapture(const QString ¶ms, const QString &path, Mlt::Producer *p)
{
// TODO
m_controller = nullptr;
if (false) { // render->updateProducer(p)) {
m_glMonitor->reconfigureMulti(params, path, p->profile());
return true;
}
return false;
}
bool Monitor::stopCapture()
{
m_glMonitor->stopCapture();
slotOpenClip(nullptr);
m_glMonitor->reconfigure(profile());
return true;
}
void Monitor::doKeyPressEvent(QKeyEvent *ev)
{
keyPressEvent(ev);
}
void Monitor::slotEditInlineMarker()
{
QQuickItem *root = m_glMonitor->rootObject();
if (root) {
std::shared_ptr model;
if (m_controller) {
// We are editing a clip marker
model = m_controller->getMarkerModel();
} else {
model = pCore->currentDoc()->getGuideModel();
}
QString newComment = root->property("markerText").toString();
bool found = false;
CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found);
if (!found || newComment == oldMarker.comment()) {
// No change
return;
}
oldMarker.setComment(newComment);
model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType());
}
}
void Monitor::prepareAudioThumb(int channels, QVariantList &audioCache)
{
m_glMonitor->setAudioThumb(channels, audioCache);
}
void Monitor::slotSwitchAudioMonitor()
{
if (!m_audioMeterWidget->isValid) {
KdenliveSettings::setMonitoraudio(0x01);
m_audioMeterWidget->setVisibility(false);
return;
}
int currentOverlay = KdenliveSettings::monitoraudio();
currentOverlay ^= m_id;
KdenliveSettings::setMonitoraudio(currentOverlay);
if ((KdenliveSettings::monitoraudio() & m_id) != 0) {
// We want to enable this audio monitor, so make monitor active
slotActivateMonitor();
}
displayAudioMonitor(isActive());
}
void Monitor::displayAudioMonitor(bool isActive)
{
bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0);
if (enable) {
connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection);
} else {
disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame);
}
m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0);
}
void Monitor::updateQmlDisplay(int currentOverlay)
{
m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0);
m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04);
m_glMonitor->rootObject()->setProperty("showFps", currentOverlay & 0x20);
m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02);
m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10);
}
void Monitor::clearDisplay()
{
m_glMonitor->clear();
}
void Monitor::panView(QPoint diff)
{
// Only pan if scrollbars are visible
if (m_horizontalScroll->isVisible()) {
m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x());
}
if (m_verticalScroll->isVisible()) {
m_verticalScroll->setValue(m_verticalScroll->value() + diff.y());
}
}
void Monitor::requestSeek(int pos)
{
m_glMonitor->seek(pos);
}
void Monitor::setProducer(Mlt::Producer *producer, int pos)
{
m_glMonitor->setProducer(producer, isActive(), pos);
}
void Monitor::reconfigure()
{
m_glMonitor->reconfigure();
}
void Monitor::slotSeekPosition(int pos)
{
m_timePos->setValue(pos);
checkOverlay();
}
void Monitor::slotStart()
{
slotActivateMonitor();
m_glMonitor->switchPlay(false);
m_glMonitor->seek(0);
}
void Monitor::slotEnd()
{
slotActivateMonitor();
m_glMonitor->switchPlay(false);
if (m_id == Kdenlive::ClipMonitor) {
m_glMonitor->seek(m_glMonitor->duration());
} else {
m_glMonitor->seek(pCore->projectDuration());
}
}
void Monitor::addSnapPoint(int pos)
{
m_snaps->addPoint(pos);
}
void Monitor::removeSnapPoint(int pos)
{
m_snaps->removePoint(pos);
}
void Monitor::slotZoomIn()
{
m_glMonitor->slotZoom(true);
}
void Monitor::slotZoomOut()
{
m_glMonitor->slotZoom(false);
}
void Monitor::setConsumerProperty(const QString &name, const QString &value)
{
m_glMonitor->setConsumerProperty(name, value);
}
diff --git a/src/monitor/qmlmanager.cpp b/src/monitor/qmlmanager.cpp
index 9f09b599f..803f8f952 100644
--- a/src/monitor/qmlmanager.cpp
+++ b/src/monitor/qmlmanager.cpp
@@ -1,160 +1,159 @@
/***************************************************************************
* Copyright (C) 2016 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
* This file is part of Kdenlive. See www.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) version 3 or any later version *
* accepted by the membership of KDE e.V. (or its successor approved *
* by the membership of KDE e.V.), which shall act as a proxy *
* defined in Section 14 of version 3 of the license. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see . *
***************************************************************************/
#include "qmlmanager.h"
#include "qml/qmlaudiothumb.h"
#include
#include
#include
QmlManager::QmlManager(QQuickView *view)
: QObject(view)
, m_view(view)
, m_sceneType(MonitorSceneNone)
{
}
void QmlManager::enableAudioThumbs(bool enable)
{
auto *audioThumbDisplay = m_view->rootObject()->findChild(QStringLiteral("audiothumb"));
if (audioThumbDisplay) {
audioThumbDisplay->setProperty("stateVisible", enable);
}
}
MonitorSceneType QmlManager::sceneType() const
{
return m_sceneType;
}
void QmlManager::setProperty(const QString &name, const QVariant &value)
{
m_view->rootObject()->setProperty(name.toUtf8().constData(), value);
}
void QmlManager::setScene(Kdenlive::MonitorId id, MonitorSceneType type, QSize profile, double profileStretch, QRect displayRect, double zoom, int duration)
{
if (type == m_sceneType) {
// Scene type already active
return;
}
if (id == Kdenlive::DvdMonitor) {
return;
}
m_sceneType = type;
QQuickItem *root = nullptr;
switch (type) {
case MonitorSceneGeometry:
m_view->setSource(QUrl(QStringLiteral("qrc:/qml/kdenlivemonitoreffectscene.qml")));
root = m_view->rootObject();
QObject::connect(root, SIGNAL(effectChanged()), this, SLOT(effectRectChanged()), Qt::UniqueConnection);
QObject::connect(root, SIGNAL(centersChanged()), this, SLOT(effectPolygonChanged()), Qt::UniqueConnection);
root->setProperty("profile", QPoint(profile.width(), profile.height()));
root->setProperty("framesize", QRect(0, 0, profile.width(), profile.height()));
root->setProperty("scalex", (double)displayRect.width() / profile.width() * zoom);
root->setProperty("scaley", (double)displayRect.width() / profileStretch / profile.width() * zoom);
root->setProperty("center", displayRect.center());
break;
case MonitorSceneCorners:
qDebug() << "/// LOADING CORNERS SCENE\n\n+++++++++++++++++++++++++\n------------------\n+++++++++++++++++";
m_view->setSource(QUrl(QStringLiteral("qrc:/qml/kdenlivemonitorcornerscene.qml")));
root = m_view->rootObject();
QObject::connect(root, SIGNAL(effectPolygonChanged()), this, SLOT(effectPolygonChanged()), Qt::UniqueConnection);
root->setProperty("profile", QPoint(profile.width(), profile.height()));
root->setProperty("framesize", QRect(0, 0, profile.width(), profile.height()));
root->setProperty("scalex", (double)displayRect.width() / profile.width() * zoom);
root->setProperty("scaley", (double)displayRect.width() / profileStretch / profile.width() * zoom);
root->setProperty("stretch", profileStretch);
root->setProperty("center", displayRect.center());
break;
case MonitorSceneRoto:
- // TODO
m_view->setSource(QUrl(QStringLiteral("qrc:/qml/kdenlivemonitorrotoscene.qml")));
root = m_view->rootObject();
QObject::connect(root, SIGNAL(effectPolygonChanged()), this, SLOT(effectRotoChanged()), Qt::UniqueConnection);
root->setProperty("profile", QPoint(profile.width(), profile.height()));
root->setProperty("framesize", QRect(0, 0, profile.width(), profile.height()));
root->setProperty("scalex", (double)displayRect.width() / profile.width() * zoom);
root->setProperty("scaley", (double)displayRect.width() / profileStretch / profile.width() * zoom);
root->setProperty("stretch", profileStretch);
root->setProperty("center", displayRect.center());
break;
case MonitorSceneSplit:
m_view->setSource(QUrl(QStringLiteral("qrc:/qml/kdenlivemonitorsplit.qml")));
root = m_view->rootObject();
break;
case MonitorSceneRipple:
m_view->setSource(QUrl(QStringLiteral("qrc:/qml/kdenlivemonitorripple.qml")));
root = m_view->rootObject();
break;
default:
m_view->setSource(
QUrl(id == Kdenlive::ClipMonitor ? QStringLiteral("qrc:/qml/kdenliveclipmonitor.qml") : QStringLiteral("qrc:/qml/kdenlivemonitor.qml")));
root = m_view->rootObject();
root->setProperty("profile", QPoint(profile.width(), profile.height()));
root->setProperty("scalex", (double)displayRect.width() / profile.width() * zoom);
root->setProperty("scaley", (double)displayRect.width() / profileStretch / profile.width() * zoom);
break;
}
if (root && duration > 0) {
root->setProperty("duration", duration);
}
const QFont ft = QFontDatabase::systemFont(QFontDatabase::FixedFont);
m_view->rootContext()->setContextProperty("fixedFont", ft);
}
void QmlManager::effectRectChanged()
{
if (!m_view->rootObject()) {
return;
}
const QRect rect = m_view->rootObject()->property("framesize").toRect();
emit effectChanged(rect);
}
void QmlManager::effectPolygonChanged()
{
if (!m_view->rootObject()) {
return;
}
QVariantList points = m_view->rootObject()->property("centerPoints").toList();
qDebug() << "// GOT NEW POLYGON FROM QML: " << points;
emit effectPointsChanged(points);
}
void QmlManager::effectRotoChanged()
{
if (!m_view->rootObject()) {
return;
}
QVariantList points = m_view->rootObject()->property("centerPoints").toList();
QVariantList controlPoints = m_view->rootObject()->property("centerPointsTypes").toList();
// rotoscoping effect needs a list of
QVariantList mix;
mix.reserve(points.count());
for (int i = 0; i < points.count(); i++) {
mix << controlPoints.at(2 * i);
mix << points.at(i);
mix << controlPoints.at(2 * i + 1);
}
emit effectPointsChanged(mix);
}
diff --git a/src/monitor/view/kdenliveclipmonitor.qml b/src/monitor/view/kdenliveclipmonitor.qml
index 554cd38a7..d805053a9 100644
--- a/src/monitor/view/kdenliveclipmonitor.qml
+++ b/src/monitor/view/kdenliveclipmonitor.qml
@@ -1,263 +1,257 @@
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Window 2.2
import Kdenlive.Controls 1.0
import QtQuick 2.6
import AudioThumb 1.0
Item {
id: root
objectName: "root"
SystemPalette { id: activePalette }
// default size, but scalable by user
height: 300; width: 400
property string markerText
property point profile
property double zoom
property double scalex
property double scaley
property bool dropped
property string fps
property bool showMarkers
property bool showTimecode
property bool showFps
property bool showSafezone
property bool showAudiothumb
property bool showToolbar: false
property real baseUnit: fontMetrics.font.pixelSize * 0.8
property int duration: 300
property int mouseRulerPos: 0
property double frameSize: 10
property double timeScale: 1
property int overlayType: controller.overlayType
property color overlayColor: 'cyan'
property bool isClipMonitor: true
property int dragType: 0
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
signal editCurrentMarker()
signal toolBarChanged(bool doAccept)
onDurationChanged: {
clipMonitorRuler.updateRuler()
}
onWidthChanged: {
clipMonitorRuler.updateRuler()
}
function updatePalette() {
clipMonitorRuler.forceRepaint()
}
function switchOverlay() {
if (controller.overlayType >= 5) {
controller.overlayType = 0
} else {
controller.overlayType = controller.overlayType + 1;
}
root.overlayType = controller.overlayType
}
MouseArea {
id: barOverArea
hoverEnabled: true
acceptedButtons: Qt.NoButton
anchors.fill: parent
}
SceneToolBar {
id: sceneToolBar
anchors {
right: parent.right
top: parent.top
topMargin: 4
rightMargin: 4
}
visible: barOverArea.mouseX >= x - 10
}
Item {
height: root.height - controller.rulerHeight
width: root.width
Item {
id: frame
objectName: "referenceframe"
width: root.profile.x * root.scalex
height: root.profile.y * root.scaley
anchors.centerIn: parent
Loader {
anchors.fill: parent
source: {
switch(root.overlayType)
{
- case 0: {
+ case 0:
return '';
- }
- case 1: {
+ case 1:
return "OverlayStandard.qml";
- }
- case 2:{
+ case 2:
return "OverlayMinimal.qml";
- }
- case 3:{
+ case 3:
return "OverlayCenter.qml";
- }
- case 4:{
+ case 4:
return "OverlayCenterDiagonal.qml";
- }
- case 5:{
+ case 5:
return "OverlayThirds.qml";
- }
}
}
}
}
Item {
id: monitorOverlay
anchors.fill: parent
QmlAudioThumb {
id: audioThumb
objectName: "audiothumb"
property bool stateVisible: true
anchors {
left: parent.left
bottom: parent.bottom
}
height: parent.height / 6
//font.pixelSize * 3
width: parent.width
visible: root.showAudiothumb
MouseArea {
id: mouseOverArea
hoverEnabled: true
onExited: audioThumb.stateVisible = false
onEntered: audioThumb.stateVisible = true
acceptedButtons: Qt.NoButton
anchors.fill: parent
}
states: [
State { when: audioThumb.stateVisible;
PropertyChanges { target: audioThumb; opacity: 1.0 } },
State { when: !audioThumb.stateVisible;
PropertyChanges { target: audioThumb; opacity: 0.0 } }
]
transitions: [ Transition {
NumberAnimation { property: "opacity"; duration: 500}
} ]
}
Text {
id: timecode
font: fixedFont
objectName: "timecode"
color: "white"
style: Text.Outline;
styleColor: "black"
text: controller.toTimecode(controller.position)
visible: root.showTimecode
anchors {
right: parent.right
bottom: parent.bottom
rightMargin: 4
}
}
Text {
id: fpsdropped
font: fixedFont
objectName: "fpsdropped"
color: root.dropped ? "red" : "white"
style: Text.Outline;
styleColor: "black"
text: root.fps + "fps"
visible: root.showFps
anchors {
right: timecode.visible ? timecode.left : parent.right
bottom: parent.bottom
rightMargin: 10
}
}
TextField {
id: marker
font: fixedFont
objectName: "markertext"
activeFocusOnPress: true
onEditingFinished: {
root.markerText = marker.displayText
marker.focus = false
root.editCurrentMarker()
}
anchors {
left: parent.left
bottom: parent.bottom
}
visible: root.showMarkers && text != ""
text: controller.markerComment
maximumLength: 20
style: TextFieldStyle {
textColor: "white"
background: Rectangle {
color: controller.position == controller.zoneIn ? "#9900ff00" : controller.position == controller.zoneOut ? "#99ff0000" : "#990000ff"
width: marker.width
}
}
}
}
MouseArea {
id: dragOverArea
hoverEnabled: true
acceptedButtons: Qt.LeftButton
x: 0
width: 2 * audioDragButton.width
height: 2.5 * audioDragButton.height
y: parent.height - height
propagateComposedEvents: true
onPressed: {
// First found child is the Column
var clickedChild = childAt(mouseX,mouseY).childAt(mouseX,mouseY)
if (clickedChild == audioDragButton) {
dragType = 1
} else if (clickedChild == videoDragButton) {
dragType = 2
} else {
dragType = 0
}
mouse.accepted = false
}
Column {
ToolButton {
id: audioDragButton
iconName: "audio-volume-medium"
tooltip: "Audio only drag"
x: 10
enabled: false
visible: dragOverArea.containsMouse
}
ToolButton {
id: videoDragButton
iconName: "kdenlive-show-video"
tooltip: "Video only drag"
x: 10
enabled: false
visible: dragOverArea.containsMouse
}
}
}
}
MonitorRuler {
id: clipMonitorRuler
anchors {
left: root.left
right: root.right
bottom: root.bottom
}
height: controller.rulerHeight
}
}
diff --git a/src/monitor/view/kdenlivemonitor.qml b/src/monitor/view/kdenlivemonitor.qml
index c382ee552..754e000a6 100644
--- a/src/monitor/view/kdenlivemonitor.qml
+++ b/src/monitor/view/kdenlivemonitor.qml
@@ -1,188 +1,182 @@
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Window 2.2
import Kdenlive.Controls 1.0
import QtQuick 2.4
import AudioThumb 1.0
Item {
id: root
objectName: "root"
SystemPalette { id: activePalette }
// default size, but scalable by user
height: 300; width: 400
property string markerText
property point profile
property double zoom
property double scalex
property double scaley
property bool dropped
property string fps
property bool showMarkers
property bool showTimecode
property bool showFps
property bool showSafezone
property bool showAudiothumb
property real baseUnit: fontMetrics.font.pixelSize * 0.8
property int duration: 300
property int mouseRulerPos: 0
property double frameSize: 10
property double timeScale: 1
property int overlayType: controller.overlayType
property color overlayColor: 'cyan'
property bool isClipMonitor: false
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
signal editCurrentMarker()
signal toolBarChanged(bool doAccept)
onDurationChanged: {
clipMonitorRuler.updateRuler()
}
onWidthChanged: {
clipMonitorRuler.updateRuler()
}
function updatePalette() {
clipMonitorRuler.forceRepaint()
}
function switchOverlay() {
if (controller.overlayType >= 5) {
controller.overlayType = 0
} else {
controller.overlayType = controller.overlayType + 1;
}
root.overlayType = controller.overlayType
}
MouseArea {
id: barOverArea
hoverEnabled: true
acceptedButtons: Qt.NoButton
anchors.fill: parent
}
SceneToolBar {
id: sceneToolBar
anchors {
right: parent.right
top: parent.top
topMargin: 4
rightMargin: 4
}
visible: barOverArea.mouseX >= x - 10
}
Item {
height: root.height - controller.rulerHeight
width: root.width
Item {
id: frame
objectName: "referenceframe"
width: root.profile.x * root.scalex
height: root.profile.y * root.scaley
anchors.centerIn: parent
Loader {
anchors.fill: parent
source: {
switch(root.overlayType)
{
- case 0: {
+ case 0:
return '';
- }
- case 1: {
+ case 1:
return "OverlayStandard.qml";
- }
- case 2:{
+ case 2:
return "OverlayMinimal.qml";
- }
- case 3:{
+ case 3:
return "OverlayCenter.qml";
- }
- case 4:{
+ case 4:
return "OverlayCenterDiagonal.qml";
- }
- case 5:{
+ case 5:
return "OverlayThirds.qml";
- }
}
}
}
}
Item {
id: monitorOverlay
anchors.fill: parent
Text {
id: timecode
font: fixedFont
objectName: "timecode"
color: "white"
style: Text.Outline;
styleColor: "black"
text: controller.toTimecode(controller.position)
visible: root.showTimecode
anchors {
right: parent.right
bottom: parent.bottom
rightMargin: 4
}
}
Text {
id: fpsdropped
font: fixedFont
objectName: "fpsdropped"
color: root.dropped ? "red" : "white"
style: Text.Outline;
styleColor: "black"
text: root.fps + "fps"
visible: root.showFps
anchors {
right: timecode.visible ? timecode.left : parent.right
bottom: parent.bottom
rightMargin: 10
}
}
TextField {
id: marker
font: fixedFont
objectName: "markertext"
activeFocusOnPress: true
onEditingFinished: {
root.markerText = marker.displayText
marker.focus = false
root.editCurrentMarker()
}
anchors {
left: parent.left
bottom: parent.bottom
}
visible: root.showMarkers && text != ""
text: controller.markerComment
maximumLength: 20
style: TextFieldStyle {
textColor: "white"
background: Rectangle {
color: controller.position == controller.zoneIn ? "#9900ff00" : controller.position == controller.zoneOut ? "#99ff0000" : "#990000ff"
width: marker.width
}
}
}
}
}
MonitorRuler {
id: clipMonitorRuler
anchors {
left: root.left
right: root.right
bottom: root.bottom
}
height: controller.rulerHeight
}
}
diff --git a/src/monitor/view/kdenlivemonitorrotoscene.qml b/src/monitor/view/kdenlivemonitorrotoscene.qml
index 997206a40..79caae99c 100644
--- a/src/monitor/view/kdenlivemonitorrotoscene.qml
+++ b/src/monitor/view/kdenlivemonitorrotoscene.qml
@@ -1,334 +1,334 @@
import QtQuick 2.6
import QtQuick.Controls 1.4
Item {
id: root
objectName: "rootrotoscene"
SystemPalette { id: activePalette }
// default size, but scalable by user
height: 300; width: 400
property string comment
property string framenum
property point profile
property point center
property double scalex : 1
property double scaley : 1
property double stretch : 1
property double sourcedar : 1
property double offsetx : 0
property double offsety : 0
property double frameSize: 10
property int duration: 300
property double timeScale: 1
property real baseUnit: fontMetrics.font.pointSize
property int mouseRulerPos: 0
onOffsetxChanged: canvas.requestPaint()
onOffsetyChanged: canvas.requestPaint()
onScalexChanged: canvas.requestPaint()
onScaleyChanged: canvas.requestPaint()
onSourcedarChanged: refreshdar()
property bool iskeyframe : true
property bool isDefined: false
property int requestedKeyFrame : -1
property int requestedSubKeyFrame : -1
// The coordinate points where the bezier curve passes
property var centerPoints : []
// The control points for the bezier curve points (2 controls points for each coordinate)
property var centerPointsTypes : []
property bool showToolbar: false
onCenterPointsTypesChanged: checkDefined()
signal effectPolygonChanged()
signal addKeyframe()
signal seekToKeyframe()
onDurationChanged: {
clipMonitorRuler.updateRuler()
}
onWidthChanged: {
clipMonitorRuler.updateRuler()
}
onIskeyframeChanged: {
console.log('KEYFRAME CHANGED: ', iskeyframe)
canvas.requestPaint()
}
FontMetrics {
id: fontMetrics
font.family: "Arial"
}
function refreshdar() {
canvas.darOffset = root.sourcedar < root.profile.x * root.stretch / root.profile.y ? (root.profile.x * root.stretch - root.profile.y * root.sourcedar) / (2 * root.profile.x * root.stretch) :(root.profile.y - root.profile.x * root.stretch / root.sourcedar) / (2 * root.profile.y);
canvas.requestPaint()
}
function checkDefined() {
root.isDefined = root.centerPointsTypes.length > 0
canvas.requestPaint()
}
Item {
id: monitorOverlay
height: root.height - controller.rulerHeight
width: root.width
Canvas {
id: canvas
property double handleSize
property double darOffset : 0
anchors.fill: parent
contextType: "2d";
handleSize: root.baseUnit / 2
renderTarget: Canvas.FramebufferObject
renderStrategy: Canvas.Cooperative
onPaint:
{
var ctx = getContext('2d')
//if (context) {
ctx.clearRect(0,0, width, height);
ctx.beginPath()
ctx.strokeStyle = Qt.rgba(1, 0, 0, 0.5)
ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5)
ctx.lineWidth = 2
if (root.centerPoints.length == 0) {
// no points defined yet
return
}
var p1 = convertPoint(root.centerPoints[0])
var startP = p1;
ctx.moveTo(p1.x, p1.y)
if (!isDefined) {
ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize);
for (var i = 1; i < root.centerPoints.length; i++) {
p1 = convertPoint(root.centerPoints[i])
ctx.lineTo(p1.x, p1.y);
ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize);
}
} else {
var c1; var c2
for (var i = 0; i < root.centerPoints.length; i++) {
p1 = convertPoint(root.centerPoints[i])
// Control points
var subkf = false
if (i == 0) {
c1 = convertPoint(root.centerPointsTypes[root.centerPointsTypes.length - 1])
if (root.requestedSubKeyFrame == root.centerPointsTypes.length - 1) {
subkf = true
}
} else {
c1 = convertPoint(root.centerPointsTypes[2*i - 1])
if (root.requestedSubKeyFrame == 2*i - 1) {
subkf = true
}
}
c2 = convertPoint(root.centerPointsTypes[2*i])
ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, p1.x, p1.y);
if (iskeyframe) {
if (subkf) {
ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8)
ctx.fillRect(c1.x - handleSize/2, c1.y - handleSize/2, handleSize, handleSize);
ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5)
} else {
ctx.fillRect(c1.x - handleSize/2, c1.y - handleSize/2, handleSize, handleSize);
}
if (root.requestedSubKeyFrame == 2 * i) {
ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8)
ctx.fillRect(c2.x - handleSize/2, c2.y - handleSize/2, handleSize, handleSize);
ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5)
} else {
ctx.fillRect(c2.x - handleSize/2, c2.y - handleSize/2, handleSize, handleSize);
}
c1 = convertPoint(root.centerPointsTypes[2*i + 1])
ctx.lineTo(c1.x, c1.y);
ctx.moveTo(p1.x, p1.y)
ctx.lineTo(c2.x, c2.y);
ctx.moveTo(p1.x, p1.y)
if (i == root.requestedKeyFrame) {
ctx.fillStyle = Qt.rgba(1, 1, 0, 0.8)
ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize);
ctx.fillStyle = Qt.rgba(1, 0, 0, 0.5)
} else {
ctx.fillRect(p1.x - handleSize, p1.y - handleSize, 2 * handleSize, 2 * handleSize);
}
}
}
if (root.centerPoints.length > 2) {
- var c1 = convertPoint(root.centerPointsTypes[root.centerPointsTypes.length - 1])
- var c2 = convertPoint(root.centerPointsTypes[0])
+ c1 = convertPoint(root.centerPointsTypes[root.centerPointsTypes.length - 1])
+ c2 = convertPoint(root.centerPointsTypes[0])
ctx.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, startP.x, startP.y);
}
}
ctx.stroke()
}
function convertPoint(p)
{
var x = frame.x + p.x * root.scalex
var y = frame.y + p.y * root.scaley
return Qt.point(x,y);
}
}
Rectangle {
id: frame
objectName: "referenceframe"
property color hoverColor: "#ff0000"
width: root.profile.x * root.scalex
height: root.profile.y * root.scaley
x: root.center.x - width / 2 - root.offsetx;
y: root.center.y - height / 2 - root.offsety;
color: "transparent"
border.color: "#ffffff00"
}
Rectangle {
anchors.centerIn: parent
width: label.contentWidth + 6
height: label.contentHeight + 6
visible: !root.isDefined && !global.containsMouse
opacity: 0.8
Text {
id: label
text: i18n('Click to add points,\nright click to close shape.')
font.pointSize: root.baseUnit
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors {
fill: parent
}
color: 'black'
}
color: "yellow"
}
MouseArea {
id: global
objectName: "global"
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
property bool pointContainsMouse
hoverEnabled: true
cursorShape: !root.isDefined ? Qt.PointingHandCursor : pointContainsMouse ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: {
if (!root.isDefined) {
if (mouse.button == Qt.RightButton) {
// close shape, define control points
var p0; var p1; var p2
for (var i = 0; i < root.centerPoints.length; i++) {
p1 = root.centerPoints[i]
if (i == 0) {
p0 = root.centerPoints[root.centerPoints.length - 1]
} else {
p0 = root.centerPoints[i - 1]
}
if (i == root.centerPoints.length - 1) {
p2 = root.centerPoints[0]
} else {
p2 = root.centerPoints[i + 1]
}
var ctrl1 = Qt.point((p0.x - p1.x) / 5, (p0.y - p1.y) / 5);
var ctrl2 = Qt.point((p2.x - p1.x) / 5, (p2.y - p1.y) / 5);
root.centerPointsTypes.push(Qt.point(p1.x + ctrl1.x, p1.y + ctrl1.y))
root.centerPointsTypes.push(Qt.point(p1.x + ctrl2.x, p1.y + ctrl2.y))
}
root.isDefined = true;
root.effectPolygonChanged()
canvas.requestPaint()
} else {
var newPoint = Qt.point((mouseX - frame.x) / root.scalex, (mouseY - frame.y) / root.scaley);
root.centerPoints.push(newPoint)
canvas.requestPaint()
}
}
}
onDoubleClicked: {
root.addKeyframe()
}
onPositionChanged: {
if (root.iskeyframe == false) return;
if (isDefined && pressed) {
if (root.requestedKeyFrame >= 0) {
var xDiff = (mouseX - frame.x) / root.scalex - root.centerPoints[root.requestedKeyFrame].x
var yDiff = (mouseY - frame.y) / root.scaley - root.centerPoints[root.requestedKeyFrame].y
root.centerPoints[root.requestedKeyFrame].x += xDiff
root.centerPoints[root.requestedKeyFrame].y += yDiff
root.centerPointsTypes[root.requestedKeyFrame * 2].x += xDiff
root.centerPointsTypes[root.requestedKeyFrame * 2].y += yDiff
root.centerPointsTypes[root.requestedKeyFrame * 2 + 1].x += xDiff
root.centerPointsTypes[root.requestedKeyFrame * 2 + 1].y += yDiff
canvas.requestPaint()
root.effectPolygonChanged()
} else if (root.requestedSubKeyFrame >= 0) {
root.centerPointsTypes[root.requestedSubKeyFrame].x = (mouseX - frame.x) / root.scalex
root.centerPointsTypes[root.requestedSubKeyFrame].y = (mouseY - frame.y) / root.scaley
canvas.requestPaint()
root.effectPolygonChanged()
}
} else if (root.centerPoints.length > 0) {
for(var i = 0; i < root.centerPoints.length; i++)
{
var p1 = canvas.convertPoint(root.centerPoints[i])
if (Math.abs(p1.x - mouseX) <= canvas.handleSize && Math.abs(p1.y - mouseY) <= canvas.handleSize) {
if (i == root.requestedKeyFrame) {
pointContainsMouse = true;
return;
}
root.requestedKeyFrame = i
canvas.requestPaint()
pointContainsMouse = true;
return;
}
}
for(var i = 0; i < root.centerPointsTypes.length; i++)
{
var p1 = canvas.convertPoint(root.centerPointsTypes[i])
if (Math.abs(p1.x - mouseX) <= canvas.handleSize/2 && Math.abs(p1.y - mouseY) <= canvas.handleSize/2) {
if (i == root.requestedSubKeyFrame) {
pointContainsMouse = true;
return;
}
root.requestedSubKeyFrame = i
canvas.requestPaint()
pointContainsMouse = true;
return;
}
}
if (root.requestedKeyFrame == -1 && root.requestedSubKeyFrame == -1) {
return;
}
root.requestedKeyFrame = -1
root.requestedSubKeyFrame = -1
pointContainsMouse = false;
canvas.requestPaint()
}
}
}
}
EffectToolBar {
id: effectToolBar
anchors {
right: parent.right
top: parent.top
topMargin: 4
rightMargin: 4
}
visible: global.mouseX >= x - 10
}
MonitorRuler {
id: clipMonitorRuler
anchors {
left: root.left
right: root.right
bottom: root.bottom
}
height: controller.rulerHeight
}
}
diff --git a/src/timeline2/view/qml/Clip.qml b/src/timeline2/view/qml/Clip.qml
index 86b5b4af0..d2202210e 100644
--- a/src/timeline2/view/qml/Clip.qml
+++ b/src/timeline2/view/qml/Clip.qml
@@ -1,760 +1,760 @@
/*
* Copyright (c) 2013-2016 Meltytech, LLC
* Author: Dan Dennedy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import QtQuick 2.6
import QtQuick.Controls 2.2
import Kdenlive.Controls 1.0
import QtQml.Models 2.2
import QtQuick.Window 2.2
import 'Timeline.js' as Logic
import com.enums 1.0
Rectangle {
id: clipRoot
property real timeScale: 1.0
property string clipName: ''
property string clipResource: ''
property string mltService: ''
property string effectNames
property int modelStart
property real scrollX: 0
property int inPoint: 0
property int outPoint: 0
property int clipDuration: 0
property bool isAudio: false
property int audioChannels
property bool showKeyframes: false
property bool isGrabbed: false
property bool grouped: false
property var audioLevels
property var markers
property var keyframeModel
- property var clipStatus: 0
- property var itemType: 0
+ property int clipStatus: 0
+ property int itemType: 0
property int fadeIn: 0
property int fadeOut: 0
property int binId: 0
property var parentTrack
property int trackIndex //Index in track repeater
property int clipId //Id of the clip in the model
property int trackId: -1 // Id of the parent track in the model
property int fakeTid: -1
property int fakePosition: 0
property int originalTrackId: -1
property int originalX: x
property int originalDuration: clipDuration
property int lastValidDuration: clipDuration
property int draggedX: x
property bool selected: false
property bool isLocked: parentTrack && parentTrack.isLocked == true
property bool hasAudio
property bool canBeAudio
property bool canBeVideo
property string hash: 'ccc' //TODO
property double speed: 1.0
property color borderColor: 'black'
property bool forceReloadThumb: false
width : clipDuration * timeScale;
opacity: dragProxyArea.drag.active && dragProxy.draggedItem == clipId ? 0.8 : 1.0
signal trimmingIn(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedIn(var clip, bool shiftTrim)
signal trimmingOut(var clip, real newDuration, var mouse, bool shiftTrim)
signal trimmedOut(var clip, bool shiftTrim)
onIsGrabbedChanged: {
if (clipRoot.isGrabbed) {
clipRoot.forceActiveFocus();
mouseArea.focus = true
}
}
onInPointChanged: {
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload()
}
}
onClipResourceChanged: {
if (itemType == ProducerType.Color) {
color: Qt.darker(getColor())
}
}
ToolTip {
visible: mouseArea.containsMouse && !dragProxyArea.pressed
font.pixelSize: root.baseUnit
delay: 1000
timeout: 5000
background: Rectangle {
color: activePalette.alternateBase
border.color: activePalette.light
}
contentItem: Label {
color: activePalette.text
text: clipRoot.clipName + ' (' + timeline.timecode(clipRoot.inPoint) + '-' + timeline.timecode(clipRoot.outPoint) + ')'
}
}
onKeyframeModelChanged: {
console.log('keyframe model changed............')
if (effectRow.keyframecanvas) {
effectRow.keyframecanvas.requestPaint()
}
}
onClipDurationChanged: {
width = clipDuration * timeScale;
}
onModelStartChanged: {
x = modelStart * timeScale;
}
onFakePositionChanged: {
x = fakePosition * timeScale;
}
onFakeTidChanged: {
if (clipRoot.fakeTid > -1 && parentTrack) {
if (clipRoot.parent != dragContainer) {
var pos = clipRoot.mapToGlobal(clipRoot.x, clipRoot.y);
clipRoot.parent = dragContainer
pos = clipRoot.mapFromGlobal(pos.x, pos.y)
clipRoot.x = pos.x
clipRoot.y = pos.y
}
clipRoot.y = Logic.getTrackById(clipRoot.fakeTid).y
}
}
onForceReloadThumbChanged: {
// TODO: find a way to force reload of clip thumbs
if (thumbsLoader.item) {
thumbsLoader.item.reload()
}
}
onTimeScaleChanged: {
x = modelStart * timeScale;
width = clipDuration * timeScale;
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload();
}
}
onScrollXChanged: {
labelRect.x = scrollX > modelStart * timeScale ? scrollX - modelStart * timeScale : 0
}
border.color: selected? activePalette.highlight : grouped ? root.groupColor : borderColor
border.width: isGrabbed ? 8 : 1.5
function updateDrag() {
var itemPos = mapToItem(tracksContainerArea, 0, 0, clipRoot.width, clipRoot.height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
function getColor() {
if (clipStatus == ClipState.Disabled) {
return 'grey'
}
if (itemType == ProducerType.Color) {
var color = clipResource.substring(clipResource.length - 9)
if (color[0] == '#') {
return color
}
return '#' + color.substring(color.length - 8, color.length - 2)
}
return isAudio? root.audioColor : root.videoColor
}
/* function reparent(track) {
console.log('TrackId: ',trackId)
parent = track
height = track.height
parentTrack = track
trackId = parentTrack.trackId
console.log('Reparenting clip to Track: ', trackId)
//generateWaveform()
}
*/
property bool variableThumbs: (isAudio || itemType == ProducerType.Color || mltService === '')
property bool isImage: itemType == ProducerType.Image
property string baseThumbPath: variableThumbs ? '' : 'image://thumbnail/' + binId + '/' + (isImage ? '#0' : '#')
property string inThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(inPoint * speed)
property string outThumbPath: (variableThumbs || isImage ) ? baseThumbPath : baseThumbPath + Math.floor(outPoint * speed)
DropArea { //Drop area for clips
anchors.fill: clipRoot
keys: 'kdenlive/effect'
property string dropData
property string dropSource
property int dropRow: -1
onEntered: {
dropData = drag.getDataAsString('kdenlive/effect')
dropSource = drag.getDataAsString('kdenlive/effectsource')
}
onDropped: {
console.log("Add effect: ", dropData)
if (dropSource == '') {
// drop from effects list
controller.addClipEffect(clipRoot.clipId, dropData);
} else {
controller.copyClipEffect(clipRoot.clipId, dropSource);
}
dropSource = ''
dropRow = -1
drag.acceptProposedAction
}
}
onAudioLevelsChanged: {
if (parentTrack && parentTrack.isAudio) {
thumbsLoader.item.reload()
}
}
MouseArea {
id: mouseArea
visible: root.activeTool === 0
anchors.fill: clipRoot
acceptedButtons: Qt.RightButton
hoverEnabled: true
cursorShape: dragProxyArea.drag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
onPressed: {
root.stopScrolling = true
if (mouse.button == Qt.RightButton) {
if (timeline.selection.indexOf(clipRoot.clipId) == -1) {
timeline.addSelection(clipRoot.clipId, true)
}
clipMenu.clipId = clipRoot.clipId
clipMenu.clipStatus = clipRoot.clipStatus
clipMenu.clipFrame = Math.round(mouse.x / timeline.scaleFactor)
clipMenu.grouped = clipRoot.grouped
clipMenu.trackId = clipRoot.trackId
clipMenu.canBeAudio = clipRoot.canBeAudio
clipMenu.canBeVideo = clipRoot.canBeVideo
clipMenu.popup()
}
}
Keys.onShortcutOverride: event.accepted = clipRoot.isGrabbed && (event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Up || event.key === Qt.Key_Down)
Keys.onLeftPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart - 1, true, true, true);
}
Keys.onRightPressed: {
controller.requestClipMove(clipRoot.clipId, clipRoot.trackId, clipRoot.modelStart + 1, true, true, true);
}
Keys.onUpPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getNextTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
Keys.onDownPressed: {
controller.requestClipMove(clipRoot.clipId, controller.getPreviousTrackId(clipRoot.trackId), clipRoot.modelStart, true, true, true);
}
onPositionChanged: {
var mapped = parentTrack.mapFromItem(clipRoot, mouse.x, mouse.y).x
root.mousePosChanged(Math.round(mapped / timeline.scaleFactor))
}
onEntered: {
var itemPos = mapToItem(tracksContainerArea, 0, 0, width, height)
initDrag(clipRoot, itemPos, clipRoot.clipId, clipRoot.modelStart, clipRoot.trackId, false)
}
onExited: {
endDrag()
}
onWheel: zoomByWheel(wheel)
}
Item {
// Clipping container
id: container
anchors.fill: parent
anchors.margins:1.5
clip: true
Loader {
id: thumbsLoader
anchors.fill: parent
source: parentTrack.isAudio ? "ClipAudioThumbs.qml" : itemType == ProducerType.Color ? "" : "ClipThumbs.qml"
}
Rectangle {
// text background
id: labelRect
color: clipRoot.selected ? 'darkred' : '#66000000'
width: label.width + 2
height: label.height
visible: clipRoot.width > width / 2
Text {
id: label
text: clipName + (clipRoot.speed != 1.0 ? ' [' + Math.round(clipRoot.speed*100) + '%]': '')
font.pixelSize: root.baseUnit * 1.2
anchors {
top: labelRect.top
left: labelRect.left
topMargin: 1
leftMargin: 1
}
color: 'white'
style: Text.Outline
styleColor: 'black'
}
}
Rectangle {
// effects
id: effectsRect
color: '#555555'
width: effectLabel.width + 2
height: effectLabel.height
x: labelRect.x
anchors.top: labelRect.bottom
visible: labelRect.visible && clipRoot.effectNames != ''
Text {
id: effectLabel
text: clipRoot.effectNames
font.pixelSize: root.baseUnit * 1.2
anchors {
top: effectsRect.top
left: effectsRect.left
topMargin: 1
leftMargin: 1
// + ((isAudio || !settings.timelineShowThumbnails) ? 0 : inThumbnail.width) + 1
}
color: 'white'
//style: Text.Outline
styleColor: 'black'
}
}
Repeater {
model: markers
delegate:
Item {
anchors.fill: parent
Rectangle {
id: markerBase
width: 1
height: parent.height
x: (model.frame - clipRoot.inPoint) * timeScale;
color: model.color
}
Rectangle {
visible: mlabel.visible
opacity: 0.7
x: markerBase.x
radius: 2
width: mlabel.width + 4
height: mlabel.height
anchors {
bottom: parent.verticalCenter
}
color: model.color
MouseArea {
z: 10
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onDoubleClicked: timeline.editMarker(clipRoot.binId, model.frame)
onClicked: timeline.position = (clipRoot.x + markerBase.x) / timeline.scaleFactor
}
}
Text {
id: mlabel
visible: timeline.showMarkers && parent.width > width * 1.5
text: model.comment
font.pixelSize: root.baseUnit
x: markerBase.x
anchors {
bottom: parent.verticalCenter
topMargin: 2
leftMargin: 2
}
color: 'white'
}
}
}
KeyframeView {
id: effectRow
visible: clipRoot.showKeyframes && clipRoot.keyframeModel
selected: clipRoot.selected
inPoint: clipRoot.inPoint
outPoint: clipRoot.outPoint
masterObject: clipRoot
kfrModel: clipRoot.keyframeModel
}
}
states: [
State {
name: 'locked'
when: isLocked
PropertyChanges {
target: clipRoot
color: root.neutralColor
opacity: 0.8
z: 0
}
},
State {
name: 'normal'
when: clipRoot.selected === false
PropertyChanges {
target: clipRoot
color: getColor()
z: 0
}
},
State {
name: 'selected'
when: clipRoot.selected === true
PropertyChanges {
target: clipRoot
color: Qt.lighter(getColor(), 2)
z: 3
}
}
]
TimelineTriangle {
id: fadeInTriangle
fillColor: 'green'
width: Math.min(clipRoot.fadeIn * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.left: clipRoot.left
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
}
Rectangle {
id: fadeInControl
anchors.left: fadeInTriangle.width > radius? undefined : fadeInTriangle.left
anchors.horizontalCenter: fadeInTriangle.width > radius? fadeInTriangle.right : undefined
anchors.top: fadeInTriangle.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#FF66FFFF'
border.width: 2
border.color: 'green'
opacity: 0
Drag.active: fadeInMouseArea.drag.active
MouseArea {
id: fadeInMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
drag.minimumX: -root.baseUnit * 2
drag.maximumX: container.width
drag.axis: Drag.XAxis
property int startX
property int startFadeIn
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
drag.smoothed: false
onPressed: {
root.stopScrolling = true
startX = parent.x
startFadeIn = clipRoot.fadeIn
parent.anchors.left = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeInTriangle.opacity = 0.5
// parentTrack.clipSelected(clipRoot, parentTrack) TODO
}
onReleased: {
root.stopScrolling = false
fadeInTriangle.opacity = 0.3
parent.opacity = 0
if (fadeInTriangle.width > parent.radius)
parent.anchors.horizontalCenter = fadeInTriangle.right
else
parent.anchors.left = fadeInTriangle.left
console.log('released fade: ', clipRoot.fadeIn)
timeline.adjustFade(clipRoot.clipId, 'fadein', clipRoot.fadeIn, startFadeIn)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round((parent.x - startX) / timeScale)
if (delta != 0) {
var duration = Math.max(0, startFadeIn + delta)
duration = Math.min(duration, clipRoot.clipDuration)
if (clipRoot.fadeIn - 1 != duration) {
timeline.adjustFade(clipRoot.clipId, 'fadein', duration, -1)
}
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeInMouseArea.containsMouse && !fadeInMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
TimelineTriangle {
id: fadeOutCanvas
fillColor: 'red'
width: Math.min(clipRoot.fadeOut * timeScale, clipRoot.width)
height: clipRoot.height - clipRoot.border.width * 2
anchors.right: clipRoot.right
anchors.top: clipRoot.top
anchors.margins: clipRoot.border.width
opacity: 0.3
transform: Scale { xScale: -1; origin.x: fadeOutCanvas.width / 2}
}
Rectangle {
id: fadeOutControl
anchors.right: fadeOutCanvas.width > radius? undefined : fadeOutCanvas.right
anchors.horizontalCenter: fadeOutCanvas.width > radius? fadeOutCanvas.left : undefined
anchors.top: fadeOutCanvas.top
anchors.topMargin: -10
width: root.baseUnit * 2
height: width
radius: width / 2
color: '#66FFFFFF'
border.width: 2
border.color: 'red'
opacity: 0
Drag.active: fadeOutMouseArea.drag.active
MouseArea {
id: fadeOutMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.minimumX: -root.baseUnit * 2
drag.maximumX: container.width
property int startX
property int startFadeOut
onEntered: parent.opacity = 0.7
onExited: {
if (!pressed) {
parent.opacity = 0
}
}
drag.smoothed: false
onPressed: {
root.stopScrolling = true
startX = parent.x
startFadeOut = clipRoot.fadeOut
parent.anchors.right = undefined
parent.anchors.horizontalCenter = undefined
parent.opacity = 1
fadeOutCanvas.opacity = 0.5
}
onReleased: {
fadeOutCanvas.opacity = 0.3
parent.opacity = 0
root.stopScrolling = false
if (fadeOutCanvas.width > parent.radius)
parent.anchors.horizontalCenter = fadeOutCanvas.left
else
parent.anchors.right = fadeOutCanvas.right
timeline.adjustFade(clipRoot.clipId, 'fadeout', clipRoot.fadeOut, startFadeOut)
bubbleHelp.hide()
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round((startX - parent.x) / timeScale)
if (delta != 0) {
var duration = Math.max(0, startFadeOut + delta)
duration = Math.min(duration, clipRoot.clipDuration)
if (clipRoot.fadeOut - 1 != duration) {
timeline.adjustFade(clipRoot.clipId, 'fadeout', duration, -1)
}
// Show fade duration as time in a "bubble" help.
var s = timeline.timecode(Math.max(duration, 0))
bubbleHelp.show(clipRoot.x + clipRoot.width, parentTrack.y + clipRoot.height, s)
}
}
}
}
SequentialAnimation on scale {
loops: Animation.Infinite
running: fadeOutMouseArea.containsMouse && !fadeOutMouseArea.pressed
NumberAnimation {
from: 1.0
to: 0.7
duration: 250
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 0.7
to: 1.0
duration: 250
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
id: trimIn
anchors.left: clipRoot.left
anchors.leftMargin: 0
height: parent.height
width: 5
color: isAudio? 'green' : 'lawngreen'
opacity: 0
Drag.active: trimInMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimInMouseArea
anchors.fill: parent
hoverEnabled: true
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
onPressed: {
root.stopScrolling = true
clipRoot.originalX = clipRoot.x
clipRoot.originalDuration = clipDuration
parent.anchors.left = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.left = clipRoot.left
if (sizeChanged) {
clipRoot.trimmedIn(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var delta = Math.round((trimIn.x) / timeScale)
if (delta !== 0) {
if (delta < -modelStart) {
delta = -modelStart
}
var newDuration = clipDuration - delta
sizeChanged = true
clipRoot.trimmingIn(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: {
parent.opacity = 0
}
}
}
Rectangle {
id: trimOut
anchors.right: clipRoot.right
anchors.rightMargin: 0
height: parent.height
width: 5
color: 'red'
opacity: 0
Drag.active: trimOutMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
visible: root.activeTool === 0 && !mouseArea.drag.active
MouseArea {
id: trimOutMouseArea
anchors.fill: parent
hoverEnabled: true
property bool shiftTrim: false
property bool sizeChanged: false
cursorShape: (containsMouse ? Qt.SizeHorCursor : Qt.ClosedHandCursor);
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
root.stopScrolling = true
clipRoot.originalDuration = clipDuration
parent.anchors.right = undefined
shiftTrim = mouse.modifiers & Qt.ShiftModifier
parent.opacity = 0
}
onReleased: {
root.stopScrolling = false
parent.anchors.right = clipRoot.right
if (sizeChanged) {
clipRoot.trimmedOut(clipRoot, shiftTrim)
sizeChanged = false
}
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var newDuration = Math.round((parent.x + parent.width) / timeScale)
if (newDuration != clipDuration) {
sizeChanged = true
clipRoot.trimmingOut(clipRoot, newDuration, mouse, shiftTrim)
}
}
}
onEntered: {
if (!pressed) {
parent.opacity = 0.5
}
}
onExited: parent.opacity = 0
}
}
/*MenuItem {
id: mergeItem
text: i18n('Merge with next clip')
onTriggered: timeline.mergeClipWithNext(trackIndex, index, false)
}
MenuItem {
text: i18n('Rebuild Audio Waveform')
onTriggered: timeline.remakeAudioLevels(trackIndex, index)
}*/
/*onPopupVisibleChanged: {
if (visible && application.OS !== 'OS X' && __popupGeometry.height > 0) {
// Try to fix menu running off screen. This only works intermittently.
menu.__yOffset = Math.min(0, Screen.height - (__popupGeometry.y + __popupGeometry.height + 40))
menu.__xOffset = Math.min(0, Screen.width - (__popupGeometry.x + __popupGeometry.width))
}
}*/
}
diff --git a/src/timeline2/view/qml/Ruler.qml b/src/timeline2/view/qml/Ruler.qml
index ebf8fe361..3d24d18b6 100644
--- a/src/timeline2/view/qml/Ruler.qml
+++ b/src/timeline2/view/qml/Ruler.qml
@@ -1,298 +1,298 @@
/*
* Copyright (c) 2013 Meltytech, LLC
* Author: Dan Dennedy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Controls.Styles 1.4
Rectangle {
id: rulerRoot
// The standard width for labels. Depends on format used (frame number or full timecode)
property int labelSize: fontMetrics.tightBoundingRect(timeline.timecode(36000)).width
// The spacing between labels. Depends on labelSize
property real labelSpacing: labelSize
// The space we want between each ticks in the ruler
property real tickSpacing: timeline.scaleFactor
property real fontUnit: root.baseUnit * 0.9
property alias rulerZone : zone
property int workingPreview : timeline.workingPreview
property int labelMod: 1
property bool useTimelineRuler : timeline.useRuler
property bool showZoneLabels: false
function adjustStepSize() {
if (timeline.scaleFactor > 19) {
// Frame size >= 20 pixels
rulerRoot.tickSpacing = timeline.scaleFactor
// labelSpacing cannot be smaller than 1 frame
rulerRoot.labelSpacing = timeline.scaleFactor > rulerRoot.labelSize * 1.3 ? timeline.scaleFactor : Math.floor(rulerRoot.labelSize/timeline.scaleFactor) * timeline.scaleFactor
} else {
rulerRoot.tickSpacing = Math.floor(3 * rulerRoot.fontUnit / timeline.scaleFactor) * timeline.scaleFactor
rulerRoot.labelSpacing = (Math.floor(rulerRoot.labelSize/rulerRoot.tickSpacing) + 1) * rulerRoot.tickSpacing
}
- rulerRoot.labelMod = Math.max((1, Math.ceil((rulerRoot.labelSize + rulerRoot.fontUnit) / rulerRoot.tickSpacing)))
+ rulerRoot.labelMod = Math.max(1, Math.ceil((rulerRoot.labelSize + rulerRoot.fontUnit) / rulerRoot.tickSpacing))
//console.log('LABELMOD: ', Math.ceil((rulerRoot.labelSize + rulerRoot.fontUnit) / rulerRoot.tickSpacing)))
}
function adjustFormat() {
rulerRoot.labelSize = fontMetrics.tightBoundingRect(timeline.timecode(36000)).width
adjustStepSize()
repaintRuler()
}
function repaintRuler() {
// Enforce repaint
tickRepeater.model = 0
tickRepeater.model = scrollView.width / rulerRoot.tickSpacing + 2
}
color: root.color
clip: true
// Timeline preview stuff
Repeater {
model: timeline.dirtyChunks
anchors.fill: parent
delegate: Rectangle {
x: modelData * timeline.scaleFactor
y: 0
width: 25 * timeline.scaleFactor
height: parent.height / 4
color: 'darkred'
}
}
Repeater {
model: timeline.renderedChunks
anchors.fill: parent
delegate: Rectangle {
x: modelData * timeline.scaleFactor
y: 0
width: 25 * timeline.scaleFactor
height: parent.height / 4
color: 'darkgreen'
}
}
Rectangle {
id: working
x: rulerRoot.workingPreview * timeline.scaleFactor
y: 0
width: 25 * timeline.scaleFactor
height: parent.height / 4
color: 'orange'
visible: rulerRoot.workingPreview > -1
}
// Ruler marks
Repeater {
id: tickRepeater
model: scrollView.width / rulerRoot.tickSpacing + 2
property int offset: Math.floor(scrollView.flickableItem.contentX /rulerRoot.tickSpacing)
Item {
property int realPos: (tickRepeater.offset + index) * rulerRoot.tickSpacing / timeline.scaleFactor
x: realPos * timeline.scaleFactor
height: parent.height
property bool showText: (tickRepeater.offset + index)%rulerRoot.labelMod == 0
Rectangle {
anchors.bottom: parent.bottom
height: parent.showText ? 8 : 4
width: 1
color: activePalette.windowText
opacity: 0.5
}
Label {
visible: parent.showText
anchors.top: parent.top
anchors.topMargin: 2
text: timeline.timecode(parent.realPos)
font.pointSize: rulerRoot.fontUnit
color: activePalette.windowText
}
}
}
// monitor zone
Rectangle {
id: zone
visible: timeline.zoneOut > timeline.zoneIn
color: useTimelineRuler ? Qt.rgba(activePalette.highlight.r,activePalette.highlight.g,activePalette.highlight.b,0.5) :
Qt.rgba(activePalette.highlight.r,activePalette.highlight.g,activePalette.highlight.b,0.25)
x: timeline.zoneIn * timeline.scaleFactor
width: (timeline.zoneOut - timeline.zoneIn) * timeline.scaleFactor
anchors.bottom: parent.bottom
height: parent.height / 3
Rectangle {
id: centerDrag
anchors.centerIn: parent
height: parent.height
width: height
color: moveMouseArea.containsMouse || moveMouseArea.drag.active ? 'white' : 'transparent'
border.color: 'white'
border.width: 1.5
opacity: 0.5
Drag.active: moveMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: moveMouseArea
anchors.fill: parent
property double startX
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: zone
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
startX = zone.x
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var offset = Math.round(zone.x/ timeline.scaleFactor) - timeline.zoneIn
if (offset != 0) {
var newPos = Math.max(0, controller.suggestSnapPoint(timeline.zoneIn + offset,root.snapping))
timeline.zoneOut += newPos - timeline.zoneIn
timeline.zoneIn = newPos
}
}
}
}
}
// Zone frame indicator
Rectangle {
visible: trimInMouseArea.drag.active || trimInMouseArea.containsMouse
width: inLabel.contentWidth
height: inLabel.contentHeight
anchors.bottom: zone.top
color: activePalette.highlight
Label {
id: inLabel
anchors.fill: parent
text: timeline.timecode(timeline.zoneIn)
font.pointSize: rulerRoot.fontUnit
color: activePalette.highlightedText
}
}
Rectangle {
visible: trimOutMouseArea.drag.active || trimOutMouseArea.containsMouse
width: outLabel.contentWidth
height: outLabel.contentHeight
anchors.bottom: zone.top
color: activePalette.highlight
x: zone.width - outLabel.contentWidth
Label {
id: outLabel
anchors.fill: parent
text: timeline.timecode(timeline.zoneOut)
font.pointSize: rulerRoot.fontUnit
color: activePalette.highlightedText
}
}
Rectangle {
id: durationRect
anchors.bottom: zone.top
visible: (!useTimelineRuler && moveMouseArea.containsMouse) || ((useTimelineRuler || trimInMouseArea.drag.active || trimOutMouseArea.drag.active) && showZoneLabels && parent.width > 3 * width) || (useTimelineRuler && !trimInMouseArea.drag.active && !trimOutMouseArea.drag.active) || moveMouseArea.drag.active
anchors.horizontalCenter: parent.horizontalCenter
width: durationLabel.contentWidth + 4
height: durationLabel.contentHeight
color: activePalette.highlight
Label {
id: durationLabel
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
text: timeline.timecode(timeline.zoneOut - timeline.zoneIn)
font.pointSize: rulerRoot.fontUnit
color: activePalette.highlightedText
}
}
Rectangle {
id: trimIn
anchors.left: parent.left
anchors.leftMargin: 0
height: parent.height
width: 5
color: 'lawngreen'
opacity: 0
Drag.active: trimInMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: trimInMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
parent.anchors.left = undefined
parent.opacity = 1
}
onReleased: {
parent.anchors.left = zone.left
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
var newPos = controller.suggestSnapPoint(timeline.zoneIn + Math.round(trimIn.x / timeline.scaleFactor), root.snapping)
if (newPos < 0) {
newPos = 0
}
timeline.zoneIn = timeline.zoneOut > -1 ? Math.min(newPos, timeline.zoneOut - 1) : newPos
}
}
onEntered: parent.opacity = 1
onExited: parent.opacity = 0
}
}
Rectangle {
id: trimOut
anchors.right: parent.right
anchors.rightMargin: 0
height: parent.height
width: 5
color: 'darkred'
opacity: 0
Drag.active: trimOutMouseArea.drag.active
Drag.proposedAction: Qt.MoveAction
MouseArea {
id: trimOutMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.SizeHorCursor
drag.target: parent
drag.axis: Drag.XAxis
drag.smoothed: false
onPressed: {
parent.anchors.right = undefined
parent.opacity = 1
}
onReleased: {
parent.anchors.right = zone.right
}
onPositionChanged: {
if (mouse.buttons === Qt.LeftButton) {
timeline.zoneOut = Math.max(controller.suggestSnapPoint(timeline.zoneIn + Math.round((trimOut.x + trimOut.width) / timeline.scaleFactor), root.snapping), timeline.zoneIn + 1)
}
}
onEntered: parent.opacity = 1
onExited: parent.opacity = 0
}
}
}
}
diff --git a/src/timeline2/view/qml/Track.qml b/src/timeline2/view/qml/Track.qml
index 83a7fccc8..67f116eef 100644
--- a/src/timeline2/view/qml/Track.qml
+++ b/src/timeline2/view/qml/Track.qml
@@ -1,347 +1,347 @@
/*
* Copyright (c) 2013-2016 Meltytech, LLC
* Author: Dan Dennedy
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import QtQuick 2.6
import QtQml.Models 2.2
import com.enums 1.0
Column{
id: trackRoot
property alias trackModel: trackModel.model
property alias rootIndex : trackModel.rootIndex
property bool isAudio
property bool isMute
property bool isHidden
property real timeScale: 1.0
property bool isCurrentTrack: false
property bool isLocked: false
property int trackInternalId : -42
property int trackThumbsFormat
- property var itemType: 0
+ property int itemType: 0
height: parent.height
/*function redrawWaveforms() {
for (var i = 0; i < repeater.count; i++)
repeater.itemAt(i).generateWaveform()
}*/
function clipAt(index) {
return repeater.itemAt(index)
}
function isClip(type) {
return type != ProducerType.Composition && type != ProducerType.Track;
}
width: clipRow.width
DelegateModel {
id: trackModel
delegate: Item {
property var itemModel : model
z: model.clipType == ProducerType.Composition ? 5 : 0
Loader {
id: loader
Binding {
target: loader.item
property: "timeScale"
value: trackRoot.timeScale
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "fakeTid"
value: model.fakeTrackId
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "fakePosition"
value: model.fakePosition
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "selected"
value: loader.item ? root.timelineSelection.indexOf(loader.item.clipId) !== -1 : false
when: loader.status == Loader.Ready && model.clipType != ProducerType.Track
}
Binding {
target: loader.item
property: "mltService"
value: model.mlt_service
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "modelStart"
value: model.start
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "scrollX"
value: scrollView.flickableItem.contentX
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "fadeIn"
value: model.fadeIn
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "effectNames"
value: model.effectNames
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "clipStatus"
value: model.clipStatus
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "fadeOut"
value: model.fadeOut
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "audioLevels"
value: model.audioLevels
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "showKeyframes"
value: model.showKeyframes
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "isGrabbed"
value: model.isGrabbed
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "keyframeModel"
value: model.keyframeModel
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "aTrack"
value: model.a_track
when: loader.status == Loader.Ready && loader.item.clipType == ProducerType.Composition
}
Binding {
target: loader.item
property: "trackHeight"
value: root.trackHeight
when: loader.status == Loader.Ready && loader.item.clipType == ProducerType.Composition
}
Binding {
target: loader.item
property: "clipDuration"
value: model.duration
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "inPoint"
value: model.in
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "outPoint"
value: model.out
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "grouped"
value: model.grouped
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "clipName"
value: model.name
when: loader.status == Loader.Ready && loader.item
}
Binding {
target: loader.item
property: "clipResource"
value: model.resource
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "speed"
value: model.speed
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "forceReloadThumb"
value: model.reloadThumb
when: loader.status == Loader.Ready && isClip(model.clipType)
}
Binding {
target: loader.item
property: "binId"
value: model.binId
when: loader.status == Loader.Ready && isClip(model.clipType)
}
sourceComponent: {
if (isClip(model.clipType)) {
return clipDelegate
} else if (model.clipType == ProducerType.Composition) {
return compositionDelegate
} else {
// Track
return undefined
}
}
onLoaded: {
item.clipId= model.item
item.parentTrack = trackRoot
if (isClip(model.clipType)) {
console.log('loaded clip: ', model.start, ', ID: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex,', TYPE:', model.clipType)
item.isAudio= model.audio
item.markers= model.markers
item.hasAudio = model.hasAudio
item.canBeAudio = model.canBeAudio
item.canBeVideo = model.canBeVideo
item.itemType = model.clipType
item.audioChannels = model.audioChannels
//item.binId= model.binId
} else if (model.clipType == ProducerType.Composition) {
console.log('loaded composition: ', model.start, ', ID: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex)
//item.aTrack = model.a_track
} else {
console.log('loaded unwanted element: ', model.item, ', index: ', trackRoot.DelegateModel.itemsIndex)
}
item.trackId = model.trackId
//item.selected= trackRoot.selection.indexOf(item.clipId) !== -1
//console.log(width, height);
}
}
}
}
Item {
id: clipRow
height: trackRoot.height
Repeater { id: repeater; model: trackModel }
}
Component {
id: clipDelegate
Clip {
height: trackRoot.height
onTrimmingIn: {
var new_duration = controller.requestItemResize(clip.clipId, newDuration, false, false, root.snapping, shiftTrim)
if (new_duration > 0) {
clip.lastValidDuration = new_duration
clip.originalX = clip.draggedX
// Show amount trimmed as a time in a "bubble" help.
var delta = new_duration - clip.originalDuration
var s = timeline.timecode(Math.abs(delta))
s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '')
.arg(s)
.arg(timeline.timecode(clipDuration))
bubbleHelp.show(clip.x - 20, trackRoot.y + trackRoot.height, s)
}
}
onTrimmedIn: {
bubbleHelp.hide()
controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, root.snapping, shiftTrim)
controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, root.snapping, shiftTrim)
}
onTrimmingOut: {
var new_duration = controller.requestItemResize(clip.clipId, newDuration, true, false, root.snapping, shiftTrim)
if (new_duration > 0) {
clip.lastValidDuration = new_duration
// Show amount trimmed as a time in a "bubble" help.
var delta = clip.originalDuration - new_duration
var s = timeline.timecode(Math.abs(delta))
s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '')
.arg(s)
.arg(timeline.timecode(new_duration))
bubbleHelp.show(clip.x + clip.width - 20, trackRoot.y + trackRoot.height, s)
}
}
onTrimmedOut: {
bubbleHelp.hide()
controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, root.snapping, shiftTrim)
controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, root.snapping, shiftTrim)
}
}
}
Component {
id: compositionDelegate
Composition {
displayHeight: trackRoot.height / 2
opacity: 0.8
selected: root.timelineSelection.indexOf(clipId) !== -1
onTrimmingIn: {
var new_duration = controller.requestItemResize(clip.clipId, newDuration, false, false, root.snapping)
if (new_duration > 0) {
clip.lastValidDuration = newDuration
clip.originalX = clip.draggedX
// Show amount trimmed as a time in a "bubble" help.
var delta = newDuration - clip.originalDuration
var s = timeline.timecode(Math.abs(delta))
s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '')
.arg(s)
.arg(timeline.timecode(clipDuration))
bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s)
}
}
onTrimmedIn: {
bubbleHelp.hide()
controller.requestItemResize(clip.clipId, clip.originalDuration, false, false, root.snapping)
controller.requestItemResize(clip.clipId, clip.lastValidDuration, false, true, root.snapping)
}
onTrimmingOut: {
var new_duration = controller.requestItemResize(clip.clipId, newDuration, true, false, root.snapping)
if (new_duration > 0) {
clip.lastValidDuration = newDuration
// Show amount trimmed as a time in a "bubble" help.
var delta = newDuration - clip.originalDuration
var s = timeline.timecode(Math.abs(delta))
s = '%1%2 = %3'.arg((delta < 0)? '+' : (delta > 0)? '-' : '')
.arg(s)
.arg(timeline.timecode(clipDuration))
bubbleHelp.show(clip.x + clip.width, trackRoot.y + trackRoot.height, s)
}
}
onTrimmedOut: {
bubbleHelp.hide()
controller.requestItemResize(clip.clipId, clip.originalDuration, true, false, root.snapping)
controller.requestItemResize(clip.clipId, clip.lastValidDuration, true, true, root.snapping)
}
}
}
}