1 /*
2 SPDX-FileCopyrightText: 2015 Jean-Baptiste Mardelle <jb@kdenlive.org>
3 This file is part of Kdenlive. See www.kdenlive.org.
4 
5 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6 */
7 
8 #include "clippropertiescontroller.h"
9 #include "bin/model/markerlistmodel.hpp"
10 #include "clipcontroller.h"
11 #include "core.h"
12 #include "dialogs/profilesdialog.h"
13 #include "doc/kdenlivedoc.h"
14 #include "kdenlivesettings.h"
15 #include "profiles/profilerepository.hpp"
16 #include "project/projectmanager.h"
17 #include "timecodedisplay.h"
18 #include <audio/audioStreamInfo.h>
19 #include "widgets/choosecolorwidget.h"
20 
21 #include <KDualAction>
22 #include <KLocalizedString>
23 
24 #ifdef KF5_USE_FILEMETADATA
25 #include <KFileMetaData/ExtractionResult>
26 #include <KFileMetaData/Extractor>
27 #include <KFileMetaData/ExtractorCollection>
28 #include <KFileMetaData/PropertyInfo>
29 #endif
30 
31 #include <KIO/Global>
32 #include <KIO/OpenFileManagerWindowJob>
33 #include "kdenlive_debug.h"
34 #include <KMessageBox>
35 #include <QCheckBox>
36 #include <QClipboard>
37 #include <QComboBox>
38 #include <QDesktopServices>
39 #include <QDoubleSpinBox>
40 #include <QFile>
41 #include <QFileDialog>
42 #include <QFontDatabase>
43 #include <QHBoxLayout>
44 #include <QLabel>
45 #include <QMenu>
46 #include <QMimeData>
47 #include <QMimeDatabase>
48 #include <QProcess>
49 #include <QScrollArea>
50 #include <QTextEdit>
51 #include <QToolBar>
52 #include <QUrl>
53 #include <QListWidgetItem>
54 #include <QButtonGroup>
55 #include <QVBoxLayout>
56 #include <QResizeEvent>
57 #include <QSortFilterProxyModel>
58 
ElidedLinkLabel(QWidget * parent)59 ElidedLinkLabel::ElidedLinkLabel(QWidget *parent)
60     : QLabel(parent)
61 {
62 }
63 
setLabelText(const QString & text,const QString & link)64 void ElidedLinkLabel::setLabelText(const QString &text, const QString &link)
65 {
66     m_text = text;
67     m_link = link;
68     int width = currentWidth();
69     updateText(width);
70 }
71 
updateText(int width)72 void ElidedLinkLabel::updateText(int width)
73 {
74     if (m_link.isEmpty()) {
75         setText(fontMetrics().elidedText(m_text, Qt::ElideLeft, width));
76     } else {
77         setText(QString("<a href=\"%1\">%2</a>").arg(m_link, fontMetrics().elidedText(m_text, Qt::ElideLeft, width)));
78     }
79 }
80 
currentWidth() const81 int ElidedLinkLabel::currentWidth() const
82 {
83     int width = 0;
84     if (isVisible()) {
85         width = contentsRect().width();
86     } else {
87         QMargins mrg = contentsMargins();
88         width = sizeHint().width() - mrg.left() - mrg.right();
89     }
90     return width;
91 }
92 
resizeEvent(QResizeEvent * event)93 void ElidedLinkLabel::resizeEvent(QResizeEvent *event)
94 {
95     int diff = event->size().width() - event->oldSize().width();
96     updateText(currentWidth() + diff);
97     QLabel::resizeEvent(event);
98 }
99 
AnalysisTree(QWidget * parent)100 AnalysisTree::AnalysisTree(QWidget *parent)
101     : QTreeWidget(parent)
102 {
103     setRootIsDecorated(false);
104     setColumnCount(2);
105     setAlternatingRowColors(true);
106     setHeaderHidden(true);
107     setDragEnabled(true);
108 }
109 
110 // virtual
mimeData(const QList<QTreeWidgetItem * > list) const111 QMimeData *AnalysisTree::mimeData(const QList<QTreeWidgetItem *> list) const
112 {
113     QString mimeData;
114     for (QTreeWidgetItem *item : list) {
115         if ((item->flags() & Qt::ItemIsDragEnabled) != 0) {
116             mimeData.append(item->text(1));
117         }
118     }
119     auto *mime = new QMimeData;
120     mime->setData(QStringLiteral("kdenlive/geometry"), mimeData.toUtf8());
121     return mime;
122 }
123 
124 #ifdef KF5_USE_FILEMETADATA
125 class ExtractionResult : public KFileMetaData::ExtractionResult
126 {
127 public:
ExtractionResult(const QString & filename,const QString & mimetype,QTreeWidget * tree)128     ExtractionResult(const QString &filename, const QString &mimetype, QTreeWidget *tree)
129         : KFileMetaData::ExtractionResult(filename, mimetype, KFileMetaData::ExtractionResult::ExtractMetaData)
130         , m_tree(tree)
131     {
132     }
133 
append(const QString &)134     void append(const QString & /*text*/) override {}
135 
addType(KFileMetaData::Type::Type)136     void addType(KFileMetaData::Type::Type /*type*/) override {}
137 
add(KFileMetaData::Property::Property property,const QVariant & value)138     void add(KFileMetaData::Property::Property property, const QVariant &value) override
139     {
140         bool decode = false;
141         switch (property) {
142         case KFileMetaData::Property::ImageMake:
143         case KFileMetaData::Property::ImageModel:
144         case KFileMetaData::Property::ImageDateTime:
145         case KFileMetaData::Property::BitRate:
146         case KFileMetaData::Property::TrackNumber:
147         case KFileMetaData::Property::ReleaseYear:
148         case KFileMetaData::Property::Composer:
149         case KFileMetaData::Property::Genre:
150         case KFileMetaData::Property::Artist:
151         case KFileMetaData::Property::Album:
152         case KFileMetaData::Property::Title:
153         case KFileMetaData::Property::Comment:
154         case KFileMetaData::Property::Copyright:
155         case KFileMetaData::Property::PhotoFocalLength:
156         case KFileMetaData::Property::PhotoExposureTime:
157         case KFileMetaData::Property::PhotoFNumber:
158         case KFileMetaData::Property::PhotoApertureValue:
159         case KFileMetaData::Property::PhotoWhiteBalance:
160         case KFileMetaData::Property::PhotoGpsLatitude:
161         case KFileMetaData::Property::PhotoGpsLongitude:
162             decode = true;
163             break;
164         default:
165             break;
166         }
167         if (decode) {
168             KFileMetaData::PropertyInfo info(property);
169             if (info.valueType() == QVariant::DateTime) {
170                 new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toDateTime().toString(Qt::DefaultLocaleShortDate));
171             } else if (info.valueType() == QVariant::Int) {
172                 int val = value.toInt();
173                 if (property == KFileMetaData::Property::BitRate) {
174                     // Adjust unit for bitrate
175                     new QTreeWidgetItem(m_tree, QStringList() << info.displayName()
176                                                               << QString::number(val / 1000) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s"));
177                 } else {
178                     new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(val));
179                 }
180             } else if (info.valueType() == QVariant::Double) {
181                 new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << QString::number(value.toDouble()));
182             } else {
183                 new QTreeWidgetItem(m_tree, QStringList() << info.displayName() << value.toString());
184             }
185         }
186     }
187 
188 private:
189     QTreeWidget *m_tree;
190 };
191 #endif
192 
ClipPropertiesController(ClipController * controller,QWidget * parent)193 ClipPropertiesController::ClipPropertiesController(ClipController *controller, QWidget *parent)
194     : QWidget(parent)
195     , m_controller(controller)
196     , m_tc(Timecode(Timecode::HH_MM_SS_HH, pCore->getCurrentFps()))
197     , m_id(controller->binId())
198     , m_type(controller->clipType())
199     , m_properties(new Mlt::Properties(controller->properties()))
200     , m_audioStream(nullptr)
201     , m_textEdit(nullptr)
202     , m_audioStreamsView(nullptr)
203     , m_activeAudioStreams(-1)
204 {
205     m_controller->mirrorOriginalProperties(m_sourceProperties);
206     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
207     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
208     auto *lay = new QVBoxLayout;
209     lay->setContentsMargins(0, 0, 0, 0);
210     m_clipLabel = new ElidedLinkLabel(this);
211 
212     if (m_type == ClipType::Color || controller->clipUrl().isEmpty()) {
213         m_clipLabel->setLabelText(controller->clipName(), QString());
214     } else {
215         m_clipLabel->setLabelText(controller->clipUrl(), controller->clipUrl());
216     }
217     connect(m_clipLabel, &QLabel::linkActivated, [](const QString &link) {
218         KIO::highlightInFileManager({QUrl::fromLocalFile(link)});
219     });
220     lay->addWidget(m_clipLabel);
221     m_tabWidget = new QTabWidget(this);
222     lay->addWidget(m_tabWidget);
223     setLayout(lay);
224     m_tabWidget->setDocumentMode(true);
225     m_tabWidget->setTabPosition(QTabWidget::East);
226     auto *forcePage = new QScrollArea(this);
227     auto *forceAudioPage = new QScrollArea(this);
228     m_propertiesPage = new QWidget(this);
229     m_markersPage = new QWidget(this);
230     m_metaPage = new QWidget(this);
231     m_analysisPage = new QWidget(this);
232 
233     // Clip properties
234     auto *propsBox = new QVBoxLayout;
235     m_propertiesTree = new QTreeWidget(this);
236     m_propertiesTree->setRootIsDecorated(false);
237     m_propertiesTree->setColumnCount(2);
238     m_propertiesTree->setAlternatingRowColors(true);
239     m_propertiesTree->sortByColumn(0, Qt::AscendingOrder);
240     m_propertiesTree->setHeaderHidden(true);
241     propsBox->addWidget(m_propertiesTree);
242     fillProperties();
243     m_propertiesPage->setLayout(propsBox);
244 
245     // Clip markers
246     auto *mBox = new QVBoxLayout;
247     m_markerTree = new QTreeView;
248     m_markerTree->setRootIsDecorated(false);
249     m_markerTree->setAlternatingRowColors(true);
250     m_markerTree->setHeaderHidden(true);
251     m_markerTree->setSelectionMode(QAbstractItemView::ExtendedSelection);
252     m_markerTree->setObjectName("markers_list");
253     mBox->addWidget(m_markerTree);
254     m_sortMarkers = std::make_unique<QSortFilterProxyModel>(this);
255     m_sortMarkers->setSourceModel(controller->getMarkerModel().get());
256     m_sortMarkers->setSortRole(MarkerListModel::PosRole);
257     m_sortMarkers->sort(0, Qt::AscendingOrder);
258     m_markerTree->setModel(m_sortMarkers.get());
259     auto *bar = new QToolBar;
260     bar->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Add marker"), this, SLOT(slotAddMarker()));
261     bar->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete marker"), this, SLOT(slotDeleteMarker()));
262     bar->addAction(QIcon::fromTheme(QStringLiteral("document-edit")), i18n("Edit marker"), this, SLOT(slotEditMarker()));
263     bar->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export markers"), this, SLOT(slotSaveMarkers()));
264     bar->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import markers"), this, SLOT(slotLoadMarkers()));
265     mBox->addWidget(bar);
266 
267     m_markersPage->setLayout(mBox);
268     connect(m_markerTree, &QAbstractItemView::activated, this, &ClipPropertiesController::slotSeekToMarker);
269     connect(m_markerTree, &QAbstractItemView::clicked, this, &ClipPropertiesController::slotSeekToMarker);
270     connect(m_markerTree, &QAbstractItemView::doubleClicked, this, &ClipPropertiesController::slotEditMarker);
271 
272     // metadata
273     auto *m2Box = new QVBoxLayout;
274     auto *metaTree = new QTreeWidget;
275     metaTree->setRootIsDecorated(true);
276     metaTree->setColumnCount(2);
277     metaTree->setAlternatingRowColors(true);
278     metaTree->setHeaderHidden(true);
279     m2Box->addWidget(metaTree);
280     slotFillMeta(metaTree);
281     m_metaPage->setLayout(m2Box);
282 
283     // Clip analysis
284     auto *aBox = new QVBoxLayout;
285     m_analysisTree = new AnalysisTree(this);
286     aBox->addWidget(new QLabel(i18n("Analysis data")));
287     aBox->addWidget(m_analysisTree);
288     auto *bar2 = new QToolBar;
289     bar2->addAction(QIcon::fromTheme(QStringLiteral("trash-empty")), i18n("Delete analysis"), this, SLOT(slotDeleteAnalysis()));
290     bar2->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Export analysis"), this, SLOT(slotSaveAnalysis()));
291     bar2->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Import analysis"), this, SLOT(slotLoadAnalysis()));
292     aBox->addWidget(bar2);
293 
294     slotFillAnalysisData();
295     m_analysisPage->setLayout(aBox);
296 
297     // Force properties
298     auto *vbox = new QVBoxLayout;
299     vbox->setSpacing(0);
300 
301     // Force Audio properties
302     auto *audioVbox = new QVBoxLayout;
303     audioVbox->setSpacing(0);
304 
305     if (m_type == ClipType::Text || m_type == ClipType::SlideShow || m_type == ClipType::TextTemplate) {
306         QPushButton *editButton = new QPushButton(i18n("Edit Clip"), this);
307         connect(editButton, &QAbstractButton::clicked, this, &ClipPropertiesController::editClip);
308         vbox->addWidget(editButton);
309     }
310     if (m_type == ClipType::Color || m_type == ClipType::Image || m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::TextTemplate) {
311         // Edit duration widget
312         m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out"));
313         int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
314         if (kdenlive_length > 0) {
315             m_originalProperties.insert(QStringLiteral("kdenlive:duration"), m_properties->get("kdenlive:duration"));
316         }
317         m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length"));
318         auto *hlay = new QHBoxLayout;
319         QCheckBox *box = new QCheckBox(i18n("Duration"), this);
320         box->setObjectName(QStringLiteral("force_duration"));
321         hlay->addWidget(box);
322         auto *timePos = new TimecodeDisplay(m_tc, this);
323         timePos->setObjectName(QStringLiteral("force_duration_value"));
324         timePos->setValue(kdenlive_length > 0 ? kdenlive_length : m_properties->get_int("length"));
325         int original_length = m_properties->get_int("kdenlive:original_length");
326         if (original_length > 0) {
327             box->setChecked(true);
328         } else {
329             timePos->setEnabled(false);
330         }
331         hlay->addWidget(timePos);
332         vbox->addLayout(hlay);
333         connect(box, &QAbstractButton::toggled, timePos, &QWidget::setEnabled);
334         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
335         connect(timePos, &TimecodeDisplay::timeCodeEditingFinished, this, &ClipPropertiesController::slotDurationChanged);
336         connect(this, &ClipPropertiesController::updateTimeCodeFormat, timePos, &TimecodeDisplay::slotUpdateTimeCodeFormat);
337         connect(this, SIGNAL(modified(int)), timePos, SLOT(setValue(int)));
338 
339         // Autorotate
340          if (m_type == ClipType::Image) {
341             int autorotate = m_properties->get_int("disable_exif");
342             m_originalProperties.insert(QStringLiteral("disable_exif"), QString::number(autorotate));
343             hlay = new QHBoxLayout;
344             box = new QCheckBox(i18n("Disable autorotate"), this);
345             connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
346             box->setObjectName(QStringLiteral("disable_exif"));
347             box->setChecked(autorotate == 1);
348             hlay->addWidget(box);
349             vbox->addLayout(hlay);
350         }
351         // connect(this, static_cast<void(ClipPropertiesController::*)(int)>(&ClipPropertiesController::modified), timePos, &TimecodeDisplay::setValue);
352     }
353     if (m_type == ClipType::TextTemplate) {
354         // Edit text widget
355         QString currentText = m_properties->get("templatetext");
356         m_originalProperties.insert(QStringLiteral("templatetext"), currentText);
357         m_textEdit = new QTextEdit(this);
358         m_textEdit->setAcceptRichText(false);
359         m_textEdit->setPlainText(currentText);
360         m_textEdit->setPlaceholderText(i18n("Enter template text here"));
361         vbox->addWidget(m_textEdit);
362         QPushButton *button = new QPushButton(i18n("Apply"), this);
363         vbox->addWidget(button);
364         connect(button, &QPushButton::clicked, this, &ClipPropertiesController::slotTextChanged);
365     } else if (m_type == ClipType::Color) {
366         // Edit color widget
367         m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
368         mlt_color color = m_properties->get_color("resource");
369         ChooseColorWidget *choosecolor = new ChooseColorWidget(i18n("Color"), QColor::fromRgb(color.r, color.g, color.b).name(), "", false, this);
370         vbox->addWidget(choosecolor);
371         // connect(choosecolor, SIGNAL(displayMessage(QString,int)), this, SIGNAL(displayMessage(QString,int)));
372         connect(choosecolor, &ChooseColorWidget::modified, this, &ClipPropertiesController::slotColorModified);
373         connect(this, static_cast<void (ClipPropertiesController::*)(const QColor &)>(&ClipPropertiesController::modified), choosecolor,
374                 &ChooseColorWidget::slotColorModified);
375     }
376     if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image) {
377         // Aspect ratio
378         int force_ar_num = m_properties->get_int("force_aspect_num");
379         int force_ar_den = m_properties->get_int("force_aspect_den");
380         m_originalProperties.insert(QStringLiteral("force_aspect_den"), (force_ar_den == 0) ? QString() : QString::number(force_ar_den));
381         m_originalProperties.insert(QStringLiteral("force_aspect_num"), (force_ar_num == 0) ? QString() : QString::number(force_ar_num));
382         auto *hlay = new QHBoxLayout;
383         QCheckBox *box = new QCheckBox(i18n("Aspect ratio"), this);
384         box->setObjectName(QStringLiteral("force_ar"));
385         vbox->addWidget(box);
386         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
387         auto *spin1 = new QSpinBox(this);
388         spin1->setMaximum(8000);
389         spin1->setObjectName(QStringLiteral("force_aspect_num_value"));
390         hlay->addWidget(spin1);
391         hlay->addWidget(new QLabel(QStringLiteral(":")));
392         auto *spin2 = new QSpinBox(this);
393         spin2->setMinimum(1);
394         spin2->setMaximum(8000);
395         spin2->setObjectName(QStringLiteral("force_aspect_den_value"));
396         hlay->addWidget(spin2);
397         if (force_ar_num == 0) {
398             // use current ratio
399             int num = m_properties->get_int("meta.media.sample_aspect_num");
400             int den = m_properties->get_int("meta.media.sample_aspect_den");
401             if (den == 0) {
402                 num = 1;
403                 den = 1;
404             }
405             spin1->setEnabled(false);
406             spin2->setEnabled(false);
407             spin1->setValue(num);
408             spin2->setValue(den);
409         } else {
410             box->setChecked(true);
411             spin1->setEnabled(true);
412             spin2->setEnabled(true);
413             spin1->setValue(force_ar_num);
414             spin2->setValue(force_ar_den);
415         }
416         connect(spin2, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
417         connect(spin1, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, &ClipPropertiesController::slotAspectValueChanged);
418         connect(box, &QAbstractButton::toggled, spin1, &QWidget::setEnabled);
419         connect(box, &QAbstractButton::toggled, spin2, &QWidget::setEnabled);
420         vbox->addLayout(hlay);
421     }
422 
423     if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Image || m_type == ClipType::Playlist) {
424         // Proxy
425         QString proxy = m_properties->get("kdenlive:proxy");
426         m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy);
427         auto *hlay = new QHBoxLayout;
428         auto *bg = new QGroupBox(this);
429         bg->setCheckable(false);
430         bg->setFlat(true);
431         auto *groupLay = new QHBoxLayout;
432         groupLay->setContentsMargins(0, 0, 0, 0);
433         auto *pbox = new QCheckBox(i18n("Proxy clip"), this);
434         pbox->setTristate(true);
435         // Proxy codec label
436         QLabel *lab = new QLabel(this);
437         pbox->setObjectName(QStringLiteral("kdenlive:proxy"));
438         bool hasProxy = proxy.length() > 2;
439         if (hasProxy) {
440             bg->setToolTip(proxy);
441             bool proxyReady = (QFileInfo(proxy).fileName() == QFileInfo(m_properties->get("resource")).fileName());
442             if (proxyReady) {
443                 pbox->setCheckState(Qt::Checked);
444                 lab->setText(m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData()));
445             } else {
446                 pbox->setCheckState(Qt::PartiallyChecked);
447             }
448         } else {
449             pbox->setCheckState(Qt::Unchecked);
450         }
451         pbox->setEnabled(pCore->projectManager()->current()->getDocumentProperty(QStringLiteral("enableproxy")).toInt() != 0);
452         connect(pbox, &QCheckBox::stateChanged, this, [this, pbox](int state) {
453             emit requestProxy(state == Qt::PartiallyChecked);
454             if (state == Qt::Checked) {
455                 QSignalBlocker bk(pbox);
456                 pbox->setCheckState(Qt::Unchecked);
457             }
458         });
459         connect(this, &ClipPropertiesController::enableProxy, pbox, &QCheckBox::setEnabled);
460         connect(this, &ClipPropertiesController::proxyModified, this, [this, pbox, bg, lab](const QString &pxy) {
461             bool hasProxyClip = pxy.length() > 2;
462             QSignalBlocker bk(pbox);
463             pbox->setCheckState(hasProxyClip ? Qt::Checked : Qt::Unchecked);
464             bg->setEnabled(pbox->isChecked());
465             bg->setToolTip(pxy);
466             lab->setText(hasProxyClip ? m_properties->get(QString("meta.media.%1.codec.name").arg(m_properties->get_int("video_index")).toUtf8().constData())
467                                       : QString());
468         });
469         hlay->addWidget(pbox);
470         bg->setEnabled(pbox->checkState() == Qt::Checked);
471 
472         groupLay->addWidget(lab);
473 
474         // Delete button
475         auto *tb = new QToolButton(this);
476         tb->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete")));
477         tb->setAutoRaise(true);
478         connect(tb, &QToolButton::clicked, this, [this, proxy]() { emit deleteProxy(); });
479         tb->setToolTip(i18n("Delete proxy file"));
480         groupLay->addWidget(tb);
481         // Folder button
482         tb = new QToolButton(this);
483         auto *pMenu = new QMenu(this);
484         tb->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
485         tb->setToolTip(i18n("Proxy options"));
486         tb->setMenu(pMenu);
487         tb->setAutoRaise(true);
488         tb->setPopupMode(QToolButton::InstantPopup);
489 
490         QAction *ac = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open folder"), this);
491         connect(ac, &QAction::triggered, this, [this]() {
492             QString pxy = m_properties->get("kdenlive:proxy");
493             QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(pxy).path()));
494         });
495         pMenu->addAction(ac);
496         ac = new QAction(QIcon::fromTheme(QStringLiteral("media-playback-start")), i18n("Play proxy clip"), this);
497         connect(ac, &QAction::triggered, this, [this]() {
498             QString pxy = m_properties->get("kdenlive:proxy");
499             QDesktopServices::openUrl(QUrl::fromLocalFile(pxy));
500         });
501         pMenu->addAction(ac);
502         ac = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy file location to clipboard"), this);
503         connect(ac, &QAction::triggered, this, [this]() {
504             QString pxy = m_properties->get("kdenlive:proxy");
505             QGuiApplication::clipboard()->setText(pxy);
506         });
507         pMenu->addAction(ac);
508         groupLay->addWidget(tb);
509         bg->setLayout(groupLay);
510         hlay->addWidget(bg);
511         vbox->addLayout(hlay);
512     }
513 
514     if (m_type == ClipType::AV || m_type == ClipType::Video) {
515         // Fps
516         QString force_fps = m_properties->get("force_fps");
517         m_originalProperties.insert(QStringLiteral("force_fps"), force_fps.isEmpty() ? QStringLiteral("-") : force_fps);
518         auto *hlay = new QHBoxLayout;
519         QCheckBox *box = new QCheckBox(i18n("Frame rate"), this);
520         box->setObjectName(QStringLiteral("force_fps"));
521         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
522         auto *spin = new QDoubleSpinBox(this);
523         spin->setMaximum(1000);
524         connect(spin, SIGNAL(valueChanged(double)), this, SLOT(slotValueChanged(double)));
525         // connect(spin, static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged), this, &ClipPropertiesController::slotValueChanged);
526         spin->setObjectName(QStringLiteral("force_fps_value"));
527         if (force_fps.isEmpty()) {
528             spin->setValue(controller->originalFps());
529         } else {
530             spin->setValue(force_fps.toDouble());
531         }
532         connect(box, &QAbstractButton::toggled, spin, &QWidget::setEnabled);
533         box->setChecked(!force_fps.isEmpty());
534         spin->setEnabled(!force_fps.isEmpty());
535         hlay->addWidget(box);
536         hlay->addWidget(spin);
537         vbox->addLayout(hlay);
538 
539         // Scanning
540         QString force_prog = m_properties->get("force_progressive");
541         m_originalProperties.insert(QStringLiteral("force_progressive"), force_prog.isEmpty() ? QStringLiteral("-") : force_prog);
542         hlay = new QHBoxLayout;
543         box = new QCheckBox(i18n("Scanning"), this);
544         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
545         box->setObjectName(QStringLiteral("force_progressive"));
546         auto *combo = new QComboBox(this);
547         combo->addItem(i18n("Interlaced"), 0);
548         combo->addItem(i18n("Progressive"), 1);
549         connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
550         combo->setObjectName(QStringLiteral("force_progressive_value"));
551         if (!force_prog.isEmpty()) {
552             combo->setCurrentIndex(force_prog.toInt());
553         }
554         connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
555         box->setChecked(!force_prog.isEmpty());
556         combo->setEnabled(!force_prog.isEmpty());
557         hlay->addWidget(box);
558         hlay->addWidget(combo);
559         vbox->addLayout(hlay);
560 
561         // Field order
562         QString force_tff = m_properties->get("force_tff");
563         m_originalProperties.insert(QStringLiteral("force_tff"), force_tff.isEmpty() ? QStringLiteral("-") : force_tff);
564         hlay = new QHBoxLayout;
565         box = new QCheckBox(i18n("Field order"), this);
566         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
567         box->setObjectName(QStringLiteral("force_tff"));
568         combo = new QComboBox(this);
569         combo->addItem(i18n("Bottom first"), 0);
570         combo->addItem(i18n("Top first"), 1);
571         connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
572         combo->setObjectName(QStringLiteral("force_tff_value"));
573         if (!force_tff.isEmpty()) {
574             combo->setCurrentIndex(force_tff.toInt());
575         }
576         connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
577         box->setChecked(!force_tff.isEmpty());
578         combo->setEnabled(!force_tff.isEmpty());
579         hlay->addWidget(box);
580         hlay->addWidget(combo);
581         vbox->addLayout(hlay);
582 
583         // Autorotate
584         QString autorotate = m_properties->get("autorotate");
585         m_originalProperties.insert(QStringLiteral("autorotate"), autorotate);
586         hlay = new QHBoxLayout;
587         box = new QCheckBox(i18n("Disable autorotate"), this);
588         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
589         box->setObjectName(QStringLiteral("autorotate"));
590         box->setChecked(autorotate == QLatin1String("0"));
591         hlay->addWidget(box);
592         vbox->addLayout(hlay);
593 
594         // Decoding threads
595         QString threads = m_properties->get("threads");
596         m_originalProperties.insert(QStringLiteral("threads"), threads);
597         hlay = new QHBoxLayout;
598         hlay->addWidget(new QLabel(i18n("Threads")));
599         auto *spinI = new QSpinBox(this);
600         spinI->setMaximum(4);
601         spinI->setMinimum(1);
602         spinI->setObjectName(QStringLiteral("threads_value"));
603         if (!threads.isEmpty()) {
604             spinI->setValue(threads.toInt());
605         } else {
606             spinI->setValue(1);
607         }
608         connect(spinI, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this,
609                 static_cast<void (ClipPropertiesController::*)(int)>(&ClipPropertiesController::slotValueChanged));
610         hlay->addWidget(spinI);
611         vbox->addLayout(hlay);
612 
613         // Video index
614         if (!m_videoStreams.isEmpty()) {
615             QString vix = m_sourceProperties.get("video_index");
616             m_originalProperties.insert(QStringLiteral("video_index"), vix);
617             hlay = new QHBoxLayout;
618 
619             KDualAction *ac = new KDualAction(i18n("Disable video"), i18n("Enable video"), this);
620             ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video")));
621             ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-video")));
622             auto *tbv = new QToolButton(this);
623             tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
624             tbv->setDefaultAction(ac);
625             tbv->setAutoRaise(true);
626             hlay->addWidget(tbv);
627             hlay->addWidget(new QLabel(i18n("Video stream")));
628             auto *videoStream = new QComboBox(this);
629             int ix = 1;
630             for (int stream : qAsConst(m_videoStreams)) {
631                 videoStream->addItem(i18n("Video stream %1", ix), stream);
632                 ix++;
633             }
634             if (!vix.isEmpty() && vix.toInt() > -1) {
635                 videoStream->setCurrentIndex(videoStream->findData(QVariant(vix)));
636             }
637             ac->setActive(vix.toInt() == -1);
638             videoStream->setEnabled(vix.toInt() > -1);
639             videoStream->setVisible(m_videoStreams.size() > 1);
640             connect(ac, &KDualAction::activeChanged, this, [this, videoStream](bool activated) {
641                 QMap<QString, QString> properties;
642                 int vindx = -1;
643                 if (activated) {
644                     videoStream->setEnabled(false);
645                 } else {
646                     videoStream->setEnabled(true);
647                     vindx = videoStream->currentData().toInt();
648                 }
649                 properties.insert(QStringLiteral("video_index"), QString::number(vindx));
650                 properties.insert(QStringLiteral("set.test_image"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
651                 emit updateClipProperties(m_id, m_originalProperties, properties);
652                 m_originalProperties = properties;
653             });
654             QObject::connect(videoStream, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this, videoStream]() {
655                 QMap<QString, QString> properties;
656                 properties.insert(QStringLiteral("video_index"), QString::number(videoStream->currentData().toInt()));
657                 emit updateClipProperties(m_id, m_originalProperties, properties);
658                 m_originalProperties = properties;
659             });
660             hlay->addWidget(videoStream);
661             vbox->addLayout(hlay);
662         }
663 
664         // Audio index
665         QMap<int, QString> audioStreamsInfo = m_controller->audioStreams();
666         if (!audioStreamsInfo.isEmpty()) {
667             QList <int> enabledStreams = m_controller->activeStreams().keys();
668             QString vix = m_sourceProperties.get("audio_index");
669             m_originalProperties.insert(QStringLiteral("audio_index"), vix);
670             QStringList streamString;
671             for (int streamIx : qAsConst(enabledStreams)) {
672                 streamString << QString::number(streamIx);
673             }
674             m_originalProperties.insert(QStringLiteral("kdenlive:active_streams"), streamString.join(QLatin1Char(';')));
675             hlay = new QHBoxLayout;
676 
677             KDualAction *ac = new KDualAction(i18n("Disable audio"), i18n("Enable audio"), this);
678             ac->setInactiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio")));
679             ac->setActiveIcon(QIcon::fromTheme(QStringLiteral("kdenlive-hide-audio")));
680             auto *tbv = new QToolButton(this);
681             tbv->setToolButtonStyle(Qt::ToolButtonIconOnly);
682             tbv->setDefaultAction(ac);
683             tbv->setAutoRaise(true);
684             hlay->addWidget(tbv);
685             hlay->addWidget(new QLabel(i18n("Audio streams")));
686             audioVbox->addLayout(hlay);
687             m_audioStreamsView = new QListWidget(this);
688             m_audioStreamsView->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding);
689             audioVbox->addWidget(m_audioStreamsView);
690             QMapIterator<int, QString> i(audioStreamsInfo);
691             while (i.hasNext()) {
692                 i.next();
693                 auto *item = new QListWidgetItem(i.value(), m_audioStreamsView);
694                 // Store stream index
695                 item->setData(Qt::UserRole, i.key());
696                 // Store oringinal name
697                 item->setData(Qt::UserRole + 1, i.value());
698                 item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
699                 if (enabledStreams.contains(i.key())) {
700                     item->setCheckState(Qt::Checked);
701                 } else {
702                     item->setCheckState(Qt::Unchecked);
703                 }
704                 updateStreamIcon(m_audioStreamsView->row(item), i.key());
705             }
706             if (audioStreamsInfo.count() > 1) {
707                 QListWidgetItem *item = new QListWidgetItem(i18n("Merge all streams"), m_audioStreamsView);
708                 item->setData(Qt::UserRole, INT_MAX);
709                 item->setData(Qt::UserRole + 1, item->text());
710                 item->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable);
711                 if (enabledStreams.contains(INT_MAX)) {
712                     item->setCheckState(Qt::Checked);
713                 } else {
714                     item->setCheckState(Qt::Unchecked);
715                 }
716             }
717             connect(m_audioStreamsView, &QListWidget::currentRowChanged, this, [this] (int row) {
718                 if (row > -1) {
719                     m_audioEffectGroup->setEnabled(true);
720                     QListWidgetItem *item = m_audioStreamsView->item(row);
721                     m_activeAudioStreams = item->data(Qt::UserRole).toInt();
722                     QStringList effects = m_controller->getAudioStreamEffect(m_activeAudioStreams);
723                     QSignalBlocker bk(m_swapChannels);
724                     QSignalBlocker bk1(m_copyChannelGroup);
725                     QSignalBlocker bk2(m_normalize);
726                     m_swapChannels->setChecked(effects.contains(QLatin1String("channelswap")));
727                     m_copyChannel1->setChecked(effects.contains(QStringLiteral("channelcopy from=0 to=1")));
728                     m_copyChannel2->setChecked(effects.contains(QStringLiteral("channelcopy from=1 to=0")));
729                     m_normalize->setChecked(effects.contains(QStringLiteral("dynamic_loudness")));
730                     int gain = 0;
731                     for (const QString &st : qAsConst(effects)) {
732                         if (st.startsWith(QLatin1String("volume "))) {
733                             QSignalBlocker bk3(m_gain);
734                             gain = st.section(QLatin1Char('='), 1).toInt();
735                             break;
736                         }
737                     }
738                     QSignalBlocker bk3(m_gain);
739                     m_gain->setValue(gain);
740                 } else {
741                     m_activeAudioStreams = -1;
742                     m_audioEffectGroup->setEnabled(false);
743                 }
744             });
745             connect(m_audioStreamsView, &QListWidget::itemChanged, this, [this] (QListWidgetItem *item) {
746                 if (!item) {
747                     return;
748                 }
749                 bool checked = item->checkState() == Qt::Checked;
750                 int streamId = item->data(Qt::UserRole).toInt();
751                 bool streamModified = false;
752                 QString currentStreams = m_originalProperties.value(QStringLiteral("kdenlive:active_streams"));
753                 QStringList activeStreams = currentStreams.split(QLatin1Char(';'));
754                 if (activeStreams.contains(QString::number(streamId))) {
755                     if (!checked) {
756                         // Stream was unselected
757                         activeStreams.removeAll(QString::number(streamId));
758                         streamModified = true;
759                     }
760                 } else if (checked) {
761                     // Stream was selected
762                     if (streamId == INT_MAX) {
763                         // merge all streams should not have any other stream selected
764                         activeStreams.clear();
765                     } else {
766                         activeStreams.removeAll(QString::number(INT_MAX));
767                     }
768                     activeStreams << QString::number(streamId);
769                     activeStreams.sort();
770                     streamModified = true;
771                 }
772                 if (streamModified) {
773                     if (activeStreams.isEmpty()) {
774                         activeStreams << QStringLiteral("-1");
775                     }
776                     QMap<QString, QString> properties;
777                     properties.insert(QStringLiteral("kdenlive:active_streams"), activeStreams.join(QLatin1Char(';')));
778                     emit updateClipProperties(m_id, m_originalProperties, properties);
779                     m_originalProperties = properties;
780                 } else if (item->text() != item->data(Qt::UserRole + 1).toString()) {
781                     // Rename event
782                     QString txt = item->text();
783                     int row = m_audioStreamsView->row(item) + 1;
784                     if (!txt.startsWith(QString("%1|").arg(row))) {
785                         txt.prepend(QString("%1|").arg(row));
786                     }
787                     m_controller->renameAudioStream(streamId, txt);
788                     QSignalBlocker bk(m_audioStreamsView);
789                     item->setText(txt);
790                     item->setData(Qt::UserRole + 1, txt);
791                 }
792             });
793             ac->setActive(vix.toInt() == -1);
794             connect(ac, &KDualAction::activeChanged, this, [this, audioStreamsInfo](bool activated) {
795                 QMap<QString, QString> properties;
796                 int vindx = -1;
797                 if (activated) {
798                     properties.insert(QStringLiteral("kdenlive:active_streams"), QStringLiteral("-1"));
799                 } else {
800                     properties.insert(QStringLiteral("kdenlive:active_streams"), QString());
801                     vindx = audioStreamsInfo.firstKey();
802                 }
803                 properties.insert(QStringLiteral("audio_index"), QString::number(vindx));
804                 properties.insert(QStringLiteral("set.test_audio"), vindx > -1 ? QStringLiteral("0") : QStringLiteral("1"));
805                 emit updateClipProperties(m_id, m_originalProperties, properties);
806                 m_originalProperties = properties;
807             });
808             // Audio effects
809             m_audioEffectGroup = new QGroupBox(this);
810             m_audioEffectGroup->setEnabled(false);
811             auto *vbox = new QVBoxLayout;
812             // Normalize
813             m_normalize = new QCheckBox(i18n("Normalize"), this);
814             connect(m_normalize, &QCheckBox::stateChanged, this, [this] (int state) {
815                 if (m_activeAudioStreams == -1) {
816                     // No stream selected, abort
817                     return;
818                 }
819                 if (state == Qt::Checked) {
820                     // Add swap channels effect
821                     m_controller->requestAddStreamEffect(m_activeAudioStreams, QStringLiteral("dynamic_loudness"));
822                 } else {
823                     // Remove swap channels effect
824                     m_controller->requestRemoveStreamEffect(m_activeAudioStreams, QStringLiteral("dynamic_loudness"));
825                 }
826                 updateStreamIcon(m_audioStreamsView->currentRow(), m_activeAudioStreams);
827             });
828             vbox->addWidget(m_normalize);
829 
830             // Swap channels
831             m_swapChannels = new QCheckBox(i18n("Swap Channels"), this);
832             connect(m_swapChannels, &QCheckBox::stateChanged, this, [this] (int state) {
833                 if (m_activeAudioStreams == -1) {
834                     // No stream selected, abort
835                     return;
836                 }
837                 if (state == Qt::Checked) {
838                     // Add swap channels effect
839                     m_controller->requestAddStreamEffect(m_activeAudioStreams, QStringLiteral("channelswap"));
840                 } else {
841                     // Remove swap channels effect
842                     m_controller->requestRemoveStreamEffect(m_activeAudioStreams, QStringLiteral("channelswap"));
843                 }
844                 updateStreamIcon(m_audioStreamsView->currentRow(), m_activeAudioStreams);
845             });
846             vbox->addWidget(m_swapChannels);
847             // Copy channel
848             auto *copyLay = new QHBoxLayout;
849             copyLay->addWidget(new QLabel(i18n("Copy Channel"), this));
850             m_copyChannel1 = new QCheckBox(i18n("1"), this);
851             m_copyChannel2 = new QCheckBox(i18n("2"), this);
852             m_copyChannelGroup = new QButtonGroup(this);
853             m_copyChannelGroup->addButton(m_copyChannel1);
854             m_copyChannelGroup->addButton(m_copyChannel2);
855             m_copyChannelGroup->setExclusive(false);
856             copyLay->addWidget(m_copyChannel1);
857             copyLay->addWidget(m_copyChannel2);
858             copyLay->addStretch(1);
859             vbox->addLayout(copyLay);
860             connect(m_copyChannelGroup, QOverload<QAbstractButton *, bool>::of(&QButtonGroup::buttonToggled), this, [this] (QAbstractButton *but, bool) {
861                 if (but == m_copyChannel1) {
862                     QSignalBlocker bk(m_copyChannelGroup);
863                     m_copyChannel2->setChecked(false);
864                 } else {
865                     QSignalBlocker bk(m_copyChannelGroup);
866                     m_copyChannel1->setChecked(false);
867                 }
868                 if (m_copyChannel1->isChecked()) {
869                     m_controller->requestAddStreamEffect(m_activeAudioStreams, QStringLiteral("channelcopy from=0 to=1"));
870                 } else if (m_copyChannel2->isChecked()) {
871                     m_controller->requestAddStreamEffect(m_activeAudioStreams, QStringLiteral("channelcopy from=1 to=0"));
872                 } else {
873                     // Remove swap channels effect
874                     m_controller->requestRemoveStreamEffect(m_activeAudioStreams, QStringLiteral("channelcopy"));
875                 }
876                 updateStreamIcon(m_audioStreamsView->currentRow(), m_activeAudioStreams);
877             });
878             // Gain
879             auto *gainLay = new QHBoxLayout;
880             gainLay->addWidget(new QLabel(i18n("Gain"), this));
881             m_gain = new QSpinBox(this);
882             m_gain->setRange(-100, 60);
883             m_gain->setSuffix(i18n("dB"));
884             connect(m_gain, QOverload<int>::of(&QSpinBox::valueChanged), this, [this] (int value) {
885                 if (m_activeAudioStreams == -1) {
886                     // No stream selected, abort
887                     return;
888                 }
889                 if (value == 0) {
890                     // Remove effect
891                     m_controller->requestRemoveStreamEffect(m_activeAudioStreams, QStringLiteral("volume"));
892                 } else {
893                     m_controller->requestAddStreamEffect(m_activeAudioStreams, QString("volume level=%1").arg(value));
894                 }
895                 updateStreamIcon(m_audioStreamsView->currentRow(), m_activeAudioStreams);
896             });
897             gainLay->addWidget(m_gain);
898             gainLay->addStretch(1);
899             vbox->addLayout(gainLay);
900 
901             vbox->addStretch(1);
902             m_audioEffectGroup->setLayout(vbox);
903             audioVbox->addWidget(m_audioEffectGroup);
904 
905             // Audio sync
906             hlay = new QHBoxLayout;
907             hlay->addWidget(new QLabel(i18n("Audio sync")));
908             auto *spinSync = new QSpinBox(this);
909             spinSync->setSuffix(i18n("ms"));
910             spinSync->setRange(-1000, 1000);
911             spinSync->setValue(qRound(1000 * m_sourceProperties.get_double("video_delay")));
912             spinSync->setObjectName(QStringLiteral("video_delay"));
913             if (spinSync->value() != 0) {
914                 m_originalProperties.insert(QStringLiteral("video_delay"), QString::number(m_sourceProperties.get_double("video_delay"), 'f'));
915             }
916             QObject::connect(spinSync, &QSpinBox::editingFinished, this, [this, spinSync]() {
917                 QMap<QString, QString> properties;
918                 properties.insert(QStringLiteral("video_delay"), QString::number(spinSync->value() / 1000., 'f'));
919                 emit updateClipProperties(m_id, m_originalProperties, properties);
920                 m_originalProperties = properties;
921             });
922             hlay->addWidget(spinSync);
923             audioVbox->addLayout(hlay);
924         }
925 
926         // Colorspace
927         hlay = new QHBoxLayout;
928         box = new QCheckBox(i18n("Colorspace"), this);
929         box->setObjectName(QStringLiteral("force_colorspace"));
930         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
931         combo = new QComboBox(this);
932         combo->setObjectName(QStringLiteral("force_colorspace_value"));
933         combo->addItem(ProfileRepository::getColorspaceDescription(240), 240);
934         combo->addItem(ProfileRepository::getColorspaceDescription(601), 601);
935         combo->addItem(ProfileRepository::getColorspaceDescription(709), 709);
936         combo->addItem(ProfileRepository::getColorspaceDescription(10), 10);
937         int force_colorspace = m_properties->get_int("force_colorspace");
938         m_originalProperties.insert(QStringLiteral("force_colorspace"), force_colorspace == 0 ? QStringLiteral("-") : QString::number(force_colorspace));
939         int colorspace = controller->videoCodecProperty(QStringLiteral("colorspace")).toInt();
940         if (colorspace == 9) {
941             colorspace = 10;
942         }
943         if (force_colorspace > 0) {
944             box->setChecked(true);
945             combo->setEnabled(true);
946             combo->setCurrentIndex(combo->findData(force_colorspace));
947         } else if (colorspace > 0) {
948             combo->setEnabled(false);
949             combo->setCurrentIndex(combo->findData(colorspace));
950         } else {
951             combo->setEnabled(false);
952         }
953         connect(box, &QAbstractButton::toggled, combo, &QWidget::setEnabled);
954         connect(combo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &ClipPropertiesController::slotComboValueChanged);
955         hlay->addWidget(box);
956         hlay->addWidget(combo);
957         vbox->addLayout(hlay);
958 
959         // Full luma
960         QString force_luma = m_properties->get("set.force_full_luma");
961         m_originalProperties.insert(QStringLiteral("set.force_full_luma"), force_luma);
962         hlay = new QHBoxLayout;
963         box = new QCheckBox(i18n("Full luma range"), this);
964         connect(box, &QCheckBox::stateChanged, this, &ClipPropertiesController::slotEnableForce);
965         box->setObjectName(QStringLiteral("set.force_full_luma"));
966         box->setChecked(!force_luma.isEmpty());
967         hlay->addWidget(box);
968         vbox->addLayout(hlay);
969         hlay->addStretch(10);
970     }
971     // Force properties page
972     QWidget *forceProp = new QWidget(this);
973     forceProp->setLayout(vbox);
974     forcePage->setWidget(forceProp);
975     forcePage->setWidgetResizable(true);
976     // Force audio properties page
977     QWidget *forceAudioProp = new QWidget(this);
978     forceAudioProp->setLayout(audioVbox);
979     forceAudioPage->setWidget(forceAudioProp);
980     forceAudioPage->setWidgetResizable(true);
981 
982     vbox->addStretch(10);
983     m_tabWidget->addTab(m_propertiesPage, QString());
984     m_tabWidget->addTab(forcePage, QString());
985     m_tabWidget->addTab(forceAudioPage, QString());
986     m_tabWidget->addTab(m_markersPage, QString());
987     m_tabWidget->addTab(m_metaPage, QString());
988     m_tabWidget->addTab(m_analysisPage, QString());
989     m_tabWidget->setTabIcon(0, QIcon::fromTheme(QStringLiteral("edit-find")));
990     m_tabWidget->setTabToolTip(0, i18n("File info"));
991     m_tabWidget->setTabIcon(1, QIcon::fromTheme(QStringLiteral("document-edit")));
992     m_tabWidget->setTabToolTip(1, i18n("Properties"));
993     m_tabWidget->setTabIcon(2, QIcon::fromTheme(QStringLiteral("audio-volume-high")));
994     m_tabWidget->setTabToolTip(2, i18n("Audio Properties"));
995     m_tabWidget->setTabIcon(3, QIcon::fromTheme(QStringLiteral("bookmark-new")));
996     m_tabWidget->setTabToolTip(3, i18n("Markers"));
997     m_tabWidget->setTabIcon(4, QIcon::fromTheme(QStringLiteral("view-grid")));
998     m_tabWidget->setTabToolTip(4, i18n("Metadata"));
999     m_tabWidget->setTabIcon(5, QIcon::fromTheme(QStringLiteral("visibility")));
1000     m_tabWidget->setTabToolTip(5, i18n("Analysis"));
1001     m_tabWidget->setCurrentIndex(KdenliveSettings::properties_panel_page());
1002     if (m_type == ClipType::Color) {
1003         m_tabWidget->setTabEnabled(0, false);
1004     }
1005     connect(m_tabWidget, &QTabWidget::currentChanged, this, &ClipPropertiesController::updateTab);
1006 }
1007 
1008 ClipPropertiesController::~ClipPropertiesController() = default;
1009 
updateStreamIcon(int row,int streamIndex)1010 void ClipPropertiesController::updateStreamIcon(int row, int streamIndex)
1011 {
1012     QStringList effects = m_controller->getAudioStreamEffect(streamIndex);
1013     QListWidgetItem *item = m_audioStreamsView->item(row);
1014     if (item) {
1015         item->setIcon(effects.isEmpty() ? QIcon() : QIcon::fromTheme(QStringLiteral("favorite")));
1016     }
1017 }
1018 
updateTab(int ix)1019 void ClipPropertiesController::updateTab(int ix)
1020 {
1021     KdenliveSettings::setProperties_panel_page(ix);
1022 }
1023 
slotRefreshTimeCode()1024 void ClipPropertiesController::slotRefreshTimeCode()
1025 {
1026     emit updateTimeCodeFormat();
1027 }
1028 
slotReloadProperties()1029 void ClipPropertiesController::slotReloadProperties()
1030 {
1031     mlt_color color;
1032     m_properties.reset(new Mlt::Properties(m_controller->properties()));
1033     m_clipLabel->setText(m_properties->get("kdenlive:clipname"));
1034     switch (m_type) {
1035     case ClipType::Color:
1036         m_originalProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
1037         m_originalProperties.insert(QStringLiteral("out"), m_properties->get("out"));
1038         m_originalProperties.insert(QStringLiteral("length"), m_properties->get("length"));
1039         emit modified(m_properties->get_int("length"));
1040         color = m_properties->get_color("resource");
1041         emit modified(QColor::fromRgb(color.r, color.g, color.b));
1042         break;
1043     case ClipType::TextTemplate:
1044         m_textEdit->setPlainText(m_properties->get("templatetext"));
1045         break;
1046     case ClipType::Image:
1047     case ClipType::AV:
1048     case ClipType::Playlist:
1049     case ClipType::Video: {
1050         QString proxy = m_properties->get("kdenlive:proxy");
1051         if (proxy != m_originalProperties.value(QStringLiteral("kdenlive:proxy"))) {
1052             m_originalProperties.insert(QStringLiteral("kdenlive:proxy"), proxy);
1053             emit proxyModified(proxy);
1054         }
1055         if (m_audioStreamsView && m_audioStreamsView->count() > 0) {
1056             int audio_ix = m_properties->get_int("audio_index");
1057             m_originalProperties.insert(QStringLiteral("kdenlive:active_streams"), m_properties->get("kdenlive:active_streams"));
1058             if (audio_ix != m_originalProperties.value(QStringLiteral("audio_index")).toInt()) {
1059                 QSignalBlocker bk(m_audioStream);
1060                 m_originalProperties.insert(QStringLiteral("audio_index"), QString::number(audio_ix));
1061             }
1062             QList <int> enabledStreams = m_controller->activeStreams().keys();
1063             qDebug()<<"=== GOT ACTIVE STREAMS: "<<enabledStreams;
1064             QSignalBlocker bk(m_audioStreamsView);
1065             for (int ix = 0; ix < m_audioStreamsView->count(); ix++) {
1066                 QListWidgetItem *item = m_audioStreamsView->item(ix);
1067                 int stream = item->data(Qt::UserRole).toInt();
1068                 item->setCheckState(enabledStreams.contains(stream) ? Qt::Checked : Qt::Unchecked);
1069             }
1070         }
1071         break;
1072     }
1073     default:
1074         break;
1075     }
1076 }
1077 
slotColorModified(const QColor & newcolor)1078 void ClipPropertiesController::slotColorModified(const QColor &newcolor)
1079 {
1080     QMap<QString, QString> properties;
1081     properties.insert(QStringLiteral("resource"), newcolor.name(QColor::HexArgb));
1082     QMap<QString, QString> oldProperties;
1083     oldProperties.insert(QStringLiteral("resource"), m_properties->get("resource"));
1084     emit updateClipProperties(m_id, oldProperties, properties);
1085 }
1086 
slotDurationChanged(int duration)1087 void ClipPropertiesController::slotDurationChanged(int duration)
1088 {
1089     QMap<QString, QString> properties;
1090     // kdenlive_length is the default duration for image / title clips
1091     int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
1092     int current_length = m_properties->get_int("length");
1093     if (kdenlive_length > 0) {
1094         // special case, image/title clips store default duration in kdenlive:duration property
1095         properties.insert(QStringLiteral("kdenlive:duration"), m_properties->frames_to_time(duration));
1096         if (duration > current_length) {
1097             properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration));
1098             properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1));
1099         }
1100     } else {
1101         properties.insert(QStringLiteral("length"), m_properties->frames_to_time(duration));
1102         properties.insert(QStringLiteral("out"), m_properties->frames_to_time(duration - 1));
1103     }
1104     emit updateClipProperties(m_id, m_originalProperties, properties);
1105     m_originalProperties = properties;
1106 }
1107 
slotEnableForce(int state)1108 void ClipPropertiesController::slotEnableForce(int state)
1109 {
1110     auto *box = qobject_cast<QCheckBox *>(sender());
1111     if (!box) {
1112         return;
1113     }
1114     QString param = box->objectName();
1115     QMap<QString, QString> properties;
1116     if (state == Qt::Unchecked) {
1117         // The force property was disable, remove it / reset default if necessary
1118         if (param == QLatin1String("force_duration")) {
1119             // special case, reset original duration
1120             auto *timePos = findChild<TimecodeDisplay *>(param + QStringLiteral("_value"));
1121             timePos->setValue(m_properties->get_int("kdenlive:original_length"));
1122             int original = m_properties->get_int("kdenlive:original_length");
1123             m_properties->set("kdenlive:original_length", nullptr);
1124             slotDurationChanged(original);
1125             return;
1126         }
1127         if (param == QLatin1String("kdenlive:transparency")) {
1128             properties.insert(param, QString());
1129         } else if (param == QLatin1String("force_ar")) {
1130             properties.insert(QStringLiteral("force_aspect_den"), QString());
1131             properties.insert(QStringLiteral("force_aspect_num"), QString());
1132             properties.insert(QStringLiteral("force_aspect_ratio"), QString());
1133         } else if (param == QLatin1String("autorotate")) {
1134             properties.insert(QStringLiteral("autorotate"), QString());
1135         } else {
1136             properties.insert(param, QString());
1137         }
1138     } else {
1139         // A force property was set
1140         if (param == QLatin1String("force_duration")) {
1141             int original_length = m_properties->get_int("kdenlive:original_length");
1142             if (original_length == 0) {
1143                 int kdenlive_length = m_properties->time_to_frames(m_properties->get("kdenlive:duration"));
1144                 m_properties->set("kdenlive:original_length", kdenlive_length > 0 ? m_properties->get("kdenlive:duration") : m_properties->get("length"));
1145             }
1146         } else if (param == QLatin1String("force_fps")) {
1147             auto *spin = findChild<QDoubleSpinBox *>(param + QStringLiteral("_value"));
1148             if (!spin) {
1149                 return;
1150             }
1151             properties.insert(param, QString::number(spin->value(), 'f'));
1152         } else if (param == QLatin1String("threads")) {
1153             auto *spin = findChild<QSpinBox *>(param + QStringLiteral("_value"));
1154             if (!spin) {
1155                 return;
1156             }
1157             properties.insert(param, QString::number(spin->value()));
1158         } else if (param == QLatin1String("force_colorspace") || param == QLatin1String("force_progressive") || param == QLatin1String("force_tff")) {
1159             auto *combo = findChild<QComboBox *>(param + QStringLiteral("_value"));
1160             if (!combo) {
1161                 return;
1162             }
1163             properties.insert(param, QString::number(combo->currentData().toInt()));
1164         } else if (param == QLatin1String("set.force_full_luma")) {
1165             properties.insert(param, QStringLiteral("1"));
1166         } else if (param == QLatin1String("autorotate")) {
1167             properties.insert(QStringLiteral("autorotate"), QStringLiteral("0"));
1168         } else if (param == QLatin1String("force_ar")) {
1169             auto *spin = findChild<QSpinBox *>(QStringLiteral("force_aspect_num_value"));
1170             auto *spin2 = findChild<QSpinBox *>(QStringLiteral("force_aspect_den_value"));
1171             if ((spin == nullptr) || (spin2 == nullptr)) {
1172                 return;
1173             }
1174             properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value()));
1175             properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value()));
1176             properties.insert(QStringLiteral("force_aspect_ratio"), QString::number(double(spin->value()) / spin2->value(), 'f'));
1177         } else if (param == QLatin1String("disable_exif")) {
1178             properties.insert(QStringLiteral("disable_exif"), QString::number(1));
1179         }
1180     }
1181     if (properties.isEmpty()) {
1182         return;
1183     }
1184     emit updateClipProperties(m_id, m_originalProperties, properties);
1185     m_originalProperties = properties;
1186 }
1187 
slotValueChanged(double value)1188 void ClipPropertiesController::slotValueChanged(double value)
1189 {
1190     auto *box = qobject_cast<QDoubleSpinBox *>(sender());
1191     if (!box) {
1192         return;
1193     }
1194     QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
1195     QMap<QString, QString> properties;
1196     properties.insert(param, QString::number(value, 'f'));
1197     emit updateClipProperties(m_id, m_originalProperties, properties);
1198     m_originalProperties = properties;
1199 }
1200 
slotValueChanged(int value)1201 void ClipPropertiesController::slotValueChanged(int value)
1202 {
1203     auto *box = qobject_cast<QSpinBox *>(sender());
1204     if (!box) {
1205         return;
1206     }
1207     QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
1208     QMap<QString, QString> properties;
1209     properties.insert(param, QString::number(value));
1210     emit updateClipProperties(m_id, m_originalProperties, properties);
1211     m_originalProperties = properties;
1212 }
1213 
slotAspectValueChanged(int)1214 void ClipPropertiesController::slotAspectValueChanged(int)
1215 {
1216     auto *spin = findChild<QSpinBox *>(QStringLiteral("force_aspect_num_value"));
1217     auto *spin2 = findChild<QSpinBox *>(QStringLiteral("force_aspect_den_value"));
1218     if ((spin == nullptr) || (spin2 == nullptr)) {
1219         return;
1220     }
1221     QMap<QString, QString> properties;
1222     properties.insert(QStringLiteral("force_aspect_den"), QString::number(spin2->value()));
1223     properties.insert(QStringLiteral("force_aspect_num"), QString::number(spin->value()));
1224     properties.insert(QStringLiteral("force_aspect_ratio"), QString::number(double(spin->value()) / spin2->value(), 'f'));
1225     emit updateClipProperties(m_id, m_originalProperties, properties);
1226     m_originalProperties = properties;
1227 }
1228 
slotComboValueChanged()1229 void ClipPropertiesController::slotComboValueChanged()
1230 {
1231     auto *box = qobject_cast<QComboBox *>(sender());
1232     if (!box) {
1233         return;
1234     }
1235     QString param = box->objectName().section(QLatin1Char('_'), 0, -2);
1236     QMap<QString, QString> properties;
1237     properties.insert(param, QString::number(box->currentData().toInt()));
1238     emit updateClipProperties(m_id, m_originalProperties, properties);
1239     m_originalProperties = properties;
1240 }
1241 
fillProperties()1242 void ClipPropertiesController::fillProperties()
1243 {
1244     m_clipProperties.clear();
1245     QList<QStringList> propertyMap;
1246 
1247     m_propertiesTree->setSortingEnabled(false);
1248 
1249 #ifdef KF5_USE_FILEMETADATA
1250     // Read File Metadata through KDE's metadata system
1251     KFileMetaData::ExtractorCollection metaDataCollection;
1252     QMimeDatabase mimeDatabase;
1253     QMimeType mimeType;
1254 
1255     mimeType = mimeDatabase.mimeTypeForFile(m_controller->clipUrl());
1256     foreach (KFileMetaData::Extractor *plugin, metaDataCollection.fetchExtractors(mimeType.name())) {
1257         ExtractionResult extractionResult(m_controller->clipUrl(), mimeType.name(), m_propertiesTree);
1258         plugin->extract(&extractionResult);
1259     }
1260 #endif
1261 
1262     // Get MLT's metadata
1263     if (m_type == ClipType::Image) {
1264         int width = m_sourceProperties.get_int("meta.media.width");
1265         int height = m_sourceProperties.get_int("meta.media.height");
1266         propertyMap.append(QStringList() << i18n("Image size") << QString::number(width) + QLatin1Char('x') + QString::number(height));
1267     }
1268     if (m_type == ClipType::AV || m_type == ClipType::Video || m_type == ClipType::Audio) {
1269         int vindex = m_sourceProperties.get_int("video_index");
1270         int default_audio = m_sourceProperties.get_int("audio_index");
1271 
1272         // Find maximum stream index values
1273         m_videoStreams.clear();
1274         int aStreams = m_sourceProperties.get_int("meta.media.nb_streams");
1275         for (int ix = 0; ix < aStreams; ++ix) {
1276             char property[200];
1277             snprintf(property, sizeof(property), "meta.media.%d.stream.type", ix);
1278             QString type = m_sourceProperties.get(property);
1279             if (type == QLatin1String("video")) {
1280                 m_videoStreams << ix;
1281             }
1282         }
1283         m_clipProperties.insert(QStringLiteral("default_video"), QString::number(vindex));
1284         m_clipProperties.insert(QStringLiteral("default_audio"), QString::number(default_audio));
1285 
1286         if (vindex > -1) {
1287             // We have a video stream
1288             QString codecInfo = QString("meta.media.%1.codec.").arg(vindex);
1289             QString streamInfo = QString("meta.media.%1.stream.").arg(vindex);
1290             QString property = codecInfo + QStringLiteral("long_name");
1291             QString codec = m_sourceProperties.get(property.toUtf8().constData());
1292             if (!codec.isEmpty()) {
1293                 propertyMap.append({i18n("Video codec"), codec});
1294             }
1295             int width = m_sourceProperties.get_int("meta.media.width");
1296             int height = m_sourceProperties.get_int("meta.media.height");
1297             propertyMap.append({i18n("Frame size"), QString::number(width) + QLatin1Char('x') + QString::number(height)});
1298 
1299             property = streamInfo + QStringLiteral("frame_rate");
1300             QString fpsValue = m_sourceProperties.get(property.toUtf8().constData());
1301             if (!fpsValue.isEmpty()) {
1302                 propertyMap.append({i18n("Frame rate"), fpsValue});
1303             } else {
1304                 int rate_den = m_sourceProperties.get_int("meta.media.frame_rate_den");
1305                 if (rate_den > 0) {
1306                     double fps = double(m_sourceProperties.get_int("meta.media.frame_rate_num")) / rate_den;
1307                     propertyMap.append({i18n("Frame rate"), QString::number(fps, 'f', 2)});
1308                 }
1309             }
1310             property = codecInfo + QStringLiteral("bit_rate");
1311             int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000;
1312             if (bitrate > 0) {
1313                 propertyMap.append({i18n("Video bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")});
1314             }
1315 
1316             int scan = m_sourceProperties.get_int("meta.media.progressive");
1317             propertyMap.append({i18n("Scanning"), (scan == 1 ? i18n("Progressive") : i18n("Interlaced"))});
1318 
1319             property = codecInfo + QStringLiteral("sample_aspect_ratio");
1320             double par = m_sourceProperties.get_double(property.toUtf8().constData());
1321             if (qFuzzyIsNull(par)) {
1322                 // Read media aspect ratio
1323                 par = m_sourceProperties.get_double("aspect_ratio");
1324             }
1325             propertyMap.append({i18n("Pixel aspect ratio"), QString::number(par, 'f', 3)});
1326             property = codecInfo + QStringLiteral("pix_fmt");
1327             propertyMap.append({i18n("Pixel format"), m_sourceProperties.get(property.toUtf8().constData())});
1328             property = codecInfo + QStringLiteral("colorspace");
1329             int colorspace = m_sourceProperties.get_int(property.toUtf8().constData());
1330             propertyMap.append({i18n("Colorspace"), ProfileRepository::getColorspaceDescription(colorspace)});
1331 
1332             int b_frames = m_sourceProperties.get_int("meta.media.has_b_frames");
1333             propertyMap.append({i18n("B frames"), (b_frames == 1 ? i18n("Yes") : i18n("No"))});
1334         }
1335         if (default_audio > -1) {
1336             propertyMap.append({i18n("Audio streams"), QString::number(m_controller->audioStreamsCount())});
1337 
1338             QString codecInfo = QString("meta.media.%1.codec.").arg(default_audio);
1339             QString property = codecInfo + QStringLiteral("long_name");
1340             QString codec = m_sourceProperties.get(property.toUtf8().constData());
1341             if (!codec.isEmpty()) {
1342                 propertyMap.append({i18n("Audio codec"), codec});
1343             }
1344             property = codecInfo + QStringLiteral("channels");
1345             int channels = m_sourceProperties.get_int(property.toUtf8().constData());
1346             propertyMap.append({i18n("Audio channels"), QString::number(channels)});
1347 
1348             property = codecInfo + QStringLiteral("sample_rate");
1349             int srate = m_sourceProperties.get_int(property.toUtf8().constData());
1350             propertyMap.append({i18n("Audio frequency"), QString::number(srate) + QLatin1Char(' ') + i18nc("Herz", "Hz")});
1351 
1352             property = codecInfo + QStringLiteral("bit_rate");
1353             int bitrate = m_sourceProperties.get_int(property.toUtf8().constData()) / 1000;
1354             if (bitrate > 0) {
1355                 propertyMap.append({i18n("Audio bitrate"), QString::number(bitrate) + QLatin1Char(' ') + i18nc("Kilobytes per seconds", "kb/s")});
1356             }
1357         }
1358     }
1359 
1360     qint64 filesize = m_sourceProperties.get_int64("kdenlive:file_size");
1361     if (filesize > 0) {
1362         QLocale locale(QLocale::system()); // use the user's locale for getting proper separators!
1363         propertyMap.append({i18n("File size"), KIO::convertSize(size_t(filesize)) + QStringLiteral(" (") + locale.toString(filesize) + QLatin1Char(')')});
1364     }
1365 
1366     for (int i = 0; i < propertyMap.count(); i++) {
1367         auto *item = new QTreeWidgetItem(m_propertiesTree, propertyMap.at(i));
1368         item->setToolTip(1, propertyMap.at(i).at(1));
1369     }
1370     m_propertiesTree->setSortingEnabled(true);
1371     m_propertiesTree->resizeColumnToContents(0);
1372 }
1373 
slotSeekToMarker()1374 void ClipPropertiesController::slotSeekToMarker()
1375 {
1376     auto markerModel = m_controller->getMarkerModel();
1377     auto current = m_sortMarkers->mapToSource(m_markerTree->currentIndex());
1378     if (!current.isValid()) return;
1379     GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble());
1380     emit seekToFrame(pos.frames(pCore->getCurrentFps()));
1381 }
1382 
slotEditMarker()1383 void ClipPropertiesController::slotEditMarker()
1384 {
1385     auto markerModel = m_controller->getMarkerModel();
1386     auto current = m_sortMarkers->mapToSource(m_markerTree->currentIndex());
1387     if (!current.isValid()) return;
1388     GenTime pos(markerModel->data(current, MarkerListModel::PosRole).toDouble());
1389     markerModel->editMarkerGui(pos, this, false, m_controller);
1390 }
1391 
slotDeleteMarker()1392 void ClipPropertiesController::slotDeleteMarker()
1393 {
1394     auto markerModel = m_controller->getMarkerModel();
1395     QModelIndexList indexes = m_markerTree->selectionModel()->selectedIndexes();
1396     QModelIndexList mapped;
1397     for (auto &ix : indexes) {
1398         mapped << m_sortMarkers->mapToSource(ix);
1399     }
1400     QList <GenTime> positions;
1401     for (auto &ix : mapped) {
1402         if (ix.isValid()) {
1403             positions << GenTime(markerModel->data(ix, MarkerListModel::PosRole).toDouble());
1404         }
1405     }
1406     if (!positions.isEmpty()) {
1407         Fun undo = []() { return true; };
1408         Fun redo = []() { return true; };
1409 
1410         for (GenTime pos : qAsConst(positions)) {
1411             markerModel->removeMarker(pos, undo, redo);
1412         }
1413         pCore->pushUndo(undo, redo, i18n("Delete marker"));
1414     }
1415 }
1416 
slotAddMarker()1417 void ClipPropertiesController::slotAddMarker()
1418 {
1419     auto markerModel = m_controller->getMarkerModel();
1420     GenTime pos(m_controller->originalProducer()->position(), m_tc.fps());
1421     markerModel->editMarkerGui(pos, this, true, m_controller, true);
1422 }
1423 
slotSaveMarkers()1424 void ClipPropertiesController::slotSaveMarkers()
1425 {
1426     QScopedPointer<QFileDialog> fd(new QFileDialog(this, i18n("Save Clip Markers"), pCore->projectManager()->current()->projectDataFolder()));
1427     fd->setMimeTypeFilters(QStringList() << QStringLiteral("application/json") << QStringLiteral("text/plain"));
1428     fd->setFileMode(QFileDialog::AnyFile);
1429     fd->setAcceptMode(QFileDialog::AcceptSave);
1430     if (fd->exec() != QDialog::Accepted) {
1431         return;
1432     }
1433     QStringList selection = fd->selectedFiles();
1434     QString url;
1435     if (!selection.isEmpty()) {
1436         url = selection.first();
1437     }
1438     if (url.isEmpty()) {
1439         return;
1440     }
1441     QFile file(url);
1442     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1443         KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName()));
1444         return;
1445     }
1446     file.write(m_controller->getMarkerModel()->toJson().toUtf8());
1447     file.close();
1448 }
1449 
slotLoadMarkers()1450 void ClipPropertiesController::slotLoadMarkers()
1451 {
1452     QScopedPointer<QFileDialog> fd(new QFileDialog(this, i18n("Load Clip Markers"), pCore->projectManager()->current()->projectDataFolder()));
1453     fd->setMimeTypeFilters(QStringList() << QStringLiteral("application/json") << QStringLiteral("text/plain"));
1454     fd->setFileMode(QFileDialog::ExistingFile);
1455     if (fd->exec() != QDialog::Accepted) {
1456         return;
1457     }
1458     QStringList selection = fd->selectedFiles();
1459     QString url;
1460     if (!selection.isEmpty()) {
1461         url = selection.first();
1462     }
1463     if (url.isEmpty()) {
1464         return;
1465     }
1466     QFile file(url);
1467     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1468         KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName()));
1469         return;
1470     }
1471     QString fileContent = QString::fromUtf8(file.readAll());
1472     file.close();
1473     bool res = m_controller->getMarkerModel()->importFromJson(fileContent, false);
1474     if (!res) {
1475         KMessageBox::error(this, i18n("An error occurred while parsing the marker file"));
1476     }
1477 }
1478 
slotFillMeta(QTreeWidget * tree)1479 void ClipPropertiesController::slotFillMeta(QTreeWidget *tree)
1480 {
1481     tree->clear();
1482     if (m_type != ClipType::AV && m_type != ClipType::Video && m_type != ClipType::Image) {
1483         // Currently, we only use exiftool on video files
1484         return;
1485     }
1486     int exifUsed = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:exiftool"));
1487     if (exifUsed == 1) {
1488         Mlt::Properties subProperties;
1489         subProperties.pass_values(*m_properties, "kdenlive:meta.exiftool.");
1490         if (subProperties.count() > 0) {
1491             QTreeWidgetItem *exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
1492             exif->setExpanded(true);
1493             for (int i = 0; i < subProperties.count(); i++) {
1494                 new QTreeWidgetItem(exif, QStringList() << subProperties.get_name(i) << subProperties.get(i));
1495             }
1496         }
1497     } else if (KdenliveSettings::use_exiftool()) {
1498         QString url = m_controller->clipUrl();
1499         // Check for Canon THM file
1500         url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".THM");
1501         if (QFile::exists(url)) {
1502             QString exifToolBinary = QStandardPaths::findExecutable(QStringLiteral("exiftool"));
1503             if (!exifToolBinary.isEmpty()) {
1504                 // Read the exif metadata embedded in the THM file
1505                 QProcess p;
1506                 QStringList args = {QStringLiteral("-g"), QStringLiteral("-args"), url};
1507                 p.start(exifToolBinary, args);
1508                 p.waitForFinished();
1509                 QString res = p.readAllStandardOutput();
1510                 m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1);
1511                 QTreeWidgetItem *exif = nullptr;
1512                 QStringList list = res.split(QLatin1Char('\n'));
1513                 for (const QString &tagline : qAsConst(list)) {
1514                     if (tagline.startsWith(QLatin1String("-File")) || tagline.startsWith(QLatin1String("-ExifTool"))) {
1515                         continue;
1516                     }
1517                     QString tag = tagline.section(QLatin1Char(':'), 1).simplified();
1518                     if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) {
1519                         continue;
1520                     }
1521                     if (!tag.section(QLatin1Char('='), 0, 0).isEmpty() && !tag.section(QLatin1Char('='), 1).simplified().isEmpty()) {
1522                         if (!exif) {
1523                             exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
1524                             exif->setExpanded(true);
1525                         }
1526                         m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0),
1527                                                       tag.section(QLatin1Char('='), 1).simplified());
1528                         new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified());
1529                     }
1530                 }
1531             }
1532         } else {
1533             if (m_type == ClipType::Image || m_controller->codec(false) == QLatin1String("h264")) {
1534                 QString exifToolBinary = QStandardPaths::findExecutable(QStringLiteral("exiftool"));
1535                 if (!exifToolBinary.isEmpty()) {
1536                     QProcess p;
1537                     QStringList args = {QStringLiteral("-g"), QStringLiteral("-args"), m_controller->clipUrl()};
1538                     p.start(exifToolBinary, args);
1539                     p.waitForFinished();
1540                     QString res = p.readAllStandardOutput();
1541                     if (m_type != ClipType::Image) {
1542                         m_controller->setProducerProperty(QStringLiteral("kdenlive:exiftool"), 1);
1543                     }
1544                     QTreeWidgetItem *exif = nullptr;
1545                     QStringList list = res.split(QLatin1Char('\n'));
1546                     for (const QString &tagline : qAsConst(list)) {
1547                         if (m_type != ClipType::Image && !tagline.startsWith(QLatin1String("-H264"))) {
1548                             continue;
1549                         }
1550                         QString tag = tagline.section(QLatin1Char(':'), 1);
1551                         if (tag.startsWith(QLatin1String("ImageWidth")) || tag.startsWith(QLatin1String("ImageHeight"))) {
1552                             continue;
1553                         }
1554                         if (!exif) {
1555                             exif = new QTreeWidgetItem(tree, QStringList() << i18n("Exif") << QString());
1556                             exif->setExpanded(true);
1557                         }
1558                         if (m_type != ClipType::Image) {
1559                             // Do not store image exif metadata in project file, would be too much noise
1560                             m_controller->setProducerProperty("kdenlive:meta.exiftool." + tag.section(QLatin1Char('='), 0, 0),
1561                                                           tag.section(QLatin1Char('='), 1).simplified());
1562                         }
1563                         new QTreeWidgetItem(exif, QStringList() << tag.section(QLatin1Char('='), 0, 0) << tag.section(QLatin1Char('='), 1).simplified());
1564                     }
1565                 }
1566             }
1567         }
1568     }
1569     int magic = m_controller->getProducerIntProperty(QStringLiteral("kdenlive:magiclantern"));
1570     if (magic == 1) {
1571         Mlt::Properties subProperties;
1572         subProperties.pass_values(*m_properties, "kdenlive:meta.magiclantern.");
1573         QTreeWidgetItem *magicL = nullptr;
1574         for (int i = 0; i < subProperties.count(); i++) {
1575             if (!magicL) {
1576                 magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString());
1577                 QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png")));
1578                 magicL->setIcon(0, icon);
1579                 magicL->setExpanded(true);
1580             }
1581             new QTreeWidgetItem(magicL, QStringList() << subProperties.get_name(i) << subProperties.get(i));
1582         }
1583     } else if (m_type != ClipType::Image && KdenliveSettings::use_magicLantern()) {
1584         QString url = m_controller->clipUrl();
1585         url = url.section(QLatin1Char('.'), 0, -2) + QStringLiteral(".LOG");
1586         if (QFile::exists(url)) {
1587             QFile file(url);
1588             if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1589                 m_controller->setProducerProperty(QStringLiteral("kdenlive:magiclantern"), 1);
1590                 QTreeWidgetItem *magicL = nullptr;
1591                 while (!file.atEnd()) {
1592                     QString line = file.readLine().simplified();
1593                     if (line.startsWith('#') || line.isEmpty() || !line.contains(QLatin1Char(':'))) {
1594                         continue;
1595                     }
1596                     if (line.startsWith(QLatin1String("CSV data"))) {
1597                         break;
1598                     }
1599                     m_controller->setProducerProperty("kdenlive:meta.magiclantern." + line.section(QLatin1Char(':'), 0, 0).simplified(),
1600                                                       line.section(QLatin1Char(':'), 1).simplified());
1601                     if (!magicL) {
1602                         magicL = new QTreeWidgetItem(tree, QStringList() << i18n("Magic Lantern") << QString());
1603                         QIcon icon(QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("meta_magiclantern.png")));
1604                         magicL->setIcon(0, icon);
1605                         magicL->setExpanded(true);
1606                     }
1607                     new QTreeWidgetItem(magicL, QStringList()
1608                                                     << line.section(QLatin1Char(':'), 0, 0).simplified() << line.section(QLatin1Char(':'), 1).simplified());
1609                 }
1610             }
1611         }
1612 
1613         // if (!meta.isEmpty())
1614         // clip->setMetadata(meta, "Magic Lantern");
1615         // clip->setProperty("magiclantern", "1");
1616     }
1617     tree->resizeColumnToContents(0);
1618 }
1619 
slotFillAnalysisData()1620 void ClipPropertiesController::slotFillAnalysisData()
1621 {
1622     m_analysisTree->clear();
1623     Mlt::Properties subProperties;
1624     subProperties.pass_values(*m_properties, "kdenlive:clipanalysis.");
1625     if (subProperties.count() > 0) {
1626         for (int i = 0; i < subProperties.count(); i++) {
1627             new QTreeWidgetItem(m_analysisTree, QStringList() << subProperties.get_name(i) << subProperties.get(i));
1628         }
1629     }
1630     m_analysisTree->resizeColumnToContents(0);
1631 }
1632 
slotDeleteAnalysis()1633 void ClipPropertiesController::slotDeleteAnalysis()
1634 {
1635     QTreeWidgetItem *current = m_analysisTree->currentItem();
1636     if (!current) {
1637         return;
1638     }
1639     emit editAnalysis(m_id, "kdenlive:clipanalysis." + current->text(0), QString());
1640 }
1641 
slotSaveAnalysis()1642 void ClipPropertiesController::slotSaveAnalysis()
1643 {
1644     const QString url =
1645         QFileDialog::getSaveFileName(this, i18n("Save Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)"));
1646     if (url.isEmpty()) {
1647         return;
1648     }
1649     KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig);
1650     KConfigGroup analysisConfig(config, "Analysis");
1651     QTreeWidgetItem *current = m_analysisTree->currentItem();
1652     analysisConfig.writeEntry(current->text(0), current->text(1));
1653 }
1654 
slotLoadAnalysis()1655 void ClipPropertiesController::slotLoadAnalysis()
1656 {
1657     const QString url =
1658         QFileDialog::getOpenFileName(this, i18n("Open Analysis Data"), QFileInfo(m_controller->clipUrl()).absolutePath(), i18n("Text File (*.txt)"));
1659     if (url.isEmpty()) {
1660         return;
1661     }
1662     KSharedConfigPtr config = KSharedConfig::openConfig(url, KConfig::SimpleConfig);
1663     KConfigGroup transConfig(config, "Analysis");
1664     // read the entries
1665     QMap<QString, QString> profiles = transConfig.entryMap();
1666     QMapIterator<QString, QString> i(profiles);
1667     while (i.hasNext()) {
1668         i.next();
1669         emit editAnalysis(m_id, "kdenlive:clipanalysis." + i.key(), i.value());
1670     }
1671 }
1672 
slotTextChanged()1673 void ClipPropertiesController::slotTextChanged()
1674 {
1675     QMap<QString, QString> properties;
1676     properties.insert(QStringLiteral("templatetext"), m_textEdit->toPlainText());
1677     emit updateClipProperties(m_id, m_originalProperties, properties);
1678     m_originalProperties = properties;
1679 }
1680 
activatePage(int ix)1681 void ClipPropertiesController::activatePage(int ix)
1682 {
1683     m_tabWidget->setCurrentIndex(ix);
1684 }
1685 
slotDeleteSelectedMarkers()1686 void ClipPropertiesController::slotDeleteSelectedMarkers()
1687 {
1688     if (m_tabWidget->currentIndex() == 3) {
1689         slotDeleteMarker();
1690     }
1691 }
1692 
slotSelectAllMarkers()1693 void ClipPropertiesController::slotSelectAllMarkers()
1694 {
1695     if (m_tabWidget->currentIndex() == 3) {
1696         m_markerTree->selectAll();
1697     }
1698 }
1699 
updateStreamInfo(int streamIndex)1700 void ClipPropertiesController::updateStreamInfo(int streamIndex)
1701 {
1702     QStringList effects = m_controller->getAudioStreamEffect(m_activeAudioStreams);
1703     QListWidgetItem *item = nullptr;
1704     for (int ix = 0; ix < m_audioStreamsView->count(); ix++) {
1705         QListWidgetItem *it = m_audioStreamsView->item(ix);
1706         int stream = it->data(Qt::UserRole).toInt();
1707         if (stream == m_activeAudioStreams) {
1708             item = it;
1709             break;
1710         }
1711     }
1712     if (item) {
1713         item->setIcon(effects.isEmpty() ? QIcon() : QIcon::fromTheme(QStringLiteral("favorite")));
1714     }
1715     if (streamIndex == m_activeAudioStreams) {
1716         QSignalBlocker bk(m_swapChannels);
1717         QSignalBlocker bk1(m_copyChannelGroup);
1718         QSignalBlocker bk2(m_normalize);
1719         m_swapChannels->setChecked(effects.contains(QLatin1String("channelswap")));
1720         m_copyChannel1->setChecked(effects.contains(QStringLiteral("channelcopy from=0 to=1")));
1721         m_copyChannel2->setChecked(effects.contains(QStringLiteral("channelcopy from=1 to=0")));
1722         m_normalize->setChecked(effects.contains(QStringLiteral("dynamic_loudness")));
1723         int gain = 0;
1724         for (const QString &st : qAsConst(effects)) {
1725             if (st.startsWith(QLatin1String("volume "))) {
1726                 QSignalBlocker bk3(m_gain);
1727                 gain = st.section(QLatin1Char('='), 1).toInt();
1728                 break;
1729             }
1730         }
1731         QSignalBlocker bk3(m_gain);
1732         m_gain->setValue(gain);
1733     }
1734 }
1735