1 /*
2 * Copyright (c) 2012-2021 Meltytech, LLC
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "imageproducerwidget.h"
19 #include "ui_imageproducerwidget.h"
20 #include "settings.h"
21 #include "mainwindow.h"
22 #include "shotcut_mlt_properties.h"
23 #include "util.h"
24 #include "dialogs/filedatedialog.h"
25 #include "proxymanager.h"
26 #include "qmltypes/qmlapplication.h"
27 #include <Logger.h>
28 #include <QFileInfo>
29 #include <QDir>
30 #include <QMenu>
31 #include <QClipboard>
32 #include <QMessageBox>
33
34 // This legacy property is only used in this widget.
35 #define kShotcutResourceProperty "shotcut_resource"
36
ImageProducerWidget(QWidget * parent)37 ImageProducerWidget::ImageProducerWidget(QWidget *parent) :
38 QWidget(parent),
39 ui(new Ui::ImageProducerWidget),
40 m_defaultDuration(-1)
41 {
42 ui->setupUi(this);
43 Util::setColorsToHighlight(ui->filenameLabel, QPalette::Base);
44 }
45
~ImageProducerWidget()46 ImageProducerWidget::~ImageProducerWidget()
47 {
48 delete ui;
49 }
50
newProducer(Mlt::Profile & profile)51 Mlt::Producer* ImageProducerWidget::newProducer(Mlt::Profile& profile)
52 {
53 QString resource = QString::fromUtf8(m_producer->get("resource"));
54 if (!resource.contains("?begin=") && m_producer->get("begin")) {
55 resource.append(QString("?begin=%1").arg(m_producer->get("begin")));
56 }
57 LOG_DEBUG() << resource;
58 Mlt::Producer* p = new Mlt::Producer(profile, resource.toUtf8().constData());
59 if (p->is_valid()) {
60 if (ui->durationSpinBox->value() > p->get_length())
61 p->set("length", p->frames_to_time(ui->durationSpinBox->value(), mlt_time_clock));
62 p->set_in_and_out(0, ui->durationSpinBox->value() - 1);
63 }
64 return p;
65 }
66
setProducer(Mlt::Producer * p)67 void ImageProducerWidget::setProducer(Mlt::Producer* p)
68 {
69 AbstractProducerWidget::setProducer(p);
70 if (m_defaultDuration == -1)
71 m_defaultDuration = m_producer->get_length();
72 QString resource;
73 if (m_producer->get(kShotcutResourceProperty)) {
74 resource = QString::fromUtf8(m_producer->get(kShotcutResourceProperty));
75 } else if (m_producer->get(kOriginalResourceProperty)) {
76 resource = QString::fromUtf8(m_producer->get(kOriginalResourceProperty));
77 } else {
78 resource = QString::fromUtf8(m_producer->get("resource"));
79 p->set("ttl", 1);
80 }
81 QString name = Util::baseName(resource);
82 QString caption = m_producer->get(kShotcutCaptionProperty);
83 if (caption.isEmpty()) {
84 caption = name;
85 m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
86 }
87 ui->filenameLabel->setText(ui->filenameLabel->fontMetrics().elidedText(caption, Qt::ElideLeft, width() - 30));
88 updateDuration();
89 resource = QDir::toNativeSeparators(resource);
90 ui->filenameLabel->setToolTip(resource);
91 bool isProxy = m_producer->get_int(kIsProxyProperty) && m_producer->get(kOriginalResourceProperty);
92 ui->resolutionLabel->setText(QString("%1x%2 %3").arg(p->get("meta.media.width")).arg(p->get("meta.media.height"))
93 .arg(isProxy? tr("(PROXY)") : ""));
94 ui->aspectNumSpinBox->blockSignals(true);
95 if (p->get(kAspectRatioNumerator) && p->get(kAspectRatioDenominator)) {
96 ui->aspectNumSpinBox->setValue(p->get_int(kAspectRatioNumerator));
97 ui->aspectDenSpinBox->setValue(p->get_int(kAspectRatioDenominator));
98 }
99 else {
100 double sar = m_producer->get_double("aspect_ratio");
101 if (m_producer->get("force_aspect_ratio"))
102 sar = m_producer->get_double("force_aspect_ratio");
103 if (sar == 1.0) {
104 ui->aspectNumSpinBox->setValue(1);
105 ui->aspectDenSpinBox->setValue(1);
106 } else {
107 ui->aspectNumSpinBox->setValue(1000 * sar);
108 ui->aspectDenSpinBox->setValue(1000);
109 }
110 }
111 ui->aspectNumSpinBox->blockSignals(false);
112 if (m_producer->get_int("ttl"))
113 ui->repeatSpinBox->setValue(m_producer->get_int("ttl"));
114 ui->sequenceCheckBox->setChecked(m_producer->get_int(kShotcutSequenceProperty));
115 ui->repeatSpinBox->setEnabled(m_producer->get_int(kShotcutSequenceProperty));
116 ui->durationSpinBox->setEnabled(!p->get(kMultitrackItemProperty));
117 ui->notesTextEdit->setPlainText(QString::fromUtf8(m_producer->get(kCommentProperty)));
118 }
119
updateDuration()120 void ImageProducerWidget::updateDuration()
121 {
122 if (m_producer->get(kFilterOutProperty))
123 ui->durationSpinBox->setValue(m_producer->get_int(kFilterOutProperty) - m_producer->get_int(kFilterInProperty) + 1);
124 else
125 ui->durationSpinBox->setValue(m_producer->get_playtime());
126 }
127
rename()128 void ImageProducerWidget::rename()
129 {
130 ui->filenameLabel->setFocus();
131 ui->filenameLabel->selectAll();
132 }
133
reopen(Mlt::Producer * p)134 void ImageProducerWidget::reopen(Mlt::Producer* p)
135 {
136 int position = m_producer->position();
137 if (position > p->get_out())
138 position = p->get_out();
139 p->set("in", m_producer->get_in());
140 MLT.stop();
141 if (MLT.setProducer(p)) {
142 AbstractProducerWidget::setProducer(nullptr);
143 return;
144 }
145 setProducer(p);
146 emit producerReopened(false);
147 emit producerChanged(p);
148 if (p->get_int(kShotcutSequenceProperty)) {
149 MLT.play();
150 } else {
151 MLT.seek(position);
152 }
153 }
154
recreateProducer()155 void ImageProducerWidget::recreateProducer()
156 {
157 QString resource = m_producer->get("resource");
158 if (!resource.startsWith("qimage:") && !resource.startsWith("pixbuf:")) {
159 QString serviceName = m_producer->get("mlt_service");
160 if (!serviceName.isEmpty()) {
161 if (QFileInfo(resource).isRelative()) {
162 QString basePath = QFileInfo(MAIN.fileName()).canonicalPath();
163 QFileInfo fi(basePath, resource);
164 resource = fi.filePath();
165 }
166 resource.prepend(':').prepend(serviceName);
167 m_producer->set("resource", resource.toUtf8().constData());
168 }
169 }
170 Mlt::Producer* p = newProducer(MLT.profile());
171 p->pass_list(*m_producer, "force_aspect_ratio," kAspectRatioNumerator "," kAspectRatioDenominator
172 ", begin, ttl," kShotcutResourceProperty ", autolength, length," kShotcutSequenceProperty ", " kPlaylistIndexProperty
173 ", " kCommentProperty "," kOriginalResourceProperty "," kDisableProxyProperty "," kIsProxyProperty);
174 Mlt::Controller::copyFilters(*m_producer, *p);
175 if (m_producer->get(kMultitrackItemProperty)) {
176 emit producerChanged(p);
177 delete p;
178 } else {
179 reopen(p);
180 }
181 }
182
on_resetButton_clicked()183 void ImageProducerWidget::on_resetButton_clicked()
184 {
185 const char *s = m_producer->get(kShotcutResourceProperty);
186 if (!s)
187 s = m_producer->get(kShotcutResourceProperty);
188 Mlt::Producer* p = new Mlt::Producer(MLT.profile(), s);
189 Mlt::Controller::copyFilters(*m_producer, *p);
190 if (m_producer->get(kMultitrackItemProperty)) {
191 emit producerChanged(p);
192 delete p;
193 } else {
194 reopen(p);
195 }
196 }
197
on_aspectNumSpinBox_valueChanged(int)198 void ImageProducerWidget::on_aspectNumSpinBox_valueChanged(int)
199 {
200 if (m_producer) {
201 double new_sar = double(ui->aspectNumSpinBox->value()) /
202 double(ui->aspectDenSpinBox->value());
203 double sar = m_producer->get_double("aspect_ratio");
204 if (m_producer->get("force_aspect_ratio") || new_sar != sar) {
205 m_producer->set("force_aspect_ratio", QString::number(new_sar).toLatin1().constData());
206 m_producer->set(kAspectRatioNumerator, ui->aspectNumSpinBox->text().toLatin1().constData());
207 m_producer->set(kAspectRatioDenominator, ui->aspectDenSpinBox->text().toLatin1().constData());
208 }
209 emit producerChanged(producer());
210 }
211 }
212
on_aspectDenSpinBox_valueChanged(int i)213 void ImageProducerWidget::on_aspectDenSpinBox_valueChanged(int i)
214 {
215 on_aspectNumSpinBox_valueChanged(i);
216 }
217
on_durationSpinBox_editingFinished()218 void ImageProducerWidget::on_durationSpinBox_editingFinished()
219 {
220 if (!m_producer)
221 return;
222 if (ui->durationSpinBox->value() == m_producer->get_playtime())
223 return;
224 recreateProducer();
225 }
226
on_sequenceCheckBox_clicked(bool checked)227 void ImageProducerWidget::on_sequenceCheckBox_clicked(bool checked)
228 {
229 QString resource = m_producer->get("resource");
230 ui->repeatSpinBox->setEnabled(checked);
231 if (checked && !m_producer->get(kShotcutResourceProperty))
232 m_producer->set(kShotcutResourceProperty, resource.toUtf8().constData());
233 m_producer->set(kShotcutSequenceProperty, checked);
234 m_producer->set("autolength", checked);
235 m_producer->set("ttl", ui->repeatSpinBox->value());
236 if (checked) {
237 QFileInfo info(resource);
238 QString name(info.fileName());
239 QString begin = "";
240 int i = name.length();
241 int count = 0;
242
243 // find the last numeric digit
244 for (; i && !name[i - 1].isDigit(); i--) {};
245 // count the digits and build the begin value
246 for (; i && name[i - 1].isDigit(); i--, count++)
247 begin.prepend(name[i - 1]);
248 if (count) {
249 m_producer->set("begin", begin.toLatin1().constData());
250 int j = begin.toInt();
251 name.replace(i, count, QString("0%1d").arg(count).prepend('%'));
252 QString serviceName = m_producer->get("mlt_service");
253 if (!serviceName.isEmpty())
254 resource = serviceName + ":" + info.path() + "/" + name;
255 else
256 resource = info.path() + "/" + name;
257 m_producer->set("resource", resource.toUtf8().constData());
258
259 // Count the number of consecutive files.
260 MAIN.showStatusMessage(tr("Getting length of image sequence..."));
261 QCoreApplication::processEvents();
262 name = info.fileName();
263 name.replace(i, count, "%1");
264 resource = info.path().append('/').append(name);
265 for (i = j; QFile::exists(resource.arg(i, count, 10, QChar('0'))); ++i) {
266 if (i % 100 == 0)
267 QCoreApplication::processEvents();
268 }
269 i -= j;
270 m_producer->set("length", m_producer->frames_to_time(i * m_producer->get_int("ttl"), mlt_time_clock));
271 ui->durationSpinBox->setValue(i);
272 MAIN.showStatusMessage(tr("Reloading image sequence..."));
273 QCoreApplication::processEvents();
274 }
275 }
276 else {
277 m_producer->Mlt::Properties::clear("begin");
278 m_producer->set("resource", m_producer->get(kShotcutResourceProperty));
279 m_producer->set("length", m_producer->frames_to_time(qRound(MLT.profile().fps() * Mlt::kMaxImageDurationSecs), mlt_time_clock));
280 ui->durationSpinBox->setValue(qRound(MLT.profile().fps() * Settings.imageDuration()));
281 }
282 recreateProducer();
283 }
284
on_repeatSpinBox_editingFinished()285 void ImageProducerWidget::on_repeatSpinBox_editingFinished()
286 {
287 m_producer->set("ttl", ui->repeatSpinBox->value());
288 ui->durationSpinBox->setValue(m_producer->get_length());
289 MAIN.showStatusMessage(tr("Reloading image sequence..."));
290 QCoreApplication::processEvents();
291 recreateProducer();
292 }
293
on_defaultDurationButton_clicked()294 void ImageProducerWidget::on_defaultDurationButton_clicked()
295 {
296 Settings.setImageDuration(ui->durationSpinBox->value() / MLT.profile().fps());
297 }
298
on_notesTextEdit_textChanged()299 void ImageProducerWidget::on_notesTextEdit_textChanged()
300 {
301 QString existing = QString::fromUtf8(m_producer->get(kCommentProperty));
302 if (ui->notesTextEdit->toPlainText() != existing) {
303 m_producer->set(kCommentProperty, ui->notesTextEdit->toPlainText().toUtf8().constData());
304 emit modified();
305 }
306 }
307
on_menuButton_clicked()308 void ImageProducerWidget::on_menuButton_clicked()
309 {
310 QMenu menu;
311 if (!MLT.resource().contains("://")) // not a network stream
312 menu.addAction(ui->actionOpenFolder);
313 menu.addAction(ui->actionCopyFullFilePath);
314 menu.addAction(ui->actionSetFileDate);
315 menu.exec(ui->menuButton->mapToGlobal(QPoint(0, 0)));
316 }
317
GetFilenameFromProducer(Mlt::Producer * producer,bool useOriginal=true)318 static QString GetFilenameFromProducer(Mlt::Producer* producer, bool useOriginal = true)
319 {
320 QString resource;
321 if (useOriginal && producer->get(kOriginalResourceProperty)) {
322 resource = QString::fromUtf8(producer->get(kOriginalResourceProperty));
323 } else if (producer->get(kShotcutResourceProperty)) {
324 resource = QString::fromUtf8(producer->get(kShotcutResourceProperty));
325 } else {
326 resource = QString::fromUtf8(producer->get("resource"));
327 }
328 if (QFileInfo(resource).isRelative()) {
329 QString basePath = QFileInfo(MAIN.fileName()).canonicalPath();
330 QFileInfo fi(basePath, resource);
331 resource = fi.filePath();
332 }
333 return resource;
334 }
335
on_actionCopyFullFilePath_triggered()336 void ImageProducerWidget::on_actionCopyFullFilePath_triggered()
337 {
338 qApp->clipboard()->setText(GetFilenameFromProducer(producer()));
339 }
340
on_actionOpenFolder_triggered()341 void ImageProducerWidget::on_actionOpenFolder_triggered()
342 {
343 Util::showInFolder(GetFilenameFromProducer(producer()));
344 }
345
on_actionSetFileDate_triggered()346 void ImageProducerWidget::on_actionSetFileDate_triggered()
347 {
348 QString resource = GetFilenameFromProducer(producer());
349 FileDateDialog dialog(resource, producer(), this);
350 dialog.setModal(QmlApplication::dialogModality());
351 dialog.exec();
352 }
353
on_filenameLabel_editingFinished()354 void ImageProducerWidget::on_filenameLabel_editingFinished()
355 {
356 if (m_producer) {
357 auto caption = ui->filenameLabel->text();
358 if (caption.isEmpty()) {
359 caption = Util::baseName(GetFilenameFromProducer(m_producer.data()));
360 ui->filenameLabel->setText(caption);
361 m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
362 } else {
363 m_producer->set(kShotcutCaptionProperty, caption.toUtf8().constData());
364 }
365 emit modified();
366 }
367 }
368
on_actionDisableProxy_triggered(bool checked)369 void ImageProducerWidget::on_actionDisableProxy_triggered(bool checked)
370 {
371 if (checked) {
372 producer()->set(kDisableProxyProperty, 1);
373
374 // Replace with original
375 if (producer()->get_int(kIsProxyProperty) && producer()->get(kOriginalResourceProperty)) {
376 Mlt::Producer original(MLT.profile(), producer()->get(kOriginalResourceProperty));
377 if (original.is_valid()) {
378 original.set(kDisableProxyProperty, 1);
379 MAIN.replaceAllByHash(Util::getHash(original), original, true);
380 }
381 }
382 } else {
383 producer()->Mlt::Properties::clear(kDisableProxyProperty);
384 ui->actionMakeProxy->setEnabled(true);
385 }
386 }
387
on_actionMakeProxy_triggered()388 void ImageProducerWidget::on_actionMakeProxy_triggered()
389 {
390 ProxyManager::generateImageProxy(*producer());
391 }
392
on_actionDeleteProxy_triggered()393 void ImageProducerWidget::on_actionDeleteProxy_triggered()
394 {
395 // Delete the file if it exists
396 QString hash = Util::getHash(*producer());
397 QString fileName = hash + ProxyManager::imageFilenameExtension();
398 QDir dir = ProxyManager::dir();
399 LOG_DEBUG() << "removing" << dir.filePath(fileName);
400 dir.remove(dir.filePath(fileName));
401
402 // Delete the pending file if it exists));
403 fileName = hash + ProxyManager::pendingImageExtension();
404 dir.remove(dir.filePath(fileName));
405
406 // Replace with original
407 if (producer()->get_int(kIsProxyProperty) && producer()->get(kOriginalResourceProperty)) {
408 Mlt::Producer original(MLT.profile(), producer()->get(kOriginalResourceProperty));
409 if (original.is_valid()) {
410 MAIN.replaceAllByHash(hash, original, true);
411 }
412 }
413 }
414
on_actionCopyHashCode_triggered()415 void ImageProducerWidget::on_actionCopyHashCode_triggered()
416 {
417 qApp->clipboard()->setText(Util::getHash(*producer()));
418 QMessageBox::information(this, qApp->applicationName(),
419 tr("The hash code below is already copied to your clipboard:\n\n") +
420 Util::getHash(*producer()),
421 QMessageBox::Ok);
422 }
423
on_proxyButton_clicked()424 void ImageProducerWidget::on_proxyButton_clicked()
425 {
426 QMenu menu;
427 if (ProxyManager::isValidImage(*producer())) {
428 menu.addAction(ui->actionMakeProxy);
429 }
430 #ifndef Q_OS_WIN
431 menu.addAction(ui->actionDeleteProxy);
432 #endif
433 menu.addAction(ui->actionDisableProxy);
434 menu.addAction(ui->actionCopyHashCode);
435 if (m_producer->get_int(kDisableProxyProperty)) {
436 ui->actionMakeProxy->setDisabled(true);
437 ui->actionDisableProxy->setChecked(true);
438 }
439 menu.exec(ui->proxyButton->mapToGlobal(QPoint(0, 0)));
440 }
441