clippropertiescontroller.cpp 68.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
/*
Copyright (C) 2015  Jean-Baptiste Mardelle <jb@kdenlive.org>
This file is part of Kdenlive. See www.kdenlive.org.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of
the License or (at your option) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
Laurent Montel's avatar
Laurent Montel committed
10
by the membership of KDE e.V.), which shall act as a proxy
11 12 13 14 15 16 17 18 19 20 21 22
defined in Section 14 of version 3 of the license.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "clippropertiescontroller.h"
23
#include "bin/model/markerlistmodel.hpp"
24
#include "clipcontroller.h"
25
#include "core.h"
26
#include "dialogs/profilesdialog.h"
27
#include "doc/kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
28
#include "kdenlivesettings.h"
29
#include "profiles/profilerepository.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
30
#include "project/projectmanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
31
#include "timecodedisplay.h"
32
#include <audio/audioStreamInfo.h>
33
#include "widgets/choosecolorwidget.h"
34

35
#include <KDualAction>
36
#include <KLocalizedString>
37 38

#ifdef KF5_USE_FILEMETADATA
39
#include <KFileMetaData/ExtractionResult>
Nicolas Carion's avatar
Nicolas Carion committed
40
#include <KFileMetaData/Extractor>
41
#include <KFileMetaData/ExtractorCollection>
Nicolas Carion's avatar
Nicolas Carion committed
42
#include <KFileMetaData/PropertyInfo>
43 44
#endif

45
#include <KIO/Global>
46

Laurent Montel's avatar
Laurent Montel committed
47
#include "kdenlive_debug.h"
48
#include <KMessageBox>
Nicolas Carion's avatar
Nicolas Carion committed
49
#include <QCheckBox>
50 51 52
#include <QClipboard>
#include <QComboBox>
#include <QDesktopServices>
53
#include <QDoubleSpinBox>
54
#include <QFile>
55
#include <QFileDialog>
Nicolas Carion's avatar
Nicolas Carion committed
56 57 58
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QLabel>
59
#include <QMenu>
60
#include <QMimeData>
Nicolas Carion's avatar
Nicolas Carion committed
61 62
#include <QMimeDatabase>
#include <QProcess>
63
#include <QScrollArea>
64
#include <QTextEdit>
Nicolas Carion's avatar
Nicolas Carion committed
65 66
#include <QToolBar>
#include <QUrl>
67
#include <QListWidgetItem>
Nicolas Carion's avatar
Nicolas Carion committed
68
#include <QVBoxLayout>
69

70 71
AnalysisTree::AnalysisTree(QWidget *parent)
    : QTreeWidget(parent)
72 73 74 75 76 77 78 79
{
    setRootIsDecorated(false);
    setColumnCount(2);
    setAlternatingRowColors(true);
    setHeaderHidden(true);
    setDragEnabled(true);
}

Nicolas Carion's avatar
Nicolas Carion committed
80
// virtual
Laurent Montel's avatar
Laurent Montel committed
81
QMimeData *AnalysisTree::mimeData(const QList<QTreeWidgetItem *> list) const
82
{
Vincent Pinon's avatar
Vincent Pinon committed
83
    QString mimeData;
84
    for (QTreeWidgetItem *item : list) {
85
        if ((item->flags() & Qt::ItemIsDragEnabled) != 0) {
Vincent Pinon's avatar
Vincent Pinon committed
86
            mimeData.append(item->text(1));
87 88
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
89
    auto *mime = new QMimeData;
Vincent Pinon's avatar
Vincent Pinon committed
90
    mime->setData(QStringLiteral("kdenlive/geometry"), mimeData.toUtf8());
91 92
    return mime;
}
93

94
#ifdef KF5_USE_FILEMETADATA
95 96
class ExtractionResult : public KFileMetaData::ExtractionResult
{
Laurent Montel's avatar
Laurent Montel committed
97 98
public:
    ExtractionResult(const QString &filename, const QString &mimetype, QTreeWidget *tree)
99 100
        : KFileMetaData::ExtractionResult(filename, mimetype, KFileMetaData::ExtractionResult::ExtractMetaData)
        , m_tree(tree)
Nicolas Carion's avatar
Nicolas Carion committed
101 102
    {
    }
Laurent Montel's avatar
Laurent Montel committed
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137

    void append(const QString & /*text*/) override {}

    void addType(KFileMetaData::Type::Type /*type*/) override {}

    void add(KFileMetaData::Property::Property property, const QVariant &value) override
    {
        bool decode = false;
        switch (property) {
        case KFileMetaData::Property::ImageMake:
        case KFileMetaData::Property::ImageModel:
        case KFileMetaData::Property::ImageDateTime:
        case KFileMetaData::Property::BitRate:
        case KFileMetaData::Property::TrackNumber:
        case KFileMetaData::Property::ReleaseYear:
        case KFileMetaData::Property::Composer:
        case KFileMetaData::Property::Genre:
        case KFileMetaData::Property::Artist:
        case KFileMetaData::Property::Album:
        case KFileMetaData::Property::Title:
        case KFileMetaData::Property::Comment:
        case KFileMetaData::Property::Copyright:
        case KFileMetaData::Property::PhotoFocalLength:
        case KFileMetaData::Property::PhotoExposureTime:
        case KFileMetaData::Property::PhotoFNumber:
        case KFileMetaData::Property::PhotoApertureValue:
        case KFileMetaData::Property::PhotoWhiteBalance:
        case KFileMetaData::Property::PhotoGpsLatitude:
        case KFileMetaData::Property::PhotoGpsLongitude:
            decode = true;
            break;
        default:
            break;
        }
        if (decode) {
138 139 140 141
            KFileMetaData::PropertyInfo info(property);
            if (info.valueType() == QVariant::DateTime) {
                new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toDateTime().toString(Qt::DefaultLocaleShortDate));
            } else if (info.valueType() == QVariant::Int) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
142 143 144
                int val = value.toInt();
                if (property == KFileMetaData::Property::BitRate) {
                    // Adjust unit for bitrate
Nicolas Carion's avatar
Nicolas Carion committed
145 146
                    new QTreeWidgetItem(m_tree, QStringList() << info.displayName()
                                                              << QString::number(val / 1000) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s"));
Laurent Montel's avatar
Laurent Montel committed
147
                } else {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
148 149
                    new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val));
                }
