]> git.sesse.net Git - kdenlive/blob - src/stopmotion/stopmotion.cpp
Keyboard shortcuts for stop motion capture
[kdenlive] / src / stopmotion / stopmotion.cpp
1 /***************************************************************************
2                           stopmotion.cpp  -  description
3                              -------------------
4     begin                : Feb 28 2008
5     copyright            : (C) 2010 by Jean-Baptiste Mardelle
6     email                : jb@kdenlive.org
7  ***************************************************************************/
8
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17
18 #include "stopmotion.h"
19 #include "../blackmagic/devices.h"
20 #include "../v4l/v4lcapture.h"
21 #include "../slideshowclip.h"
22 #include "kdenlivesettings.h"
23
24
25 #include <KDebug>
26 #include <KGlobalSettings>
27 #include <KFileDialog>
28 #include <KStandardDirs>
29 #include <KMessageBox>
30 #include <kdeversion.h>
31 #include <KNotification>
32
33 #ifdef QIMAGEBLITZ
34 #include <qimageblitz/qimageblitz.h>
35 #endif
36
37 #include <QtConcurrentRun>
38 #include <QInputDialog>
39 #include <QComboBox>
40 #include <QVBoxLayout>
41 #include <QTimer>
42 #include <QPainter>
43 #include <QAction>
44 #include <QWheelEvent>
45 #include <QMenu>
46
47 MyLabel::MyLabel(QWidget *parent) :
48     QLabel(parent)
49 {
50 }
51
52 void MyLabel::setImage(QImage img)
53 {
54     m_img = img;
55 }
56
57 //virtual
58 void MyLabel::wheelEvent(QWheelEvent * event)
59 {
60     if (event->delta() > 0) emit seek(true);
61     else emit seek(false);
62 }
63
64 //virtual
65 void MyLabel::paintEvent(QPaintEvent * event)
66 {
67     Q_UNUSED(event);
68
69     QRect r(0, 0, width(), height());
70     QPainter p(this);
71     p.fillRect(r, QColor(KdenliveSettings::window_background()));
72     double aspect_ratio = (double) m_img.width() / m_img.height();
73     int pictureHeight = height();
74     int pictureWidth = width();
75     int calculatedWidth = aspect_ratio * height();
76     if (calculatedWidth > width()) pictureHeight = width() / aspect_ratio;
77     else {
78         int calculatedHeight = width() / aspect_ratio;
79         if (calculatedHeight > height()) pictureWidth = height() * aspect_ratio;
80     }
81     p.drawImage(QRect((width() - pictureWidth) / 2, (height() - pictureHeight) / 2, pictureWidth, pictureHeight), m_img, QRect(0, 0, m_img.width(), m_img.height()));
82     p.end();
83 }
84
85
86 StopmotionWidget::StopmotionWidget(KUrl projectFolder, const QList< QAction * > actions, QWidget *parent) :
87     QDialog(parent)
88     , Ui::Stopmotion_UI()
89     , m_projectFolder(projectFolder)
90     , m_bmCapture(NULL)
91     , m_sequenceFrame(0)
92     , m_animatedIndex(-1)
93 {
94     addActions(actions);
95     setupUi(this);
96     setWindowTitle(i18n("Stop Motion Capture"));
97     setFont(KGlobalSettings::toolBarFont());
98
99     live_button->setIcon(KIcon("camera-photo"));
100
101     m_captureAction = actions.at(0);
102     connect(m_captureAction, SIGNAL(triggered()), this, SLOT(slotCaptureFrame()));
103     capture_button->setDefaultAction(m_captureAction);
104
105     connect(actions.at(1), SIGNAL(triggered()), this, SLOT(slotSwitchLive()));
106
107     preview_button->setIcon(KIcon("media-playback-start"));
108     capture_button->setEnabled(false);
109
110     // Build config menu
111     QMenu *confMenu = new QMenu;
112     m_showOverlay = actions.at(2);
113     connect(m_showOverlay, SIGNAL(triggered(bool)), this, SLOT(slotShowOverlay(bool)));
114     confMenu->addAction(m_showOverlay);
115
116 #ifdef QIMAGEBLITZ
117     m_effectIndex = KdenliveSettings::blitzeffect();
118     QMenu *effectsMenu = new QMenu(i18n("Overlay effect"));
119     QActionGroup *effectGroup = new QActionGroup(this);
120     QAction *noEffect = new QAction(i18n("No Effect"), effectGroup);
121     noEffect->setData(1);
122     QAction *contrastEffect = new QAction(i18n("Contrast"), effectGroup);
123     contrastEffect->setData(2);
124     QAction *edgeEffect = new QAction(i18n("Edge detect"), effectGroup);
125     edgeEffect->setData(3);
126     QAction *brightEffect = new QAction(i18n("Brighten"), effectGroup);
127     brightEffect->setData(4);
128     QAction *invertEffect = new QAction(i18n("Invert"), effectGroup);
129     invertEffect->setData(5);
130     QAction *thresEffect = new QAction(i18n("Threshold"), effectGroup);
131     thresEffect->setData(6);
132
133     effectsMenu->addAction(noEffect);
134     effectsMenu->addAction(contrastEffect);
135     effectsMenu->addAction(edgeEffect);
136     effectsMenu->addAction(brightEffect);
137     effectsMenu->addAction(invertEffect);
138     effectsMenu->addAction(thresEffect);
139     QList <QAction *> list = effectsMenu->actions();
140     for (int i = 0; i < list.count(); i++) {
141         list.at(i)->setCheckable(true);
142         if (list.at(i)->data().toInt() == m_effectIndex) {
143             list.at(i)->setChecked(true);
144         }
145     }
146     connect(effectsMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotUpdateOverlayEffect(QAction*)));
147     confMenu->addMenu(effectsMenu);
148 #endif
149
150     QAction *showThumbs = new QAction(KIcon("image-x-generic"), i18n("Show sequence thumbnails"), this);
151     showThumbs->setCheckable(true);
152     showThumbs->setChecked(KdenliveSettings::showstopmotionthumbs());
153     connect(showThumbs, SIGNAL(triggered(bool)), this, SLOT(slotShowThumbs(bool)));
154
155     QAction *removeCurrent = new QAction(KIcon("edit-delete"), i18n("Delete current frame"), this);
156     //TODO: implement frame deletion
157     //connect(removeCurrent, SIGNAL(triggered()), this, SLOT(slotRemoveFrame()));
158
159     QAction *capInterval = new QAction(KIcon(), i18n("Set capture interval"), this);
160     connect(capInterval, SIGNAL(triggered()), this, SLOT(slotSetCaptureInterval()));
161
162     confMenu->addAction(showThumbs);
163     confMenu->addAction(capInterval);
164     confMenu->addAction(removeCurrent);
165     config_button->setIcon(KIcon("configure"));
166     config_button->setMenu(confMenu);
167
168     // Build capture menu
169     QMenu *capMenu = new QMenu;
170     m_intervalCapture = new QAction(KIcon("edit-redo"), i18n("Interval capture"), this);
171     m_intervalCapture->setCheckable(true);
172     m_intervalCapture->setChecked(false);
173     connect(m_intervalCapture, SIGNAL(triggered(bool)), this, SLOT(slotIntervalCapture(bool)));
174     capMenu->addAction(m_intervalCapture);
175     capture_button->setMenu(capMenu);
176
177     connect(sequence_name, SIGNAL(textChanged(const QString &)), this, SLOT(sequenceNameChanged(const QString &)));
178     m_layout = new QVBoxLayout;
179     if (BMInterface::getBlackMagicDeviceList(capture_device, NULL)) {
180         // Found a BlackMagic device
181         m_bmCapture = new BmdCaptureHandler(m_layout);
182         connect(m_bmCapture, SIGNAL(gotMessage(const QString &)), this, SLOT(slotGotHDMIMessage(const QString &)));
183     }
184     if (QFile::exists(KdenliveSettings::video4vdevice())) {
185         if (m_bmCapture == NULL) m_bmCapture = new V4lCaptureHandler(m_layout);
186         capture_device->addItem(m_bmCapture->getDeviceName(KdenliveSettings::video4vdevice()), "v4l");
187     }
188
189     connect(m_bmCapture, SIGNAL(frameSaved(const QString)), this, SLOT(slotNewThumb(const QString)));
190     connect(capture_device, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateHandler()));
191     m_frame_preview = new MyLabel(this);
192     connect(m_frame_preview, SIGNAL(seek(bool)), this, SLOT(slotSeekFrame(bool)));
193     m_layout->addWidget(m_frame_preview);
194     m_frame_preview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
195     video_preview->setLayout(m_layout);
196     live_button->setChecked(false);
197     button_addsequence->setEnabled(false);
198     connect(live_button, SIGNAL(clicked(bool)), this, SLOT(slotLive(bool)));
199     connect(button_addsequence, SIGNAL(clicked(bool)), this, SLOT(slotAddSequence()));
200     connect(preview_button, SIGNAL(clicked(bool)), this, SLOT(slotPlayPreview(bool)));
201     connect(frame_list, SIGNAL(currentRowChanged(int)), this, SLOT(slotShowSelectedFrame()));
202     connect(this, SIGNAL(doCreateThumbs(QImage, int)), this, SLOT(slotCreateThumbs(QImage, int)));
203
204     frame_list->setHidden(!KdenliveSettings::showstopmotionthumbs());
205     parseExistingSequences();
206 }
207
208 StopmotionWidget::~StopmotionWidget()
209 {
210     m_bmCapture->stopPreview();
211 }
212
213 void StopmotionWidget::slotUpdateOverlayEffect(QAction *act)
214 {
215 #ifdef QIMAGEBLITZ
216     if (act) m_effectIndex = act->data().toInt();
217     KdenliveSettings::setBlitzeffect(m_effectIndex);
218     if (m_showOverlay->isChecked()) slotUpdateOverlay();
219 #endif
220 }
221
222 void StopmotionWidget::closeEvent(QCloseEvent *e)
223 {
224     slotLive(false);
225     QDialog::closeEvent(e);
226 }
227
228 void StopmotionWidget::slotSetCaptureInterval()
229 {
230     int interval = QInputDialog::getInteger(this, i18n("Set Capture Interval"), i18n("Interval (in seconds)"), KdenliveSettings::captureinterval(), 1);
231     if (interval > 0 && interval != KdenliveSettings::captureinterval())
232         KdenliveSettings::setCaptureinterval(interval);
233 }
234
235 void StopmotionWidget::slotShowThumbs(bool show)
236 {
237     KdenliveSettings::setShowstopmotionthumbs(show);
238     if (show) {
239         frame_list->clear();
240         sequenceNameChanged(sequence_name->currentText());
241     } else {
242         m_filesList.clear();
243         frame_list->clear();
244     }
245     frame_list->setHidden(!show);
246 }
247
248 void StopmotionWidget::slotIntervalCapture(bool capture)
249 {
250     if (capture) slotCaptureFrame();
251 }
252
253
254 void StopmotionWidget::slotUpdateHandler()
255 {
256     QString data = capture_device->itemData(capture_device->currentIndex()).toString();
257     m_bmCapture->stopPreview();
258     delete m_bmCapture;
259     m_layout->removeWidget(m_frame_preview);
260     if (data == "v4l") {
261         m_bmCapture = new V4lCaptureHandler(m_layout);
262     } else {
263         m_bmCapture = new BmdCaptureHandler(m_layout);
264         connect(m_bmCapture, SIGNAL(gotMessage(const QString &)), this, SLOT(slotGotHDMIMessage(const QString &)));
265     }
266     m_layout->addWidget(m_frame_preview);
267 }
268
269 void StopmotionWidget::slotGotHDMIMessage(const QString &message)
270 {
271     log_box->insertItem(0, message);
272 }
273
274 void StopmotionWidget::parseExistingSequences()
275 {
276     sequence_name->clear();
277     sequence_name->addItem(QString());
278     QDir dir(m_projectFolder.path());
279     QStringList filters;
280     filters << "*_0000.png";
281     //dir.setNameFilters(filters);
282     QStringList sequences = dir.entryList(filters, QDir::Files, QDir::Name);
283     //kDebug()<<"PF: "<<<<", sm: "<<sequences;
284     foreach(QString sequencename, sequences) {
285         sequence_name->addItem(sequencename.section("_", 0, -2));
286     }
287 }
288
289 void StopmotionWidget::slotSwitchLive()
290 {
291     setUpdatesEnabled(false);
292     if (m_frame_preview->isHidden()) {
293         m_bmCapture->hidePreview(true);
294         m_frame_preview->setHidden(false);
295     } else {
296         m_frame_preview->setHidden(true);
297         m_bmCapture->hidePreview(false);
298     }
299     setUpdatesEnabled(true);
300 }
301
302 void StopmotionWidget::slotLive(bool isOn)
303 {
304     if (isOn) {
305         //m_frame_preview->setImage(QImage());
306         m_frame_preview->setHidden(true);
307         m_bmCapture->startPreview(KdenliveSettings::hdmi_capturedevice(), KdenliveSettings::hdmi_capturemode());
308         capture_button->setEnabled(true);
309     } else {
310         m_bmCapture->stopPreview();
311         m_frame_preview->setHidden(false);
312         capture_button->setEnabled(false);
313         live_button->setChecked(false);
314     }
315 }
316
317 void StopmotionWidget::slotShowOverlay(bool isOn)
318 {
319     if (isOn) {
320         if (live_button->isChecked() && m_sequenceFrame > 0) {
321             slotUpdateOverlay();
322         }
323     } else {
324         m_bmCapture->hideOverlay();
325     }
326 }
327
328 void StopmotionWidget::slotUpdateOverlay()
329 {
330     QString path = getPathForFrame(m_sequenceFrame - 1);
331     if (!QFile::exists(path)) return;
332     QImage img(path);
333     if (img.isNull()) {
334         QTimer::singleShot(1000, this, SLOT(slotUpdateOverlay()));
335         return;
336     }
337
338 #ifdef QIMAGEBLITZ
339     //img = Blitz::convolveEdge(img, 0, Blitz::Low);
340     switch (m_effectIndex) {
341     case 2:
342         img = Blitz::contrast(img, true, 6);
343         break;
344     case 3:
345         img = Blitz::edge(img);
346         break;
347     case 4:
348         img = Blitz::intensity(img, 0.5);
349         break;
350     case 5:
351         Blitz::invert(img);
352         break;
353     case 6:
354         img = Blitz::threshold(img, 120, Blitz::Grayscale, qRgba(0, 0, 0, 0), qRgba(255, 0, 0, 255));
355         //img = Blitz::flatten(img, QColor(255, 0, 0, 255), QColor(0, 0, 0, 0));
356         break;
357     default:
358         break;
359
360     }
361 #endif
362     m_bmCapture->showOverlay(img);
363 }
364
365 void StopmotionWidget::sequenceNameChanged(const QString &name)
366 {
367     // Get rid of frames from previous sequence
368     disconnect(this, SIGNAL(doCreateThumbs(QImage, int)), this, SLOT(slotCreateThumbs(QImage, int)));
369     m_filesList.clear();
370     m_future.waitForFinished();
371     frame_list->clear();
372     if (name.isEmpty()) {
373         button_addsequence->setEnabled(false);
374     } else {
375         // Check if we are editing an existing sequence
376         QString pattern = SlideshowClip::selectedPath(getPathForFrame(0, sequence_name->currentText()), false, QString(), &m_filesList);
377         m_sequenceFrame = m_filesList.isEmpty() ? 0 : SlideshowClip::getFrameNumberFromPath(m_filesList.last()) + 1;
378         if (!m_filesList.isEmpty()) {
379             m_sequenceName = sequence_name->currentText();
380             connect(this, SIGNAL(doCreateThumbs(QImage, int)), this, SLOT(slotCreateThumbs(QImage, int)));
381             m_future = QtConcurrent::run(this, &StopmotionWidget::slotPrepareThumbs);
382             button_addsequence->setEnabled(true);
383         } else {
384             // new sequence
385             connect(this, SIGNAL(doCreateThumbs(QImage, int)), this, SLOT(slotCreateThumbs(QImage, int)));
386             button_addsequence->setEnabled(false);
387         }
388         capture_button->setEnabled(live_button->isChecked());
389     }
390 }
391
392 void StopmotionWidget::slotCaptureFrame()
393 {
394     if (sequence_name->currentText().isEmpty()) {
395         QString seqName = QInputDialog::getText(this, i18n("Create New Sequence"), i18n("Enter sequence name"));
396         if (seqName.isEmpty()) return;
397         sequence_name->blockSignals(true);
398         sequence_name->setItemText(sequence_name->currentIndex(), seqName);
399         sequence_name->blockSignals(false);
400     }
401     if (m_sequenceName != sequence_name->currentText()) {
402         m_sequenceName = sequence_name->currentText();
403         m_sequenceFrame = 0;
404     }
405     //capture_button->setEnabled(false);
406     QString currentPath = getPathForFrame(m_sequenceFrame);
407     kDebug() << "Capture FRame NB: " << m_sequenceFrame;
408     m_bmCapture->captureFrame(currentPath);
409     KNotification::event("FrameCaptured");
410     m_sequenceFrame++;
411     button_addsequence->setEnabled(true);
412     if (m_intervalCapture->isChecked()) QTimer::singleShot(KdenliveSettings::captureinterval() * 1000, this, SLOT(slotCaptureFrame()));
413 }
414
415
416 void StopmotionWidget::slotNewThumb(const QString path)
417 {
418     if (!KdenliveSettings::showstopmotionthumbs()) return;
419     m_filesList.append(path);
420     if (m_showOverlay->isChecked()) slotUpdateOverlay();
421     if (!m_future.isRunning()) m_future = QtConcurrent::run(this, &StopmotionWidget::slotPrepareThumbs);
422 }
423
424 void StopmotionWidget::slotPrepareThumbs()
425 {
426     if (m_filesList.isEmpty()) return;
427     QString path = m_filesList.takeFirst();
428     emit doCreateThumbs(QImage(path), SlideshowClip::getFrameNumberFromPath(path));
429
430 }
431
432 void StopmotionWidget::slotCreateThumbs(QImage img, int ix)
433 {
434     if (img.isNull()) {
435         m_future = QtConcurrent::run(this, &StopmotionWidget::slotPrepareThumbs);
436         return;
437     }
438     int height = 90;
439     int width = height * img.width() / img.height();
440     frame_list->setIconSize(QSize(width, height));
441     QPixmap pix = QPixmap::fromImage(img).scaled(width, height);
442     QString nb = QString::number(ix);
443     QPainter p(&pix);
444     QFontInfo finfo(font());
445     p.fillRect(0, 0, finfo.pixelSize() * nb.count() + 6, finfo.pixelSize() + 6, QColor(80, 80, 80, 150));
446     p.setPen(Qt::white);
447     p.drawText(QPoint(3, finfo.pixelSize() + 3), nb);
448     p.end();
449     QIcon icon(pix);
450     QListWidgetItem *item = new QListWidgetItem(icon, QString(), frame_list);
451     item->setToolTip(getPathForFrame(ix, sequence_name->currentText()));
452     item->setData(Qt::UserRole, ix);
453     m_future = QtConcurrent::run(this, &StopmotionWidget::slotPrepareThumbs);
454 }
455
456 QString StopmotionWidget::getPathForFrame(int ix, QString seqName)
457 {
458     if (seqName.isEmpty()) seqName = m_sequenceName;
459     return m_projectFolder.path(KUrl::AddTrailingSlash) + seqName + "_" + QString::number(ix).rightJustified(4, '0', false) + ".png";
460 }
461
462 void StopmotionWidget::slotShowFrame(const QString &path)
463 {
464     //slotLive(false);
465     QImage img(path);
466     capture_button->setEnabled(false);
467     if (!img.isNull()) {
468         m_bmCapture->hidePreview(true);
469         m_frame_preview->setImage(img);
470         m_frame_preview->setHidden(false);
471         m_frame_preview->update();
472     }
473 }
474
475 void StopmotionWidget::slotShowSelectedFrame()
476 {
477     QListWidgetItem *item = frame_list->currentItem();
478     if (item) {
479         //int ix = item->data(Qt::UserRole).toInt();;
480         slotShowFrame(item->toolTip());
481     }
482 }
483
484 void StopmotionWidget::slotAddSequence()
485 {
486     emit addOrUpdateSequence(getPathForFrame(0));
487 }
488
489 void StopmotionWidget::slotPlayPreview(bool animate)
490 {
491     if (!animate) {
492         // stop animation
493         m_animationList.clear();
494         return;
495     }
496     if (KdenliveSettings::showstopmotionthumbs()) {
497         frame_list->setCurrentRow(0);
498         QTimer::singleShot(200, this, SLOT(slotAnimate()));
499     } else {
500         SlideshowClip::selectedPath(getPathForFrame(0, sequence_name->currentText()), false, QString(), &m_animationList);
501         slotAnimate();
502     }
503 }
504
505 void StopmotionWidget::slotAnimate()
506 {
507     //slotShowFrame(m_animatedIndex);
508     if (KdenliveSettings::showstopmotionthumbs()) {
509         //TODO: loop
510         if (frame_list->currentRow() < (frame_list->count() - 1)) {
511             frame_list->setCurrentRow(frame_list->currentRow() + 1);
512             QTimer::singleShot(200, this, SLOT(slotAnimate()));
513         } else preview_button->setChecked(false);
514     } else if (!m_animationList.isEmpty()) {
515         slotShowFrame(m_animationList.takeFirst());
516         QTimer::singleShot(200, this, SLOT(slotAnimate()));
517     } else preview_button->setChecked(false);
518
519 }
520
521 QListWidgetItem *StopmotionWidget::getFrameFromIndex(int ix)
522 {
523     QListWidgetItem *item = NULL;
524     int pos = ix;
525     if (ix >= frame_list->count()) {
526         pos = frame_list->count() - 1;
527     }
528     if (ix < 0) pos = 0;
529     item = frame_list->item(pos);
530
531     int value = item->data(Qt::UserRole).toInt();
532     if (value == ix) return item;
533     else if (value < ix) {
534         pos++;
535         while (pos < frame_list->count()) {
536             item = frame_list->item(pos);
537             value = item->data(Qt::UserRole).toInt();
538             if (value == ix) return item;
539             pos++;
540         }
541     } else {
542         pos --;
543         while (pos >= 0) {
544             item = frame_list->item(pos);
545             value = item->data(Qt::UserRole).toInt();
546             if (value == ix) return item;
547             pos --;
548         }
549     }
550     return NULL;
551 }
552
553
554 void StopmotionWidget::selectFrame(int ix)
555 {
556     frame_list->blockSignals(true);
557     QListWidgetItem *item = getFrameFromIndex(ix);
558     if (!item) return;
559     frame_list->setCurrentItem(item);
560     frame_list->blockSignals(false);
561 }
562
563 void StopmotionWidget::slotSeekFrame(bool forward)
564 {
565     int ix = frame_list->currentRow();
566     if (forward) {
567         if (ix < frame_list->count() - 1) frame_list->setCurrentRow(ix + 1);
568     } else if (ix > 0) frame_list->setCurrentRow(ix - 1);
569 }
570
571