1236 lines
54 KiB
C++
1236 lines
54 KiB
C++
/*
|
|
* Copyright (c) 2016-2026 Meltytech, LLC
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "keyframesdock.h"
|
|
|
|
#include "Logger.h"
|
|
#include "actions.h"
|
|
#include "mainwindow.h"
|
|
#include "qmltypes/qmlproducer.h"
|
|
#include "qmltypes/qmlutilities.h"
|
|
#include "qmltypes/qmlview.h"
|
|
#include "settings.h"
|
|
#include "widgets/docktoolbar.h"
|
|
|
|
#include <QAction>
|
|
#include <QActionGroup>
|
|
#include <QDir>
|
|
#include <QIcon>
|
|
#include <QMenu>
|
|
#include <QQmlContext>
|
|
#include <QQmlEngine>
|
|
#include <QQuickItem>
|
|
#include <QSlider>
|
|
#include <QToolButton>
|
|
#include <QUrl>
|
|
#include <QVBoxLayout>
|
|
|
|
#include <cmath>
|
|
|
|
static QmlMetadata m_emptyQmlMetadata;
|
|
static QmlFilter m_emptyQmlFilter;
|
|
|
|
KeyframesDock::KeyframesDock(QmlProducer *qmlProducer, QWidget *parent)
|
|
: QDockWidget(tr("Keyframes"), parent)
|
|
, m_qview(QmlUtilities::sharedEngine(), this)
|
|
, m_qmlProducer(qmlProducer)
|
|
{
|
|
LOG_DEBUG() << "begin";
|
|
setObjectName("KeyframesDock");
|
|
QIcon icon = QIcon::fromTheme("chronometer",
|
|
QIcon(":/icons/oxygen/32x32/actions/chronometer.png"));
|
|
toggleViewAction()->setIcon(icon);
|
|
setMinimumSize(200, 50);
|
|
setWhatsThis("https://forum.shotcut.org/t/about-keyframes/12957/1");
|
|
|
|
setupActions();
|
|
|
|
m_mainMenu = new QMenu(tr("Keyframes"), this);
|
|
m_mainMenu->addAction(Actions["keyframesTrimInAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesTrimOutAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesAnimateInAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesAnimateOutAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesScrubDragAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesToggleKeyframeAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesSeekPreviousAction"]);
|
|
m_mainMenu->addAction(Actions["keyframesSeekNextAction"]);
|
|
QMenu *viewMenu = new QMenu(tr("View"), this);
|
|
viewMenu->addAction(Actions["keyframesZoomOutAction"]);
|
|
viewMenu->addAction(Actions["keyframesZoomInAction"]);
|
|
viewMenu->addAction(Actions["keyframesZoomFitAction"]);
|
|
m_mainMenu->addMenu(viewMenu);
|
|
Actions.loadFromMenu(m_mainMenu);
|
|
|
|
m_keyMenu = new QMenu(tr("Keyframe"), this);
|
|
m_keyTypePrevMenu = new QMenu(tr("From Previous"), this);
|
|
m_keyTypePrevMenu->addAction(Actions["keyframesTypePrevHoldAction"]);
|
|
m_keyTypePrevMenu->addAction(Actions["keyframesTypePrevLinearAction"]);
|
|
m_keyTypePrevMenu->addAction(Actions["keyframesTypePrevSmoothNaturalAction"]);
|
|
|
|
QMenu *keyEaseOutMenu = new QMenu(tr("Ease Out"), this);
|
|
icon = QIcon::fromTheme("keyframe-ease-out",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-ease-out.png"));
|
|
keyEaseOutMenu->setIcon(icon);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutSinuAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutQuadAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutCubeAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutQuartAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutQuintAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutExpoAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutCircAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutBackAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutElasAction"]);
|
|
keyEaseOutMenu->addAction(Actions["keyframesTypePrevEaseOutBounAction"]);
|
|
m_keyTypePrevMenu->addMenu(keyEaseOutMenu);
|
|
m_keyMenu->addMenu(m_keyTypePrevMenu);
|
|
m_keyTypeNextMenu = new QMenu(tr("To Next"), this);
|
|
m_keyTypeNextMenu->addAction(Actions["keyframesTypeHoldAction"]);
|
|
m_keyTypeNextMenu->addAction(Actions["keyframesTypeLinearAction"]);
|
|
m_keyTypeNextMenu->addAction(Actions["keyframesTypeSmoothNaturalAction"]);
|
|
|
|
QMenu *keyEaseInMenu = new QMenu(tr("Ease In"), this);
|
|
icon = QIcon::fromTheme("keyframe-ease-in",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-ease-in.png"));
|
|
keyEaseInMenu->setIcon(icon);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInSinuAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInQuadAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInCubeAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInQuartAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInQuintAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInExpoAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInCircAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInBackAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInElasAction"]);
|
|
keyEaseInMenu->addAction(Actions["keyframesTypeEaseInBounAction"]);
|
|
m_keyTypeNextMenu->addMenu(keyEaseInMenu);
|
|
|
|
QMenu *keyEaseInOutMenu = new QMenu(tr("Ease In/Out"), this);
|
|
icon = QIcon::fromTheme("keyframe-ease-inout",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-ease-inout.png"));
|
|
keyEaseInOutMenu->setIcon(icon);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutSinuAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutQuadAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutCubeAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutQuartAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutQuintAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutExpoAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutCircAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutBackAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutElasAction"]);
|
|
keyEaseInOutMenu->addAction(Actions["keyframesTypeEaseInOutBounAction"]);
|
|
m_keyTypeNextMenu->addMenu(keyEaseInOutMenu);
|
|
m_keyMenu->addMenu(m_keyTypeNextMenu);
|
|
m_keyMenu->addAction(Actions["keyframesRemoveAction"]);
|
|
Actions.loadFromMenu(m_keyMenu);
|
|
|
|
m_clipMenu = new QMenu(tr("Keyframes Clip"), this);
|
|
m_clipMenu->addAction(Actions["keyframesRebuildAudioWaveformAction"]);
|
|
Actions.loadFromMenu(m_clipMenu);
|
|
|
|
QVBoxLayout *vboxLayout = new QVBoxLayout();
|
|
vboxLayout->setSpacing(0);
|
|
vboxLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
DockToolBar *toolbar = new DockToolBar(tr("Keyframes Controls"));
|
|
QToolButton *menuButton = new QToolButton();
|
|
menuButton->setIcon(
|
|
QIcon::fromTheme("show-menu", QIcon(":/icons/oxygen/32x32/actions/show-menu.png")));
|
|
menuButton->setToolTip(tr("Keyframes Menu"));
|
|
menuButton->setAutoRaise(true);
|
|
menuButton->setPopupMode(QToolButton::QToolButton::InstantPopup);
|
|
menuButton->setMenu(m_mainMenu);
|
|
toolbar->addWidget(menuButton);
|
|
toolbar->addSeparator();
|
|
toolbar->addAction(Actions["keyframesTrimInAction"]);
|
|
toolbar->addAction(Actions["keyframesTrimOutAction"]);
|
|
toolbar->addAction(Actions["keyframesAnimateInAction"]);
|
|
toolbar->addAction(Actions["keyframesAnimateOutAction"]);
|
|
toolbar->addSeparator();
|
|
toolbar->addAction(Actions["timelineSnapAction"]);
|
|
toolbar->addAction(Actions["keyframesScrubDragAction"]);
|
|
toolbar->addSeparator();
|
|
toolbar->addAction(Actions["keyframesZoomOutAction"]);
|
|
QSlider *zoomSlider = new QSlider();
|
|
zoomSlider->setOrientation(Qt::Horizontal);
|
|
zoomSlider->setMaximumWidth(200);
|
|
zoomSlider->setMinimum(0);
|
|
zoomSlider->setMaximum(300);
|
|
zoomSlider->setValue(100);
|
|
zoomSlider->setTracking(false);
|
|
connect(zoomSlider, &QSlider::valueChanged, this, [&](int value) {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
emit setZoom(value / 100.0);
|
|
});
|
|
connect(this, &KeyframesDock::timeScaleChanged, zoomSlider, [=]() {
|
|
double value = round(pow(m_timeScale - 0.01, 1.0 / 3.0) * 100.0);
|
|
zoomSlider->setValue(value);
|
|
});
|
|
toolbar->addWidget(zoomSlider);
|
|
toolbar->addAction(Actions["keyframesZoomInAction"]);
|
|
toolbar->addAction(Actions["keyframesZoomFitAction"]);
|
|
|
|
vboxLayout->setMenuBar(toolbar);
|
|
|
|
m_qview.setFocusPolicy(Qt::StrongFocus);
|
|
m_qview.quickWindow()->setPersistentSceneGraph(false);
|
|
#ifndef Q_OS_MAC
|
|
m_qview.setAttribute(Qt::WA_AcceptTouchEvents);
|
|
#endif
|
|
setWidget(&m_qview);
|
|
|
|
QmlUtilities::setCommonProperties(m_qview.rootContext());
|
|
m_qview.rootContext()->setContextProperty("keyframes", this);
|
|
m_qview.rootContext()->setContextProperty("view", new QmlView(&m_qview));
|
|
m_qview.rootContext()->setContextProperty("parameters", &m_model);
|
|
m_qview.setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
m_qview.setClearColor(palette().window().color());
|
|
m_qview.quickWindow()->setPersistentSceneGraph(false);
|
|
#ifndef Q_OS_MAC
|
|
m_qview.setAttribute(Qt::WA_AcceptTouchEvents);
|
|
#endif
|
|
setCurrentFilter(0, 0);
|
|
connect(this, SIGNAL(visibilityChanged(bool)), this, SLOT(load(bool)));
|
|
|
|
vboxLayout->addWidget(&m_qview);
|
|
QWidget *dockContentsWidget = new QWidget();
|
|
dockContentsWidget->setLayout(vboxLayout);
|
|
QDockWidget::setWidget(dockContentsWidget);
|
|
|
|
LOG_DEBUG() << "end";
|
|
}
|
|
|
|
void KeyframesDock::setupActions()
|
|
{
|
|
QIcon icon;
|
|
QAction *action;
|
|
|
|
action = new QAction(tr("Set Filter Start"), this);
|
|
action->setShortcut(QKeySequence(Qt::Key_BracketLeft));
|
|
action->setWhatsThis("https://forum.shotcut.org/t/trimming-filters/13212/1");
|
|
icon = QIcon::fromTheme("keyframes-filter-in",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframes-filter-in.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter && m_filter->allowTrim()) {
|
|
int i = m_qmlProducer->position() + m_qmlProducer->in();
|
|
m_model.trimFilterIn(i);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = false;
|
|
if (m_filter && m_filter->allowTrim())
|
|
enabled = true;
|
|
action->setEnabled(enabled);
|
|
});
|
|
Actions.add("keyframesTrimInAction", action);
|
|
|
|
action = new QAction(tr("Set Filter End"), this);
|
|
action->setShortcut(QKeySequence(Qt::Key_BracketRight));
|
|
action->setWhatsThis("https://forum.shotcut.org/t/trimming-filters/13212/1");
|
|
icon = QIcon::fromTheme("keyframes-filter-out",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframes-filter-out.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter && m_filter->allowTrim()) {
|
|
int i = m_qmlProducer->position() + m_qmlProducer->in();
|
|
m_model.trimFilterOut(i);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = false;
|
|
if (m_filter && m_filter->allowTrim())
|
|
enabled = true;
|
|
action->setEnabled(enabled);
|
|
});
|
|
Actions.add("keyframesTrimOutAction", action);
|
|
|
|
action = new QAction(tr("Set First Simple Keyframe"), this);
|
|
action->setShortcut(QKeySequence(Qt::Key_BraceLeft));
|
|
action->setWhatsThis("https://forum.shotcut.org/t/simple-keyframes/43639/1");
|
|
icon = QIcon::fromTheme("keyframes-simple-in",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframes-simple-in.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter && m_filter->allowAnimateIn()) {
|
|
int i = m_qmlProducer->position() + m_qmlProducer->in() - m_filter->in();
|
|
m_filter->setAnimateIn(i);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = false;
|
|
if (m_filter && m_filter->allowAnimateIn())
|
|
enabled = true;
|
|
action->setEnabled(enabled);
|
|
});
|
|
Actions.add("keyframesAnimateInAction", action);
|
|
|
|
action = new QAction(tr("Set Second Simple Keyframe"), this);
|
|
action->setShortcut(QKeySequence(Qt::Key_BraceRight));
|
|
action->setWhatsThis("https://forum.shotcut.org/t/simple-keyframes/43639/1");
|
|
icon = QIcon::fromTheme("keyframes-simple-out",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframes-simple-out.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter && m_filter->allowAnimateOut()) {
|
|
int i = m_filter->out() - (m_qmlProducer->position() + m_qmlProducer->in());
|
|
m_filter->setAnimateOut(i);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = false;
|
|
if (m_filter && m_filter->allowAnimateOut())
|
|
enabled = true;
|
|
action->setEnabled(enabled);
|
|
});
|
|
Actions.add("keyframesAnimateOutAction", action);
|
|
|
|
action = new QAction(tr("Scrub While Dragging"), this);
|
|
icon = QIcon::fromTheme("scrub_drag", QIcon(":/icons/oxygen/32x32/actions/scrub_drag.png"));
|
|
action->setIcon(icon);
|
|
action->setCheckable(true);
|
|
action->setChecked(Settings.keyframesDragScrub());
|
|
connect(action, &QAction::triggered, this, [&](bool checked) {
|
|
Settings.setKeyframesDragScrub(checked);
|
|
});
|
|
connect(&Settings, &ShotcutSettings::keyframesDragScrubChanged, action, [=]() {
|
|
action->setChecked(Settings.keyframesDragScrub());
|
|
});
|
|
Actions.add("keyframesScrubDragAction", action);
|
|
|
|
action = new QAction(tr("Zoom Keyframes Out"), this);
|
|
action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Minus));
|
|
icon = QIcon::fromTheme("zoom-out", QIcon(":/icons/oxygen/32x32/actions/zoom-out.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
emit zoomOut();
|
|
});
|
|
Actions.add("keyframesZoomOutAction", action);
|
|
|
|
action = new QAction(tr("Zoom Keyframes In"), this);
|
|
action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_Plus));
|
|
icon = QIcon::fromTheme("zoom-in", QIcon(":/icons/oxygen/32x32/actions/zoom-in.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
emit zoomIn();
|
|
});
|
|
Actions.add("keyframesZoomInAction", action);
|
|
|
|
action = new QAction(tr("Zoom Keyframes To Fit"), this);
|
|
action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_0));
|
|
icon = QIcon::fromTheme("zoom-fit-best",
|
|
QIcon(":/icons/oxygen/32x32/actions/zoom-fit-best.png"));
|
|
action->setIcon(icon);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
emit zoomToFit();
|
|
});
|
|
Actions.add("keyframesZoomFitAction", action);
|
|
|
|
// Actions to modify previous keyframes
|
|
QActionGroup *keyframeTypePrevActionGroup = new QActionGroup(this);
|
|
keyframeTypePrevActionGroup->setExclusive(true);
|
|
|
|
action = new QAction(tr("Hold"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::DiscreteInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-hold",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-hold.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevHoldAction", action);
|
|
|
|
action = new QAction(tr("Linear"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::LinearInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-linear",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-linear.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevLinearAction", action);
|
|
|
|
action = new QAction(tr("Smooth"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::SmoothNaturalInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-smooth",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-smooth.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevSmoothNaturalAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Sinusoidal"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutSinusoidal);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-sinu",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-sinu.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutSinuAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Quadratic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutQuadratic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-quad",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-quad.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutQuadAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Cubic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutCubic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-cube",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-cube.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutCubeAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Quartic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutQuartic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-quar",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-quar.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutQuartAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Quintic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutQuintic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-quin",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-quin.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutQuintAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Exponential"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutExponential);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-expo",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-expo.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutExpoAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Circular"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutCircular);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-circ",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-circ.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutCircAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Back"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutBack);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-back",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-back.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutBackAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Elastic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutElastic);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-elas",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-elas.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutElasAction", action);
|
|
|
|
action = new QAction(tr("Ease Out Bounce"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt() - 1,
|
|
KeyframesModel::EaseOutBounce);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-out-boun",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-out-boun.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypePrevActionGroup->addAction(action);
|
|
Actions.add("keyframesTypePrevEaseOutBounAction", action);
|
|
|
|
// Actions to modify selected keyframes
|
|
QActionGroup *keyframeTypeActionGroup = new QActionGroup(this);
|
|
keyframeTypeActionGroup->setExclusive(true);
|
|
|
|
action = new QAction(tr("Hold"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::DiscreteInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-hold",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-hold.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeHoldAction", action);
|
|
|
|
action = new QAction(tr("Linear"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::LinearInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-linear",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-linear.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeLinearAction", action);
|
|
|
|
action = new QAction(tr("Smooth"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::SmoothNaturalInterpolation);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("keyframe-smooth",
|
|
QIcon(":/icons/oxygen/32x32/actions/keyframe-smooth.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeSmoothNaturalAction", action);
|
|
|
|
action = new QAction(tr("Ease In Sinusoidal"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInSinusoidal);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-sinu", QIcon(":/icons/oxygen/32x32/actions/ease-in-sinu.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInSinuAction", action);
|
|
|
|
action = new QAction(tr("Ease In Quadratic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInQuadratic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-quad", QIcon(":/icons/oxygen/32x32/actions/ease-in-quad.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInQuadAction", action);
|
|
|
|
action = new QAction(tr("Ease In Cubic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInCubic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-cube", QIcon(":/icons/oxygen/32x32/actions/ease-in-cube.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInCubeAction", action);
|
|
|
|
action = new QAction(tr("Ease In Quartic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInQuartic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-quar", QIcon(":/icons/oxygen/32x32/actions/ease-in-quar.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInQuartAction", action);
|
|
|
|
action = new QAction(tr("Ease In Quintic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInQuintic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-quin", QIcon(":/icons/oxygen/32x32/actions/ease-in-quin.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInQuintAction", action);
|
|
|
|
action = new QAction(tr("Ease In Exponential"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInExponential);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-expo", QIcon(":/icons/oxygen/32x32/actions/ease-in-expo.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInExpoAction", action);
|
|
|
|
action = new QAction(tr("Ease In Circular"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInCircular);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-circ", QIcon(":/icons/oxygen/32x32/actions/ease-in-circ.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInCircAction", action);
|
|
|
|
action = new QAction(tr("Ease In Back"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInBack);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-back", QIcon(":/icons/oxygen/32x32/actions/ease-in-back.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInBackAction", action);
|
|
|
|
action = new QAction(tr("Ease In Elastic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInElastic);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-elas", QIcon(":/icons/oxygen/32x32/actions/ease-in-elas.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInElasAction", action);
|
|
|
|
action = new QAction(tr("Ease In Bounce"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInBounce);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-in-boun", QIcon(":/icons/oxygen/32x32/actions/ease-in-boun.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInBounAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Sinusoidal"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutSinusoidal);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-sinu",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-sinu.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutSinuAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Quadratic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutQuadratic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-quad",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-quad.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutQuadAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Cubic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutCubic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-cube",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-cube.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutCubeAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Quartic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutQuartic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-quar",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-quar.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutQuartAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Quintic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutQuintic);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-quin",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-quin.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutQuintAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Exponential"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutExponential);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-expo",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-expo.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutExpoAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Circular"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutCircular);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-circ",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-circ.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutCircAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Back"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutBack);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-back",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-back.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutBackAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Elastic"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutElastic);
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
bool enabled = true;
|
|
if (m_metadata && m_metadata->keyframes() && !m_metadata->keyframes()->allowOvershoot()) {
|
|
enabled = false;
|
|
}
|
|
action->setVisible(enabled);
|
|
action->setEnabled(enabled);
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-elas",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-elas.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutElasAction", action);
|
|
|
|
action = new QAction(tr("Ease In/Out Bounce"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.setInterpolation(currentTrack,
|
|
keyframeIndex.toInt(),
|
|
KeyframesModel::EaseInOutBounce);
|
|
}
|
|
});
|
|
icon = QIcon::fromTheme("ease-inout-boun",
|
|
QIcon(":/icons/oxygen/32x32/actions/ease-inout-boun.png"));
|
|
action->setIcon(icon);
|
|
keyframeTypeActionGroup->addAction(action);
|
|
Actions.add("keyframesTypeEaseInOutBounAction", action);
|
|
|
|
action = new QAction(tr("Remove"), this);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (!isVisible() || !m_qview.rootObject())
|
|
return;
|
|
int currentTrack = m_qview.rootObject()->property("currentTrack").toInt();
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
m_model.remove(currentTrack, keyframeIndex.toInt());
|
|
}
|
|
});
|
|
Actions.add("keyframesRemoveAction", action);
|
|
|
|
action = new QAction(tr("Rebuild Audio Waveform"), this);
|
|
action->setEnabled(Settings.timelineShowWaveforms());
|
|
connect(action, &QAction::triggered, this, [&](bool checked) {
|
|
if (m_qmlProducer && Settings.timelineShowWaveforms()) {
|
|
m_qmlProducer->remakeAudioLevels();
|
|
}
|
|
});
|
|
connect(&Settings, &ShotcutSettings::timelineShowWaveformsChanged, action, [=]() {
|
|
action->setEnabled(Settings.timelineShowWaveforms());
|
|
});
|
|
Actions.add("keyframesRebuildAudioWaveformAction", action);
|
|
|
|
action = new QAction(tr("Seek Previous Keyframe"), this);
|
|
action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_BracketLeft));
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter) {
|
|
if (m_model.advancedKeyframesInUse())
|
|
seekPrevious();
|
|
else
|
|
emit seekPreviousSimple();
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
});
|
|
Actions.add("keyframesSeekPreviousAction", action);
|
|
|
|
action = new QAction(tr("Seek Next Keyframe"), this);
|
|
action->setShortcut(QKeySequence(Qt::ALT | Qt::Key_BracketRight));
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter) {
|
|
if (m_model.advancedKeyframesInUse())
|
|
seekNext();
|
|
else
|
|
emit seekNextSimple();
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
});
|
|
Actions.add("keyframesSeekNextAction", action);
|
|
|
|
action = new QAction(tr("Toggle Keyframe At Playhead"), this);
|
|
action->setShortcut(QKeySequence(Qt::Key_Semicolon));
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
connect(action, &QAction::triggered, this, [&]() {
|
|
if (m_qmlProducer && m_filter && currentParameter() >= 0) {
|
|
auto position = m_qmlProducer->position() - (m_filter->in() - m_qmlProducer->in());
|
|
auto parameterIndex = currentParameter();
|
|
if (m_model.isKeyframe(parameterIndex, position)) {
|
|
auto keyframeIndex = m_model.keyframeIndex(parameterIndex, position);
|
|
m_model.remove(parameterIndex, keyframeIndex);
|
|
} else {
|
|
m_model.addKeyframe(parameterIndex, position);
|
|
}
|
|
}
|
|
});
|
|
connect(this, &KeyframesDock::newFilter, action, [=]() {
|
|
action->setEnabled(m_qmlProducer && m_filter);
|
|
});
|
|
Actions.add("keyframesToggleKeyframeAction", action);
|
|
}
|
|
|
|
int KeyframesDock::seekPrevious()
|
|
{
|
|
if (m_qmlProducer) {
|
|
int position = m_model.previousKeyframePosition(currentParameter(),
|
|
m_qmlProducer->position()
|
|
+ m_qmlProducer->in());
|
|
position -= m_qmlProducer->in();
|
|
m_qmlProducer->setPosition(position);
|
|
return m_model.keyframeIndex(currentParameter(),
|
|
position + m_qmlProducer->in() - m_filter->in());
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int KeyframesDock::seekNext()
|
|
{
|
|
if (m_qmlProducer) {
|
|
int position = m_model.nextKeyframePosition(currentParameter(),
|
|
m_qmlProducer->position() + m_qmlProducer->in());
|
|
position -= m_qmlProducer->in();
|
|
if (position > m_qmlProducer->position())
|
|
m_qmlProducer->setPosition(position);
|
|
else
|
|
position = m_qmlProducer->position();
|
|
return m_model.keyframeIndex(currentParameter(),
|
|
position + m_qmlProducer->in() - m_filter->in());
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void KeyframesDock::setCurrentFilter(QmlFilter *filter, QmlMetadata *meta)
|
|
{
|
|
m_filter = filter;
|
|
m_metadata = meta;
|
|
if (!m_filter || !m_filter->producer().is_valid()) {
|
|
m_filter = &m_emptyQmlFilter;
|
|
m_metadata = &m_emptyQmlMetadata;
|
|
}
|
|
m_model.load(m_filter, m_metadata);
|
|
disconnect(this, SIGNAL(changed()));
|
|
connect(m_filter, SIGNAL(changed(QString)), SIGNAL(changed()));
|
|
connect(m_filter, SIGNAL(changed(QString)), &m_model, SLOT(onFilterChanged(QString)));
|
|
connect(m_filter, SIGNAL(animateInChanged()), &m_model, SLOT(reload()));
|
|
connect(m_filter, SIGNAL(animateOutChanged()), &m_model, SLOT(reload()));
|
|
connect(m_filter, SIGNAL(inChanged(int)), &m_model, SLOT(onFilterInChanged(int)));
|
|
emit newFilter();
|
|
}
|
|
|
|
bool KeyframesDock::event(QEvent *event)
|
|
{
|
|
bool result = QDockWidget::event(event);
|
|
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
|
|
load(true);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void KeyframesDock::keyPressEvent(QKeyEvent *event)
|
|
{
|
|
QDockWidget::keyPressEvent(event);
|
|
if (!event->isAccepted())
|
|
MAIN.keyPressEvent(event);
|
|
}
|
|
|
|
void KeyframesDock::keyReleaseEvent(QKeyEvent *event)
|
|
{
|
|
QDockWidget::keyReleaseEvent(event);
|
|
if (!event->isAccepted())
|
|
MAIN.keyReleaseEvent(event);
|
|
}
|
|
|
|
int KeyframesDock::currentParameter() const
|
|
{
|
|
if (!m_qview.rootObject())
|
|
return 0;
|
|
return m_qview.rootObject()->property("currentTrack").toInt();
|
|
}
|
|
|
|
void KeyframesDock::setTimeScale(double value)
|
|
{
|
|
m_timeScale = value;
|
|
emit timeScaleChanged();
|
|
}
|
|
|
|
void KeyframesDock::load(bool force)
|
|
{
|
|
LOG_DEBUG() << "begin" << m_qview.source().isEmpty() << force;
|
|
|
|
if (m_qview.source().isEmpty() || force) {
|
|
QDir viewPath = QmlUtilities::qmlDir();
|
|
viewPath.cd("views");
|
|
viewPath.cd("keyframes");
|
|
m_qview.engine()->addImportPath(viewPath.path());
|
|
|
|
QDir modulePath = QmlUtilities::qmlDir();
|
|
modulePath.cd("modules");
|
|
m_qview.engine()->addImportPath(modulePath.path());
|
|
|
|
m_qview.setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
m_qview.quickWindow()->setColor(palette().window().color());
|
|
QUrl source = QUrl::fromLocalFile(viewPath.absoluteFilePath("keyframes.qml"));
|
|
m_qview.setSource(source);
|
|
connect(m_qview.rootObject(), SIGNAL(rightClicked()), this, SLOT(onDockRightClicked()));
|
|
connect(m_qview.rootObject(),
|
|
SIGNAL(keyframeRightClicked()),
|
|
this,
|
|
SLOT(onKeyframeRightClicked()));
|
|
connect(m_qview.rootObject(), SIGNAL(clipRightClicked()), this, SLOT(onClipRightClicked()));
|
|
emit timeScaleChanged();
|
|
}
|
|
}
|
|
|
|
void KeyframesDock::reload()
|
|
{
|
|
load(true);
|
|
}
|
|
|
|
void KeyframesDock::onProducerModified()
|
|
{
|
|
// The clip name may have changed.
|
|
if (m_qmlProducer)
|
|
emit m_qmlProducer->producerChanged();
|
|
}
|
|
|
|
void KeyframesDock::onDockRightClicked()
|
|
{
|
|
m_mainMenu->popup(QCursor::pos());
|
|
}
|
|
|
|
void KeyframesDock::onKeyframeRightClicked()
|
|
{
|
|
if (!m_qview.rootObject())
|
|
return;
|
|
bool firstKey = false;
|
|
bool lastKey = false;
|
|
for (auto keyframeIndex : m_qview.rootObject()->property("selection").toList()) {
|
|
int keyIndex = keyframeIndex.toInt();
|
|
if (keyIndex == 0) {
|
|
firstKey = true;
|
|
}
|
|
if (keyIndex >= m_model.keyframeCount(currentParameter()) - 1) {
|
|
lastKey = true;
|
|
}
|
|
}
|
|
m_keyTypePrevMenu->setEnabled(!firstKey);
|
|
m_keyTypeNextMenu->setEnabled(!lastKey);
|
|
m_keyMenu->popup(QCursor::pos());
|
|
}
|
|
|
|
void KeyframesDock::onClipRightClicked()
|
|
{
|
|
m_clipMenu->popup(QCursor::pos());
|
|
}
|