150
            } else if (info.valueType() == QVariant::Double) {
Laurent Montel's avatar
Laurent Montel committed
151 152 153
                new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(value.toDouble()));
            } else {
                new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toString());
154
            }
Laurent Montel's avatar
Laurent Montel committed
155 156
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
157

158 159 160
private:
    QTreeWidget *m_tree;
};
161
#endif
162

163
ClipPropertiesController::ClipPropertiesController(ClipController *controller, QWidget *parent)
164 165
    : QWidget(parent)
    , m_controller(controller)
166
    , m_tc(Timecode(Timecode::HH_MM_SS_HH, pCore->getCurrentFps()))
167
    , m_id(controller->binId())
168
    , m_type(controller->clipType())
169
    , m_properties(new Mlt::Properties(controller->properties()))
170
    , m_audioStream(nullptr)
171
    , m_textEdit(nullptr)
172
    , m_audioStreamsView(nullptr)
173
{
174
    m_controller->mirrorOriginalProperties(m_sourceProperties);
175
    setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
176
    setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
Nicolas Carion's avatar
Nicolas Carion committed
177
    auto *lay = new QVBoxLayout;
Laurent Montel's avatar
Laurent Montel committed
178
    lay->setContentsMargins(0, 0, 0, 0);
179 180 181 182 183 184 185
    m_clipLabel = new QLabel(controller->clipName());
    lay->addWidget(m_clipLabel);
    m_tabWidget = new QTabWidget(this);
    lay->addWidget(m_tabWidget);
    setLayout(lay);
    m_tabWidget->setDocumentMode(true);
    m_tabWidget->setTabPosition(QTabWidget::East);
Nicolas Carion's avatar
Nicolas Carion committed
186
    auto *forcePage = new QScrollArea(this);
187
    auto *forceAudioPage = new QScrollArea(this);
188 189
    m_propertiesPage = new QWidget(this);
    m_markersPage = new QWidget(this);
190
    m_metaPage = new QWidget(this);
191
    m_analysisPage = new QWidget(this);
192 193

    // Clip properties
Nicolas Carion's avatar
Nicolas Carion committed
194
    auto *propsBox = new QVBoxLayout;
195 196 197 198 199 200 201 202
    m_propertiesTree = new QTreeWidget(this);
    m_propertiesTree->setRootIsDecorated(false);
    m_propertiesTree->setColumnCount(2);
    m_propertiesTree->setAlternatingRowColors(true);
    m_propertiesTree->sortByColumn(0, Qt::AscendingOrder);
    m_propertiesTree->setHeaderHidden(true);
    propsBox->addWidget(m_propertiesTree);
    fillProperties();
203 204 205
    m_propertiesPage->setLayout(propsBox);

    // Clip markers
Nicolas Carion's avatar
Nicolas Carion committed
206
    auto *mBox = new QVBoxLayout;
207
    m_markerTree = new QTreeView;
208 209 210
    m_markerTree->setRootIsDecorated(false);
    m_markerTree->setAlternatingRowColors(true);
    m_markerTree->setHeaderHidden(true);
211
    m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
212
    m_markerTree->setModel(controller->getMarkerModel().get());
213
    m_markerTree->setObjectName("markers_list");
214
    mBox->addWidget(m_markerTree);
Nicolas Carion's avatar
Nicolas Carion committed
215
    auto *bar = new QToolBar;
216 217 218 219 220
    bar->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker()));
    bar->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker()));
    bar->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker()));
    bar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers()));
    bar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers()));
