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