/*
 * 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;

/*!
    \qmltype KeyframesDock
    \inqmlmodule org.shotcut.qml
    \brief The Keyframes panel controller, available as the \c keyframes context property.

    \c keyframes is injected into the Keyframes QML view and exposes navigation
    methods and the time-scale zoom state.
*/

/*!
    \qmlsignal KeyframesDock::changed()
    \brief Emitted when a filter parameter value changes from within the Keyframes panel.
*/

/*!
    \qmlsignal KeyframesDock::newFilter()
    \brief Emitted when the filter itself (not just a parameter) has been changed.
*/

/*!
    \qmlsignal KeyframesDock::timeScaleChanged()
    \brief Emitted when \l timeScale changes.
*/

/*!
    \qmlsignal KeyframesDock::seekPreviousSimple()
    \brief Emitted to request seeking to the previous simple-keyframe boundary.
*/

/*!
    \qmlsignal KeyframesDock::seekNextSimple()
    \brief Emitted to request seeking to the next simple-keyframe boundary.
*/

/*!
    \qmlsignal KeyframesDock::zoomIn()
    \brief Emitted to request zooming in on the keyframes ruler.
*/

/*!
    \qmlsignal KeyframesDock::zoomOut()
    \brief Emitted to request zooming out on the keyframes ruler.
*/

/*!
    \qmlsignal KeyframesDock::zoomToFit()
    \brief Emitted to request resetting zoom to show all keyframes.
*/

/*!
    \qmlsignal KeyframesDock::dockClicked()
    \brief Emitted when the user clicks inside the Keyframes dock area.
*/

/*!
    \qmlproperty real KeyframesDock::timeScale
    \brief The horizontal zoom factor of the Keyframes ruler. Settable.
*/

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);
}

/*!
    \qmlmethod int KeyframesDock::seekPrevious()
    \brief Seeks the playhead to the previous keyframe position.
    Returns the new playhead position in frames.
*/

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;
}

/*!
    \qmlmethod int KeyframesDock::seekNext()
    \brief Seeks the playhead to the next keyframe position.
    Returns the new playhead position in frames.
*/

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();
}

/*!
    \qmlmethod void KeyframesDock::load(bool force)
    \brief Loads (or reloads if \a force is \c true) the Keyframes panel for the current filter.
*/

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();
    }
}

/*!
    \qmlmethod void KeyframesDock::reload()
    \brief Reloads the keyframes model for the current filter without resetting the view.
*/

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());
}