221
    mBox->addWidget(bar);
222

223
    m_markersPage->setLayout(mBox);
Laurent Montel's avatar
Laurent Montel committed
224
    connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotSeekToMarker);
225 226

    // metadata
Nicolas Carion's avatar
Nicolas Carion committed
227 228
    auto *m2Box = new QVBoxLayout;
    auto *metaTree = new QTreeWidget;
229 230 231 232 233 234
    metaTree->setRootIsDecorated(true);
    metaTree->setColumnCount(2);
    metaTree->setAlternatingRowColors(true);
    metaTree->setHeaderHidden(true);
    m2Box->addWidget(metaTree);
    slotFillMeta(metaTree);
Laurent Montel's avatar
Laurent Montel committed
235
    m_metaPage->setLayout(m2Box);
236 237

    // Clip analysis
Nicolas Carion's avatar
Nicolas Carion committed
238
    auto *aBox = new QVBoxLayout;
239
    m_analysisTree = new AnalysisTree(this);
Nicolas Carion's avatar
Nicolas Carion committed
240 241
    aBox->addWidget(new QLabel(i18n("Analysis data")));
    aBox->addWidget(m_analysisTree);
Nicolas Carion's avatar
Nicolas Carion committed
242
    auto *bar2 = new QToolBar;
243 244 245
    bar2->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis()));
    bar2->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis()));
    bar2->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis()));
246 247 248
    aBox->addWidget(bar2);

    slotFillAnalysisData();
Laurent Montel's avatar
Laurent Montel committed
249
    m_analysisPage->setLayout(aBox);
250

251
    // Force properties
Nicolas Carion's avatar
Nicolas Carion committed
252
    auto *vbox = new QVBoxLayout;
253
    vbox->setSpacing(0);
254 255 256 257 258
    
    // Force Audio properties
    auto *audioVbox = new QVBoxLayout;
    audioVbox->setSpacing(0);

259
    if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) {
260
        QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this);
Laurent Montel's avatar
Laurent Montel committed
261
        connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip);
262 263
        vbox->addWidget(editButton);
    }
264
    if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) {
265
        // Edit duration widget
266
        m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out"));
267
        int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
Laurent Montel's avatar
Laurent Montel committed
268
        if (kdenlive_length > 0) {
269
            m_originalProperties.insert(QStringLiteral("kdenlive:duration"), m_properties->get("kdenlive:duration"));
Laurent Montel's avatar
Laurent Montel committed
270
        }
271
        m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length"));
Nicolas Carion's avatar
Nicolas Carion committed
272
        auto *hlay = new QHBoxLayout;
273
        QCheckBox *box = new QCheckBox(i18n("Duration"), this);
274
        box->setObjectName(QStringLiteral("force_duration"));
275
        hlay->addWidget(box);
276
        auto *timePos = new TimecodeDisplay(m_tc, this);
277
        timePos->setObjectName(QStringLiteral("force_duration_value"));
278 279
        timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties->get_int("length"));
        int original_length = m_properties->get_int("kdenlive:original_length");
280 281
        if (original_length > 0) {
            box->setChecked(true);
Laurent Montel's avatar
Laurent Montel committed
282 283
        } else {
            timePos->setEnabled(false);
284 285 286
        }
        hlay->addWidget(timePos);
        vbox->addLayout(hlay);
Laurent Montel's avatar
Laurent Montel committed
287 288 289 290
        connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled);
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
        connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged);
        connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat);
291
        connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int)));
292
        // connect(this, static_cast<void(ClipPropertiesController::*)(int)>(&ClipPropertiesController::modified), timePos, &TimecodeDisplay::setValue);
293
    }
