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