294
    if (m_type == ClipType::TextTemplate) {
295
        // Edit text widget
296
        QString currentText = m_properties->get("templatetext");
297
        m_originalProperties.insert(QStringLiteral("templatetext"), currentText);
298
        m_textEdit = new QTextEdit(this);
299
        m_textEdit->setAcceptRichText(false);
300 301
        m_textEdit->setPlainText(currentText);
        m_textEdit->setPlaceholderText(i18n("Enter template text here"));
302 303 304 305
        vbox->addWidget(m_textEdit);
        QPushButton *button = new QPushButton(i18n("Apply"), this);
        vbox->addWidget(button);
        connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged);
306
    } else if (m_type == ClipType::Color) {
307
        // Edit color widget
308 309
        m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
        mlt_color color = m_properties->get_color("resource");
310
        ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this);
311
        vbox->addWidget(choosecolor);
Nicolas Carion's avatar
Nicolas Carion committed
312
        // connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int)));
Laurent Montel's avatar
Laurent Montel committed
313
        connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified);
314 315
        connect(this, static_cast<void (ClipPropertiesController::*)(const QColor &)>(&ClipPropertiesController::modified), choosecolor,
                &ChooseColorWidget::slotColorModified);
316
    }
317
    if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) {
318
        // Aspect ratio
319 320
        int force_ar_num = m_properties->get_int("force_aspect_num");
        int force_ar_den = m_properties->get_int("force_aspect_den");
321 322
        m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den));
        m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num));
Nicolas Carion's avatar
Nicolas Carion committed
323
        auto *hlay = new QHBoxLayout;
324
        QCheckBox *box = new QCheckBox(i18n("Aspect ratio"), this);
325
        box->setObjectName(QStringLiteral("force_ar"));
326
        vbox->addWidget(box);
Laurent Montel's avatar
Laurent Montel committed
327
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
Nicolas Carion's avatar
Nicolas Carion committed
328
        auto *spin1 = new QSpinBox(this);
329
        spin1->setMaximum(8000);
330
        spin1->setObjectName(QStringLiteral("force_aspect_num_value"));
331
        hlay->addWidget(spin1);
332
        hlay->addWidget(new QLabel(QStringLiteral(":")));
Nicolas Carion's avatar
Nicolas Carion committed
333
        auto *spin2 = new QSpinBox(this);
334 335
        spin2->setMinimum(1);
        spin2->setMaximum(8000);
336
        spin2->setObjectName(QStringLiteral("force_aspect_den_value"));
337 338
        hlay->addWidget(spin2);
        if (force_ar_num == 0) {
339
            // use current ratio
340 341
            int num = m_properties->get_int("meta.media.sample_aspect_num");
            int den = m_properties->get_int("meta.media.sample_aspect_den");
342 343 344 345
            if (den == 0) {
                num = 1;
                den = 1;
            }
346 347
            spin1->setEnabled(false);
            spin2->setEnabled(false);
348 349
            spin1->setValue(num);
            spin2->setValue(den);
Laurent Montel's avatar
Laurent Montel committed
350
        } else {
351 352 353 354 355 356
            box->setChecked(true);
            spin1->setEnabled(true);
            spin2->setEnabled(true);
            spin1->setValue(force_ar_num);
            spin2->setValue(force_ar_den);
        }
357 358
        connect(spin2, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
        connect(spin1, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
Laurent Montel's avatar
Laurent Montel committed
359 360
        connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled);
        connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled);
361
        vbox->addLayout(hlay);
362 363

        // Proxy
364
        QString proxy = m_properties->get("kdenlive:proxy");
365 366
        m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy);
        hlay = new QHBoxLayout;
Nicolas Carion's avatar
Nicolas Carion committed
367
        auto *bg = new QGroupBox(this);
368 369
        bg->setCheckable(false);
        bg->setFlat(true);
Nicolas Carion's avatar
Nicolas Carion committed
370
        auto *groupLay = new QHBoxLayout;
371 372
        groupLay->setContentsMargins(0, 0, 0, 0);
        auto *pbox = new QCheckBox(i18n("Proxy clip"), this);
373 374 375
        pbox->setTristate(true);
        // Proxy codec label
        QLabel *lab = new QLabel(this);
376
        pbox->setObjectName(QStringLiteral("kdenlive:proxy"));
377 378 379
        bool hasProxy = proxy.length() > 2;
        if (hasProxy) {
            bg->setToolTip(proxy);
380
            bool proxyReady = (QFileInfo(proxy).fileName() == QFileInfo(m_properties->get("resource")).fileName());
381 382
            if (proxyReady) {
                pbox->setCheckState(Qt::Checked);
383
                lab->setText(m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData()));
384 385 386 387 388 389
            } else {
                pbox->setCheckState(Qt::PartiallyChecked);
            }
        } else {
            pbox->setCheckState(Qt::Unchecked);
        }
390
        pbox->setEnabled(pCore->projectManager()->current()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0);
Nicolas Carion's avatar
Nicolas Carion committed
391
        connect(pbox, &QCheckBox::stateChanged, [this, pbox](int state) {
392 393 394 395 396
            emit requestProxy(state == Qt::PartiallyChecked);
            if (state == Qt::Checked) {
                QSignalBlocker bk(pbox);
                pbox->setCheckState(Qt::Unchecked);
            }
397
        });
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
398
        connect(this, &ClipPropertiesController::enableProxy, pbox, &QCheckBox::setEnabled);
399
        connect(this, &ClipPropertiesController::proxyModified, [this, pbox, bg, lab](const QString &pxy) {
400
            bool hasProxyClip = pxy.length() > 2;
401
            QSignalBlocker bk(pbox);
402
            pbox->setCheckState(hasProxyClip ? Qt::Checked : Qt::Unchecked);
403
            bg->setEnabled(pbox->isChecked());
404
            bg->setToolTip(pxy);
405 406
            lab->setText(hasProxyClip ? m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData())
                                      : QString());
407 408
        });
        hlay->addWidget(pbox);
409 410 411 412
        bg->setEnabled(pbox->checkState() == Qt::Checked);

        groupLay->addWidget(lab);

413
        // Delete button
Nicolas Carion's avatar
Nicolas Carion committed
414
        auto *tb = new QToolButton(this);
415 416
        tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
        tb->setAutoRaise(true);
417
        connect(tb, &QToolButton::clicked, [this, proxy]() { emit deleteProxy(); });
418 419 420 421
        tb->setToolTip(i18n("Delete proxy file"));
        groupLay->addWidget(tb);
        // Folder button
        tb = new QToolButton(this);
Nicolas Carion's avatar
Nicolas Carion committed
422
        auto *pMenu = new QMenu(this);
423 424 425
        tb->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
        tb->setToolTip(i18n("Proxy options"));
        tb->setMenu(pMenu);
426
        tb->setAutoRaise(true);
427 428 429
        tb->setPopupMode(QToolButton::InstantPopup);

        QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open folder"), this);
430
        connect(ac, &QAction::triggered, [this]() {
431
            QString pxy = m_properties->get("kdenlive:proxy");
432 433
            QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(pxy).path()));
        });
434 435
        pMenu->addAction(ac);
        ac = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play proxy clip"), this);
436
        connect(ac, &QAction::triggered, [this]() {
437
            QString pxy = m_properties->get("kdenlive:proxy");
438 439
            QDesktopServices::openUrl(QUrl::fromLocalFile(pxy));
        });
440 441
        pMenu->addAction(ac);
        ac = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy file location to clipboard"), this);
442
        connect(ac, &QAction::triggered, [this]() {
443
            QString pxy = m_properties->get("kdenlive:proxy");
444 445
            QGuiApplication::clipboard()->setText(pxy);
        });
446
        pMenu->addAction(ac);
447 448 449 450
        groupLay->addWidget(tb);
        bg->setLayout(groupLay);
        hlay->addWidget(bg);
        vbox->addLayout(hlay);
Laurent Montel's avatar
Laurent Montel committed
451
    }
452

453
    if (m_type == ClipType::AV || m_type == ClipType::Video) {
454
        QLocale locale;
455
        locale.setNumberOptions(QLocale::OmitGroupSeparator);
456 457

        // Fps
458
        QString force_fps = m_properties->get("force_fps");
459
        m_originalProperties.insert(QStringLiteral("force_fps"), force_fps.isEmpty() ? QStringLiteral("-") : force_fps);
Nicolas Carion's avatar
Nicolas Carion committed
460
        auto *hlay = new QHBoxLayout;
461
        QCheckBox *box = new QCheckBox(i18n("Frame rate"), this);
462
        box->setObjectName(QStringLiteral("force_fps"));
Laurent Montel's avatar
Laurent Montel committed
463
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
Nicolas Carion's avatar
Nicolas Carion committed
464
        auto *spin = new QDoubleSpinBox(this);
465 466
        spin->setMaximum(1000);
        connect(spin, SIGNAL(valueChanged(double)), this, SLOT(slotValueChanged(double)));
467
        // connect(spin, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, &ClipPropertiesController::slotValueChanged);
468
        spin->setObjectName(QStringLiteral("force_fps_value"));
469 470
        if (force_fps.isEmpty()) {
            spin->setValue(controller->originalFps());
Laurent Montel's avatar
Laurent Montel committed
471
        } else {
472 473
            spin->setValue(locale.toDouble(force_fps));
        }
Laurent Montel's avatar
Laurent Montel committed
474
        connect(box, &QAbstractButton::toggled, spin, &QWidget::setEnabled);
475 476 477 478 479 480
        box->setChecked(!force_fps.isEmpty());
        spin->setEnabled(!force_fps.isEmpty());
        hlay->addWidget(box);
        hlay->addWidget(spin);
        vbox->addLayout(hlay);

481
        // Scanning
482
        QString force_prog = m_properties->get("force_progressive");
483
        m_originalProperties.insert(QStringLiteral("force_progressive"), force_prog.isEmpty() ? QStringLiteral("-") : force_prog);
484 485
        hlay = new QHBoxLayout;
        box = new QCheckBox(i18n("Scanning"), this);
Laurent Montel's avatar
Laurent Montel committed
486
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
487
        box->setObjectName(QStringLiteral("force_progressive"));
Nicolas Carion's avatar
Nicolas Carion committed
488
        auto *combo = new QComboBox(this);
489 490
        combo->addItem(i18n("Interlaced"), 0);
        combo->addItem(i18n("Progressive"), 1);
491
        connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
492
        combo->setObjectName(QStringLiteral("force_progressive_value"));
493 494 495
        if (!force_prog.isEmpty()) {
            combo->setCurrentIndex(force_prog.toInt());
        }
Laurent Montel's avatar
Laurent Montel committed
496
        connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
497 498 499 500 501
        box->setChecked(!force_prog.isEmpty());
        combo->setEnabled(!force_prog.isEmpty());
        hlay->addWidget(box);
        hlay->addWidget(combo);
        vbox->addLayout(hlay);
502

503
        // Field order
504
        QString force_tff = m_properties->get("force_tff");
505
        m_originalProperties.insert(QStringLiteral("force_tff"), force_tff.isEmpty() ? QStringLiteral("-") : force_tff);
506 507
        hlay = new QHBoxLayout;
        box = new QCheckBox(i18n("Field order"), this);
Laurent Montel's avatar
Laurent Montel committed
508
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
509
        box->setObjectName(QStringLiteral("force_tff"));
510 511 512
        combo = new QComboBox(this);
        combo->addItem(i18n("Bottom first"), 0);
        combo->addItem(i18n("Top first"), 1);
513
        connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
514
        combo->setObjectName(QStringLiteral("force_tff_value"));
515 516 517
        if (!force_tff.isEmpty()) {
            combo->setCurrentIndex(force_tff.toInt());
        }
Laurent Montel's avatar
Laurent Montel committed
518
        connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
519 520 521 522 523
        box->setChecked(!force_tff.isEmpty());
        combo->setEnabled(!force_tff.isEmpty());
        hlay->addWidget(box);
        hlay->addWidget(combo);
        vbox->addLayout(hlay);
524

Nicolas Carion's avatar
Nicolas Carion committed
525
        // Autorotate
526
        QString autorotate = m_properties->get("autorotate");
527 528 529
        m_originalProperties.insert(QStringLiteral("autorotate"), autorotate);
        hlay = new QHBoxLayout;
        box = new QCheckBox(i18n("Disable autorotate"), this);
Laurent Montel's avatar
Laurent Montel committed
530
        connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
531 532 533 534 535
        box->setObjectName(QStringLiteral("autorotate"));
        box->setChecked(autorotate == QLatin1String("0"));
        hlay->addWidget(box);
        vbox->addLayout(hlay);

Nicolas Carion's avatar
Nicolas Carion committed
536
        // Decoding threads
537
        QString threads = m_properties->get("threads");
538
        m_originalProperties.insert(QStringLiteral("threads"), threads);
539 540
        hlay = new QHBoxLayout;
        hlay->addWidget(new QLabel(i18n("Threads")));
Nicolas Carion's avatar
Nicolas Carion committed
541
        auto *spinI = new QSpinBox(this);
542
        spinI->setMaximum(4);
543
        spinI->setMinimum(1);
544
        spinI->setObjectName(QStringLiteral("threads_value"));
545 546
        if (!threads.isEmpty()) {
            spinI->setValue(threads.toInt());
Laurent Montel's avatar
Laurent Montel committed
547 548
        } else {
            spinI->setValue(1);
549
        }
Nicolas Carion's avatar
Nicolas Carion committed
550 551
        connect(spinI, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
                static_cast<void (ClipPropertiesController::*)(int)>(&ClipPropertiesController::slotValueChanged));
552 553 554
        hlay->addWidget(spinI);
        vbox->addLayout(hlay);

Nicolas Carion's avatar
Nicolas Carion committed
555
        // Video index
556
        if (!m_videoStreams.isEmpty()) {
557
            QString vix = m_sourceProperties.get("video_index");
558 559 560 561 562 563
            m_originalProperties.insert(QStringLiteral("video_index"), vix);
            hlay = new QHBoxLayout;

            KDualAction *ac = new KDualAction(i18n("Disable video"), i18n("Enable video"), this);
            ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video")));
            ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-video")));
Nicolas Carion's avatar
Nicolas Carion committed
564
            auto *tbv = new QToolButton(this);
565 566 567 568 569
            tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
            tbv->setDefaultAction(ac);
            tbv->setAutoRaise(true);
            hlay->addWidget(tbv);
            hlay->addWidget(new QLabel(i18n("Video stream")));
Nicolas Carion's avatar
Nicolas Carion committed
570
            auto *videoStream = new QComboBox(this);
571 572 573 574 575 576 577 578 579 580
            int ix = 1;
            for (int stream : m_videoStreams) {
                videoStream->addItem(i18n("Video stream %1", ix), stream);
                ix++;
            }
            if (!vix.isEmpty() && vix.toInt() > -1) {
                videoStream->setCurrentIndex(videoStream->findData(QVariant(vix)));
            }
            ac->setActive(vix.toInt() == -1);
            videoStream->setEnabled(vix.toInt() > -1);
581
            videoStream->setVisible(m_videoStreams.size() > 1);
Nicolas Carion's avatar
Nicolas Carion committed
582
            connect(ac, &KDualAction::activeChanged, [this, videoStream](bool activated) {
583
                QMap<QString, QString> properties;
584
                int vindx = -1;
585 586 587 588
                if (activated) {
                    videoStream->setEnabled(false);
                } else {
                    videoStream->setEnabled(true);
589
                    vindx = videoStream->currentData().toInt();
590
                }
591 592
                properties.insert(QStringLiteral("video_index"), QString::number(vindx));
                properties.insert(QStringLiteral("set.test_image"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
593 594 595
                emit updateClipProperties(m_id, m_originalProperties, properties);
                m_originalProperties = properties;
            });
596
            QObject::connect(videoStream, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this, videoStream]() {
597 598 599 600 601 602 603
                QMap<QString, QString> properties;
                properties.insert(QStringLiteral("video_index"), QString::number(videoStream->currentData().toInt()));
                emit updateClipProperties(m_id, m_originalProperties, properties);
                m_originalProperties = properties;
            });
            hlay->addWidget(videoStream);
            vbox->addLayout(hlay);
604
        }
605

Nicolas Carion's avatar
Nicolas Carion committed
606
        // Audio index
607
        QMap<int, QString> audioStreamsInfo = m_controller->audioStreams();
608
        if (!audioStreamsInfo.isEmpty()) {
609
            QList <int> enabledStreams = m_controller->activeStreams().keys();
610
            QString vix = m_sourceProperties.get("audio_index");
611
            m_originalProperties.insert(QStringLiteral("audio_index"), vix);
612 613 614 615 616
            QStringList streamString;
            for (int streamIx : enabledStreams) {
                streamString << QString::number(streamIx);
            }
            m_originalProperties.insert(QStringLiteral("kdenlive:active_streams"), streamString.join(QLatin1Char(';')));
617 618 619 620 621
            hlay = new QHBoxLayout;

            KDualAction *ac = new KDualAction(i18n("Disable audio"), i18n("Enable audio"), this);
            ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio")));
            ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio")));
Nicolas Carion's avatar
Nicolas Carion committed
622
            auto *tbv = new QToolButton(this);
623 624 625 626
            tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
            tbv->setDefaultAction(ac);
            tbv->setAutoRaise(true);
            hlay->addWidget(tbv);
627 628 629 630 631 632
            hlay->addWidget(new QLabel(i18n("Audio streams")));
            audioVbox->addLayout(hlay);
            m_audioStreamsView = new QListWidget(this);
            m_audioStreamsView->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
            audioVbox->addWidget(m_audioStreamsView);
            //m_audioStream = new QComboBox(this);
633
            QMapIterator<int, QString> i(audioStreamsInfo);
634 635
            while (i.hasNext()) {
                i.next();
636 637 638 639 640 641 642 643 644 645
                QListWidgetItem *item = new QListWidgetItem(i.value(), m_audioStreamsView);
                item->setData(Qt::UserRole, i.key());
                // Store oringinal name
                item->setData(Qt::UserRole + 1, i.value());
                item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
                if (enabledStreams.contains(i.key())) {
                    item->setCheckState(Qt::Checked);
                } else {
                    item->setCheckState(Qt::Unchecked);
                }
646
            }
647 648 649 650 651 652 653 654 655 656
            if (audioStreamsInfo.count() > 1) {
                QListWidgetItem *item = new QListWidgetItem(i18n("Merge all streams"), m_audioStreamsView);
                item->setData(Qt::UserRole, INT_MAX);
                item->setData(Qt::UserRole + 1, item->text());
                item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
                if (enabledStreams.contains(INT_MAX)) {
                    item->setCheckState(Qt::Checked);
                } else {
                    item->setCheckState(Qt::Unchecked);
                }
657
            }
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
            connect(m_audioStreamsView, &QListWidget::itemChanged, [this] (QListWidgetItem *item) {
                if (!item) {
                    return;
                }
                bool checked = item->checkState() == Qt::Checked;
                int streamId = item->data(Qt::UserRole).toInt();
                bool streamModified = false;
                QString currentStreams = m_originalProperties.value(QStringLiteral("kdenlive:active_streams"));
                QStringList activeStreams = currentStreams.split(QLatin1Char(';'));
                if (activeStreams.contains(QString::number(streamId))) {
                    if (!checked) {
                        // Stream was unselected
                        activeStreams.removeAll(QString::number(streamId));
                        streamModified = true;
                    }
                } else if (checked) {
                    // Stream was selected
                    activeStreams << QString::number(streamId);
                    activeStreams.sort();
                    streamModified = true;
                }
                if (streamModified) {
                    if (activeStreams.isEmpty()) {
                        activeStreams << QStringLiteral("-1");
                    }
                    QMap<QString, QString> properties;
                    properties.insert(QStringLiteral("kdenlive:active_streams"), activeStreams.join(QLatin1Char(';')));
                    emit updateClipProperties(m_id, m_originalProperties, properties);
                    m_originalProperties = properties;
                } else if (item->text() != item->data(Qt::UserRole + 1).toString()) {
                    // Rename event
                    QString txt = item->text();
                    int row = m_audioStreamsView->row(item) + 1;
                    if (!txt.startsWith(QString("%1|").arg(row))) {
                        txt.prepend(QString("%1|").arg(row));
                    }
                    m_controller->renameAudioStream(streamId, txt);
                    QSignalBlocker bk(m_audioStreamsView);
                    item->setText(txt);
                    item->setData(Qt::UserRole + 1, txt);
                }
            });
700
            ac->setActive(vix.toInt() == -1);
701
            connect(ac, &KDualAction::activeChanged, [this, audioStreamsInfo](bool activated) {
702
                QMap<QString, QString> properties;
703
                int vindx = -1;
704
                if (activated) {
705
                    properties.insert(QStringLiteral("kdenlive:active_streams"), QStringLiteral("-1"));
706
                } else {
707 708
                    properties.insert(QStringLiteral("kdenlive:active_streams"), QString());
                    vindx = audioStreamsInfo.firstKey();
709
                }
710 711
                properties.insert(QStringLiteral("audio_index"), QString::number(vindx));
                properties.insert(QStringLiteral("set.test_audio"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
712 713 714
                emit updateClipProperties(m_id, m_originalProperties, properties);
                m_originalProperties = properties;
            });
715

716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
            // Audio sync
            hlay = new QHBoxLayout;
            hlay->addWidget(new QLabel(i18n("Audio sync")));
            auto *spinSync = new QSpinBox(this);
            spinSync->setSuffix(i18n("ms"));
            spinSync->setRange(-1000, 1000);
            spinSync->setValue(qRound(1000 * m_sourceProperties.get_double("video_delay")));
            spinSync->setObjectName(QStringLiteral("video_delay"));
            if (spinSync->value() != 0) {
                m_originalProperties.insert(QStringLiteral("video_delay"), locale.toString(m_sourceProperties.get_double("video_delay")));
            }
            //QObject::connect(spinSync, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), [this, spinSync]() {
            QObject::connect(spinSync, &QSpinBox::editingFinished, [this, spinSync, locale]() {
                QMap<QString, QString> properties;
                properties.insert(QStringLiteral("video_delay"), locale.toString(spinSync->value() / 1000.));
                emit updateClipProperties(m_id, m_originalProperties, properties);
                m_originalProperties = properties;
            });
            hlay->addWidget(spinSync);
735
            audioVbox->addLayout(hlay);