clipitem.cpp 84.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/***************************************************************************
 *   Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
 ***************************************************************************/

Vincent Pinon's avatar
Vincent Pinon committed
20
#include "clipitem.h"
21
#include "abstractgroupitem.h"
Vincent Pinon's avatar
Vincent Pinon committed
22
#include "customtrackscene.h"
23
#include "customtrackview.h"
Vincent Pinon's avatar
Vincent Pinon committed
24 25
#include "transition.h"

26 27
#include "renderer.h"
#include "kdenlivesettings.h"
Vincent Pinon's avatar
Vincent Pinon committed
28
#include "doc/kthumb.h"
29
#include "bin/projectclip.h"
30
#include "mltcontroller/effectscontroller.h"
Vincent Pinon's avatar
Vincent Pinon committed
31
#include "onmonitoritems/rotoscoping/rotowidget.h"
32
#include "utils/KoIconUtils.h"
33

34
#include <klocalizedstring.h>
Laurent Montel's avatar
Laurent Montel committed
35
#include "kdenlive_debug.h"
36 37 38 39 40 41
#include <QPainter>
#include <QTimer>
#include <QStyleOptionGraphicsItem>
#include <QGraphicsScene>
#include <QMimeData>

42 43
static int FRAME_SIZE;

44
ClipItem::ClipItem(ProjectClip *clip, const ItemInfo &info, double fps, double speed, int strobe, int frame_width, bool generateThumbs) :
45 46 47 48
    AbstractClipItem(info, QRectF(), fps),
    m_binClip(clip),
    m_startFade(0),
    m_endFade(0),
49
    m_clipState(PlaylistState::Original),
50
    m_originalClipState(PlaylistState::Original),
51 52 53
    m_startPix(QPixmap()),
    m_endPix(QPixmap()),
    m_hasThumbs(false),
Laurent Montel's avatar
Laurent Montel committed
54
    m_timeLine(nullptr),
55 56 57 58 59
    m_startThumbRequested(false),
    m_endThumbRequested(false),
    //m_hover(false),
    m_speed(speed),
    m_strobe(strobe),
60
    m_framePixelWidth(0)
61 62 63 64
{
    setZValue(2);
    m_effectList = EffectsList(true);
    FRAME_SIZE = frame_width;
65
    setRect(0, 0, (info.endPos - info.startPos).frames(m_fps) - 0.02, (double) itemHeight());
66
    // set speed independent info
67
    if (m_speed <= 0 && m_speed > -1) {
68
        m_speed = -1.0;
69
    }
70 71 72 73 74 75
    m_speedIndependantInfo = m_info;
    m_speedIndependantInfo.cropStart = GenTime((int)(m_info.cropStart.frames(m_fps) * qAbs(m_speed)), m_fps);
    m_speedIndependantInfo.cropDuration = GenTime((int)(m_info.cropDuration.frames(m_fps) * qAbs(m_speed)), m_fps);

    m_clipType = m_binClip->clipType();
    //m_cropStart = info.cropStart;
76 77
    if (m_binClip->hasLimitedDuration()) {
        m_maxDuration = m_binClip->duration();
78
    } else {
79 80 81
        // For color / image / text clips, we have unlimited duration
        m_maxDuration = GenTime();
    }
82
    setAcceptDrops(true);
83
    m_audioThumbReady = m_binClip->audioThumbCreated();
84
    //setAcceptsHoverEvents(true);
Laurent Montel's avatar
Laurent Montel committed
85
    connect(m_binClip, &ProjectClip::refreshClipDisplay, this, &ClipItem::slotRefreshClip);
86
    if (m_clipType == AV || m_clipType == Video || m_clipType == SlideShow || m_clipType == Playlist) {
87
        m_baseColor = QColor(141, 166, 215);
88
        if (m_binClip->isReady()) {
89 90
            m_hasThumbs = true;
            m_startThumbTimer.setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
91
            connect(&m_startThumbTimer, &QTimer::timeout, this, &ClipItem::slotGetStartThumb);
92
            m_endThumbTimer.setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
93
            connect(&m_endThumbTimer, &QTimer::timeout, this, &ClipItem::slotGetEndThumb);
Laurent Montel's avatar
Laurent Montel committed
94
            connect(m_binClip, SIGNAL(thumbReady(int, QImage)), this, SLOT(slotThumbReady(int, QImage)));
95 96 97
            if (generateThumbs && KdenliveSettings::videothumbnails()) {
                QTimer::singleShot(0, this, &ClipItem::slotFetchThumbs);
            }
98 99
        }
    } else if (m_clipType == Color) {
100
        m_baseColor = m_binClip->getProducerColorProperty(QStringLiteral("resource"));
101
    } else if (m_clipType == Image || m_clipType == Text || m_clipType == QText || m_clipType == TextTemplate) {
102
        m_baseColor = QColor(141, 166, 215);
103
        m_startPix = m_binClip->thumbnail(frame_width, rect().height());
104
        connect(m_binClip, SIGNAL(thumbUpdated(QImage)), this, SLOT(slotUpdateThumb(QImage)));
105 106 107 108
        //connect(m_clip->thumbProducer(), SIGNAL(thumbReady(int,QImage)), this, SLOT(slotThumbReady(int,QImage)));
    } else if (m_clipType == Audio) {
        m_baseColor = QColor(141, 215, 166);
    }
Laurent Montel's avatar
Laurent Montel committed
109
    connect(m_binClip, &ProjectClip::gotAudioData, this, &ClipItem::slotGotAudioData);
110 111 112
    m_paintColor = m_baseColor;
}

113 114
ClipItem::~ClipItem()
{
115
    blockSignals(true);
116 117
    m_endThumbTimer.stop();
    m_startThumbTimer.stop();
118
    if (scene()) {
Laurent Montel's avatar
Laurent Montel committed
119
        scene()->removeItem(this);
120 121 122 123
    }
    //if (m_clipType == Video | AV | SlideShow | Playlist) { // WRONG, cannot use |
    //disconnect(m_clip->thumbProducer(), SIGNAL(thumbReady(int,QImage)), this, SLOT(slotThumbReady(int,QImage)));
    //disconnect(m_clip, SIGNAL(gotAudioData()), this, SLOT(slotGotAudioData()));
124
    //}
125
    delete m_timeLine;
126 127
}

Laurent Montel's avatar
Laurent Montel committed
128
ClipItem *ClipItem::clone(const ItemInfo &info) const
129
{
130
    ClipItem *duplicate = new ClipItem(m_binClip, info, m_fps, m_speed, m_strobe, FRAME_SIZE);
131
    duplicate->setPos(pos());
132 133 134 135 136 137
    if (m_clipType == Image || m_clipType == Text || m_clipType == TextTemplate) {
        duplicate->slotSetStartThumb(m_startPix);
    } else if (m_clipType != Color && m_clipType != QText) {
        if (info.cropStart == m_info.cropStart) {
            duplicate->slotSetStartThumb(m_startPix);
        }
138 139 140
        if (info.cropStart + (info.endPos - info.startPos) == m_info.cropStart + m_info.cropDuration) {
            duplicate->slotSetEndThumb(m_endPix);
        }
141
    }
142
    duplicate->setEffectList(m_effectList);
143
    duplicate->setState(m_clipState);
144
    duplicate->setFades(fadeIn(), fadeOut());
145
    //duplicate->setSpeed(m_speed);
146 147 148
    return duplicate;
}

Laurent Montel's avatar
Laurent Montel committed
149
void ClipItem::setEffectList(const EffectsList &effectList)
150
{
151
    m_effectList.clone(effectList);
152
    m_effectNames = m_effectList.effectNames().join(QStringLiteral(" / "));
153 154
    m_startFade = 0;
    m_endFade = 0;
155
    if (!m_effectList.isEmpty()) {
156
        // If we only have one fade in /ou effect, always display it in timeline
157
        for (int i = 0; i < m_effectList.count(); ++i) {
158 159
            bool startFade = false;
            bool endFade = false;
160
            QDomElement effect = m_effectList.at(i);
161
            QString effectId = effect.attribute(QStringLiteral("id"));
162 163
            // check if it is a fade effect
            int fade = 0;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
164 165
            if (effectId == QLatin1String("fadein") || effectId == QLatin1String("fade_from_black")) {
                fade = EffectsList::parameter(effect, QStringLiteral("out")).toInt() - EffectsList::parameter(effect, QStringLiteral("in")).toInt();
166
                startFade = true;
167
            } else if (effectId == QLatin1String("fadeout") || effectId == QLatin1String("fade_to_black")) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
168
                fade = EffectsList::parameter(effect, QStringLiteral("in")).toInt() - EffectsList::parameter(effect, QStringLiteral("out")).toInt();
169 170 171 172 173
                endFade = true;
            }
            if (fade > 0) {
                if (!startFade) {
                    m_startFade = fade;
174 175
                } else {
                    m_startFade = 0;
176
                }
177
            } else if (fade < 0) {
178 179
                if (!endFade) {
                    m_endFade = -fade;
180 181
                } else {
                    m_endFade = 0;
182 183 184
                }
            }
        }
185
        setSelectedEffect(1);
186
    }
187 188
}

189
const EffectsList ClipItem::effectList() const
190
{
191
    return m_effectList;
192 193
}

194 195
int ClipItem::selectedEffectIndex() const
{
196 197 198
    return m_selectedEffect;
}

Laurent Montel's avatar
Laurent Montel committed
199
void ClipItem::initEffect(ProfileInfo pInfo, const QDomElement &effect, int diff, int offset)
200
{
201
    EffectsController::initEffect(m_info, pInfo, m_effectList, m_binClip->getProducerProperty(QStringLiteral("proxy")), effect, diff, offset);
202 203
}

204
bool ClipItem::checkKeyFrames(int width, int height, int previousDuration, int cutPos)
205
{
206
    bool clipEffectsModified = false;
207
    int effectsCount = m_effectList.count();
208
    if (effectsCount == 0) {
209
        // reset keyframes
210
        m_keyframeView.reset();
211
    }
212
    // go through all effects this clip has
213
    for (int ix = 0; ix < effectsCount; ++ix) {
214
        // Check geometry params
215 216
        QDomElement effect = m_effectList.at(ix);
        clipEffectsModified = resizeGeometries(effect, width, height, previousDuration, cutPos == -1 ? 0 : cutPos, cropDuration().frames(m_fps) - 1, cropStart().frames(m_fps));
217
        QString newAnimation = resizeAnimations(effect, previousDuration, cutPos == -1 ? 0 : cutPos, cropDuration().frames(m_fps) - 1, cropStart().frames(m_fps));
218
        if (!newAnimation.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
219
            //setKeyframes(ix, newAnimation.split(QLatin1Char(';'), QString::SkipEmptyParts));
220 221 222 223 224 225
            clipEffectsModified = true;
        }
        if (clipEffectsModified) {
            setKeyframes(ix);
            continue;
        }
226 227 228 229
    }
    return clipEffectsModified;
}

230
void ClipItem::setKeyframes(const int ix)
231
{
232
    QDomElement effect = m_effectList.at(ix);
233 234 235
    if (effect.attribute(QStringLiteral("disable")) == QLatin1String("1")) {
        return;
    }
236
    QLocale locale;
237
    locale.setNumberOptions(QLocale::OmitGroupSeparator);
238

239
    if (m_keyframeView.loadKeyframes(locale, effect, cropStart().frames(m_fps), cropDuration().frames(m_fps))) {
240 241 242
        // Keyframable effect found
        update();
    }
243 244
}

245 246
void ClipItem::setSelectedEffect(const int ix)
{
247 248 249
    int editedKeyframe = -1;
    if (m_selectedEffect == ix) {
        // reloading same effect, keep current keyframe reference
250
        editedKeyframe = m_keyframeView.activeKeyframe;
251
    }
252
    m_selectedEffect = ix;
253
    QLocale locale;
254
    locale.setNumberOptions(QLocale::OmitGroupSeparator);
255
    QDomElement effect = effectAtIndex(m_selectedEffect);
256
    bool refreshClip = false;
257
    m_keyframeView.reset();
258
    if (!effect.isNull() && effect.attribute(QStringLiteral("disable")) != QLatin1String("1")) {
Laurent Montel's avatar
Laurent Montel committed
259
        QString effectId = effect.attribute(QStringLiteral("id"));
260 261

        // Check for fades to display in timeline
Laurent Montel's avatar
Laurent Montel committed
262 263
        int startFade1 = m_effectList.hasEffect(QString(), QStringLiteral("fadein"));
        int startFade2 = m_effectList.hasEffect(QString(), QStringLiteral("fade_from_black"));
264 265 266 267

        if (startFade1 >= 0 && startFade2 >= 0) {
            // We have 2 fade ins, only display if effect is selected
            if (ix == startFade1 || ix == startFade2) {
Laurent Montel's avatar
Laurent Montel committed
268
                m_startFade = EffectsList::parameter(effect, QStringLiteral("out")).toInt() - EffectsList::parameter(effect, QStringLiteral("in")).toInt();
269
                refreshClip = true;
270
            } else {
271 272 273 274 275 276
                m_startFade = 0;
                refreshClip = true;
            }
        } else if (startFade1 >= 0 || startFade2 >= 0) {
            int current = qMax(startFade1, startFade2);
            QDomElement fade = effectAtIndex(current);
Laurent Montel's avatar
Laurent Montel committed
277
            m_startFade = EffectsList::parameter(fade, QStringLiteral("out")).toInt() - EffectsList::parameter(fade, QStringLiteral("in")).toInt();
278 279 280 281
            refreshClip = true;
        }

        // Check for fades out to display in timeline
Laurent Montel's avatar
Laurent Montel committed
282 283
        int endFade1 = m_effectList.hasEffect(QString(), QStringLiteral("fadeout"));
        int endFade2 = m_effectList.hasEffect(QString(), QStringLiteral("fade_to_black"));
284 285 286 287

        if (endFade1 >= 0 && endFade2 >= 0) {
            // We have 2 fade ins, only display if effect is selected
            if (ix == endFade1 || ix == endFade2) {
Laurent Montel's avatar
Laurent Montel committed
288
                m_endFade = EffectsList::parameter(effect, QStringLiteral("out")).toInt() - EffectsList::parameter(effect, QStringLiteral("in")).toInt();
289
                refreshClip = true;
290
            } else {
291 292 293 294 295 296
                m_endFade = 0;
                refreshClip = true;
            }
        } else if (endFade1 >= 0 || endFade2 >= 0) {
            int current = qMax(endFade1, endFade2);
            QDomElement fade = effectAtIndex(current);
Laurent Montel's avatar
Laurent Montel committed
297
            m_endFade = EffectsList::parameter(fade, QStringLiteral("out")).toInt() - EffectsList::parameter(fade, QStringLiteral("in")).toInt();
298 299
            refreshClip = true;
        }
300
        if (m_keyframeView.loadKeyframes(locale, effect, cropStart().frames(m_fps), cropDuration().frames(m_fps)) && !refreshClip) {
301 302 303
            if (editedKeyframe >= 0) {
                m_keyframeView.activeKeyframe = editedKeyframe;
            }
304
            update();
305
            return;
306
        }
307
    }
308

309
    if (refreshClip) {
310 311 312 313
        update();
    }
}

314
QStringList ClipItem::keyframes(const int index)
315
{
316
    QStringList result;
317
    QDomElement effect = m_effectList.at(index);
318
    QDomNodeList params = effect.elementsByTagName(QStringLiteral("parameter"));
319

320
    for (int i = 0; i < params.count(); ++i) {
321
        QDomElement e = params.item(i).toElement();
322 323 324 325 326
        if (e.isNull()) {
            continue;
        }
        if (e.attribute(QStringLiteral("type")) == QLatin1String("keyframe")
                || e.attribute(QStringLiteral("type")) == QLatin1String("simplekeyframe")) {
327
            result.append(e.attribute(QStringLiteral("keyframes")));
328
        }
329 330
        /*else if (e.attribute(QStringLiteral("type")) == QLatin1String("animated"))
            result.append(e.attribute(QStringLiteral("value")));*/
331 332 333 334
    }
    return result;
}

335 336
QDomElement ClipItem::selectedEffect()
{
337
    if (m_selectedEffect == -1 || m_effectList.isEmpty()) {
338
        return QDomElement();
339
    }
340
    return effectAtIndex(m_selectedEffect);
341 342
}

343
void ClipItem::resetThumbs(bool clearExistingThumbs)
344
{
345
    if (m_clipType == Image || m_clipType == Text || m_clipType == QText || m_clipType == Color || m_clipType == Audio || m_clipType == TextTemplate) {
346 347 348
        // These clip thumbnails are linked to bin thumbnail, not dynamic, nothing to do
        return;
    }
349 350 351
    if (clearExistingThumbs) {
        m_startPix = QPixmap();
        m_endPix = QPixmap();
352
        m_audioThumbCachePic.clear();
353
    }
354 355 356
    slotFetchThumbs();
}

357
void ClipItem::refreshClip(bool checkDuration, bool forceResetThumbs)
358
{
359
    if (checkDuration && m_binClip->hasLimitedDuration() && (m_maxDuration != m_binClip->duration())) {
360
        m_maxDuration = m_binClip->duration();
361
        if (m_clipType != Image && m_clipType != Text && m_clipType != QText && m_clipType != Color && m_clipType != TextTemplate) {
362
            if (m_maxDuration != GenTime() && m_info.cropStart + m_info.cropDuration > m_maxDuration) {
363
                // Clip duration changed, make sure to stay in correct range
364 365 366
                if (m_info.cropStart > m_maxDuration) {
                    m_info.cropStart = GenTime();
                    m_info.cropDuration = qMin(m_info.cropDuration, m_maxDuration);
367
                } else {
368
                    m_info.cropDuration = m_maxDuration;
369
                }
Till Theato's avatar
cleanup  
Till Theato committed
370
                updateRectGeometry();
371 372 373
            }
        }
    }
374
    if (m_clipType == Color) {
375
        m_baseColor = m_binClip->getProducerColorProperty(QStringLiteral("resource"));
376
        m_paintColor = m_baseColor;
377
        update();
378 379 380
    } else if (KdenliveSettings::videothumbnails()) {
        resetThumbs(forceResetThumbs);
    }
381 382
}

383 384
void ClipItem::slotFetchThumbs()
{
Laurent Montel's avatar
Laurent Montel committed
385
    if (scene() == nullptr || m_clipType == Audio || m_clipType == Color) {
386 387
        return;
    }
388
    if (m_clipType == Image || m_clipType == Text || m_clipType == QText || m_clipType == TextTemplate) {
389 390 391
        if (m_startPix.isNull()) {
            slotGetStartThumb();
        }
392 393 394
        return;
    }

Laurent Montel's avatar
Laurent Montel committed
395
    QList<int> frames;
396
    if (m_startPix.isNull()) {
397
        m_startThumbRequested = true;
398 399 400 401
        frames.append((int)m_speedIndependantInfo.cropStart.frames(m_fps));
    }

    if (m_endPix.isNull()) {
402
        m_endThumbRequested = true;
403
        frames.append((int)(m_speedIndependantInfo.cropStart + m_speedIndependantInfo.cropDuration).frames(m_fps) - 1);
404
    }
405

406
    if (!frames.isEmpty()) {
407
        m_binClip->slotExtractImage(frames);
408
    }
409 410
}

411 412 413
void ClipItem::stopThumbs()
{
    // Clip is about to be deleted, make sure we don't request thumbnails
Laurent Montel's avatar
Laurent Montel committed
414 415
    disconnect(&m_startThumbTimer, &QTimer::timeout, this, &ClipItem::slotGetStartThumb);
    disconnect(&m_endThumbTimer, &QTimer::timeout, this, &ClipItem::slotGetEndThumb);
416 417
}

418 419
void ClipItem::slotGetStartThumb()
{
420
    m_startThumbRequested = true;
421
    m_binClip->slotExtractImage(QList<int>() << (int)m_speedIndependantInfo.cropStart.frames(m_fps));
422 423
}

424 425
void ClipItem::slotGetEndThumb()
{
426
    m_endThumbRequested = true;
427
    m_binClip->slotExtractImage(QList<int>() << (int)(m_speedIndependantInfo.cropStart + m_speedIndependantInfo.cropDuration).frames(m_fps) - 1);
428 429
}

430
void ClipItem::slotSetStartThumb(const QImage &img)
431
{
432 433
    if (!img.isNull() && img.format() == QImage::Format_ARGB32) {
        QPixmap pix = QPixmap::fromImage(img);
434
        m_startPix = pix;
435 436 437
        QRectF r = boundingRect();
        double width = FRAME_SIZE / projectScene()->scale().x() * projectScene()->scale().y();
        r.setRight(r.left() + width + 2);
438
        update(r);
439 440 441
    }
}

442
void ClipItem::slotSetEndThumb(const QImage &img)
443
{
444 445
    if (!img.isNull() && img.format() == QImage::Format_ARGB32) {
        QPixmap pix = QPixmap::fromImage(img);
446
        m_endPix = pix;
447 448 449
        QRectF r = boundingRect();
        double width = FRAME_SIZE / projectScene()->scale().x() * projectScene()->scale().y();
        r.setLeft(r.right() - width - 2);
450 451
        update(r);
    }
452 453
}

Laurent Montel's avatar
Laurent Montel committed
454
void ClipItem::slotThumbReady(int frame, const QImage &img)
455
{
Laurent Montel's avatar
Laurent Montel committed
456
    if (scene() == nullptr) {
457 458
        return;
    }
459
    if (m_startThumbRequested && frame == m_speedIndependantInfo.cropStart.frames(m_fps)) {
460
        QRectF r = boundingRect();
461 462
        QPixmap pix = QPixmap::fromImage(img);
        double width = FRAME_SIZE / projectScene()->scale().x() * projectScene()->scale().y();
463 464
        m_startPix = pix;
        m_startThumbRequested = false;
465
        update(r.left(), r.top(), width, r.height());
466
        if (m_clipType == Image || m_clipType == Text || m_clipType == QText || m_clipType == TextTemplate) {
467 468
            update(r.right() - width, r.top(), width, pix.height());
        }
469
    } else if (m_endThumbRequested && frame == (m_speedIndependantInfo.cropStart + m_speedIndependantInfo.cropDuration).frames(m_fps) - 1) {
470
        QRectF r = boundingRect();
471
        QPixmap pix = QPixmap::fromImage(img);
472
        double width = FRAME_SIZE / projectScene()->scale().x() * projectScene()->scale().y();
473 474
        m_endPix = pix;
        m_endThumbRequested = false;
475
        update(r.right() - width, r.top(), width, r.height());
476 477 478
    } else if (projectScene()->scale().x() == FRAME_SIZE) {
        // We are in full zoom, each frame should be painted
        update();
479
    }
480 481
}

482
void ClipItem::slotSetStartThumb(const QPixmap &pix)
483
{
484 485 486
    m_startPix = pix;
}

487
void ClipItem::slotSetEndThumb(const QPixmap &pix)
488
{
489 490 491
    m_endPix = pix;
}

492 493
QPixmap ClipItem::startThumb() const
{
494 495 496
    return m_startPix;
}

497 498
QPixmap ClipItem::endThumb() const
{
499 500 501
    return m_endPix;
}

502 503
void ClipItem::slotGotAudioData()
{
504
    m_audioThumbReady = true;
505
    if (m_clipType == AV && m_clipState != PlaylistState::AudioOnly) {
506 507 508
        QRectF r = boundingRect();
        r.setTop(r.top() + r.height() / 2 - 1);
        update(r);
509 510 511
    } else {
        update();
    }
Marco Gittler's avatar
Marco Gittler committed
512 513
}

514 515
int ClipItem::type() const
{
516
    return AVWidget;
517 518
}

519 520
QDomElement ClipItem::xml() const
{
521
    return itemXml();
522 523
}

524 525
QDomElement ClipItem::itemXml() const
{
526 527
    QDomDocument doc;
    QDomElement xml = m_binClip->toXml(doc);
528 529 530 531 532 533 534 535 536 537 538
    if (m_speed != 1.0) {
        xml.setAttribute(QStringLiteral("speed"), m_speed);
    }
    if (m_strobe > 1) {
        xml.setAttribute(QStringLiteral("strobe"), m_strobe);
    }
    if (m_clipState == PlaylistState::AudioOnly) {
        xml.setAttribute(QStringLiteral("audio_only"), 1);
    } else if (m_clipState == PlaylistState::VideoOnly) {
        xml.setAttribute(QStringLiteral("video_only"), 1);
    }
539
    return doc.documentElement();
540 541
}

542
ClipType ClipItem::clipType() const
543
{
544
    return m_clipType;
545 546
}

547 548
QString ClipItem::clipName() const
{
549 550 551
    if (m_speed == 1.0) {
        return m_binClip->name();
    } else {
Laurent Montel's avatar
Laurent Montel committed
552
        return m_binClip->name() + QStringLiteral(" - ") + QString::number(m_speed * 100, 'f', 0) + QLatin1Char('%');
553
    }
554 555
}

556 557
void ClipItem::flashClip()
{
Laurent Montel's avatar
Laurent Montel committed
558
    if (m_timeLine == nullptr) {
559
        m_timeLine = new QTimeLine(500, this);
560
        m_timeLine->setUpdateInterval(80);
561
        m_timeLine->setCurveShape(QTimeLine::EaseInOutCurve);
562
        m_timeLine->setFrameRange(0, 100);
Laurent Montel's avatar
Laurent Montel committed
563
        connect(m_timeLine, &QTimeLine::valueChanged, this, &ClipItem::animate);
564
    }
565
    m_timeLine->start();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
566 567
}

568
void ClipItem::animate(qreal /*value*/)
569
{
570
    QRectF r = boundingRect();
571
    //r.setHeight(20);
572
    update(r);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
573 574
}

575 576 577
// virtual
void ClipItem::paint(QPainter *painter,
                     const QStyleOptionGraphicsItem *option,
578 579
                     QWidget *)
{
580
    QPalette palette = scene()->palette();
581
    QColor paintColor = m_paintColor;
582 583
    QColor textColor;
    QColor textBgColor;
584 585
    QPen framePen;
    if (isSelected() || (parentItem() && parentItem()->isSelected())) {
586 587
        textColor = palette.highlightedText().color();
        textBgColor = palette.highlight().color();
588
        framePen.setColor(textBgColor);
589
        paintColor.setRed(qMin((int) (paintColor.red() * 1.5), 255));
590
    } else {
591 592 593
        textColor = palette.text().color();
        textBgColor = palette.window().color();
        textBgColor.setAlpha(200);
594
        framePen.setColor(m_paintColor.darker());
595
    }
596
    const QRectF exposed = option->exposedRect;
597 598 599
    const QTransform transformation = painter->worldTransform();
    const QRectF mappedExposed = transformation.mapRect(exposed);
    const QRectF mapped = transformation.mapRect(rect());
600
    painter->setWorldMatrixEnabled(false);
601 602 603
    QPainterPath p;
    p.addRect(mappedExposed);
    QPainterPath q;
604 605 606 607 608
    if (KdenliveSettings::clipcornertype() == 0) {
        q.addRoundedRect(mapped, 3, 3);
    } else {
        q.addRect(mapped);
    }
609
    painter->setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform, false);
610 611 612
    painter->setClipPath(p.intersected(q));
    painter->setPen(Qt::NoPen);
    painter->fillRect(mappedExposed, paintColor);
613
    painter->setPen(m_paintColor.darker());
614
    if (m_clipState == PlaylistState::Disabled) {
615
        painter->setOpacity(0.3);
616
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
617
    // draw thumbnails
618
    if (KdenliveSettings::videothumbnails() && m_clipState != PlaylistState::AudioOnly && m_originalClipState != PlaylistState::AudioOnly) {
619
        QRectF thumbRect;
620
        if ((m_clipType == Image || m_clipType == Text || m_clipType == QText || m_clipType == TextTemplate) && !m_startPix.isNull()) {
621 622 623
            if (thumbRect.isNull()) {
                thumbRect = QRectF(0, 0, mapped.height() / m_startPix.height() * m_startPix.width(), mapped.height());
            }
624 625
            thumbRect.moveTopRight(mapped.topRight());
            painter->drawPixmap(thumbRect, m_startPix, m_startPix.rect());
626
        } else if (!m_endPix.isNull()) {
627 628 629
            if (thumbRect.isNull()) {
                thumbRect = QRectF(0, 0, mapped.height() / m_endPix.height() * m_endPix.width(), mapped.height());
            }
630 631
            thumbRect.moveTopRight(mapped.topRight());
            painter->drawPixmap(thumbRect, m_endPix, m_endPix.rect());
632
        }
633
        if (!m_startPix.isNull()) {
634 635 636
            if (thumbRect.isNull()) {
                thumbRect = QRectF(0, 0, mapped.height() / m_startPix.height() * m_startPix.width(), mapped.height());
            }
637 638
            thumbRect.moveTopLeft(mapped.topLeft());
            painter->drawPixmap(thumbRect, m_startPix, m_startPix.rect());
639
        }
640 641

        // if we are in full zoom, paint thumbnail for every frame
642
        if (clipType() != Color && clipType() != Audio && transformation.m11() == FRAME_SIZE) {
643
            int offset = (m_info.startPos - m_info.cropStart).frames(m_fps);
644 645
            int left = qMax((int) m_info.cropStart.frames(m_fps) + 1, (int) mapToScene(exposed.left(), 0).x() - offset);
            int right = qMin((int)(m_info.cropStart + m_info.cropDuration).frames(m_fps) - 1, (int) mapToScene(exposed.right(), 0).x() - offset);
646 647
            QPointF startPos = mapped.topLeft();
            int startOffset = m_info.cropStart.frames(m_fps);
648
            if (clipType() == Image || clipType() == Text || clipType() == QText || m_clipType == TextTemplate) {
649
                for (int i = left; i <= right; ++i) {
650
                    painter->drawPixmap(startPos + QPointF(FRAME_SIZE * (i - startOffset), 0), m_startPix);
651
                }
652
            } else {
653 654 655 656 657
                QImage img;
                QPen pen(Qt::white);
                pen.setStyle(Qt::DotLine);
                QSet <int> missing;
                for (int i = left; i <= right; ++i) {
658
                    QPointF xpos = startPos + QPointF(FRAME_SIZE * (i - startOffset), 0);
659
                    thumbRect.moveTopLeft(xpos);
660
                    img = m_binClip->findCachedThumb(i);
661 662 663
                    if (img.isNull()) {
                        missing << i;
                    } else {
664
                        painter->drawImage(thumbRect, img);
665
                    }
666 667 668 669
                    painter->drawLine(xpos, xpos + QPointF(0, mapped.height()));
                }
                if (!missing.isEmpty()) {
                    m_binClip->slotQueryIntraThumbs(missing.toList());
670 671
                }
            }
672
        }
673
    }
674
    // draw audio thumbnails
675
    if (KdenliveSettings::audiothumbnails() && m_speed == 1.0 && m_clipState != PlaylistState::VideoOnly && m_originalClipState != PlaylistState::VideoOnly && (((m_clipType == AV || m_clipType == Playlist) && (exposed.bottom() > (rect().height() / 2) || m_originalClipState == PlaylistState::AudioOnly || m_clipState == PlaylistState::AudioOnly)) || m_clipType == Audio) && m_audioThumbReady && !m_binClip->audioFrameCache.isEmpty()) {
676
        int startpixel = qMax(0, (int) exposed.left());
677
        int endpixel = qMax(0, (int)(exposed.right() + 0.5) + 1);
678
        QRectF mappedRect = mapped;
679
        if (m_clipType != Audio && m_clipState != PlaylistState::AudioOnly && m_originalClipState != PlaylistState::AudioOnly && KdenliveSettings::videothumbnails()) {
680
            mappedRect.setTop(mappedRect.bottom() - mapped.height() / 2);
681
        }
682

683
        double scale = transformation.m11();
684 685 686 687 688 689
        int channels = m_binClip->audioChannels();
        int cropLeft = m_info.cropStart.frames(m_fps);
        double startx = transformation.map(QPoint(startpixel, 0)).x();
        double endx = transformation.map(QPoint(endpixel, 0)).x();
        int offset = 1;
        if (scale < 1) {
690
            offset = (int)(1.0 / scale);
691
        }
692
        int audioLevelCount = m_binClip->audioFrameCache.count() - 1;
693 694 695
        if (!KdenliveSettings::displayallchannels()) {
            // simplified audio
            int channelHeight = mappedRect.height();
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
            int startOffset = startpixel + cropLeft;
            int i = startOffset;
            if (offset * scale > 1.0) {
                // Pixels are smaller than a frame, draw using painterpath
                QPainterPath positiveChannelPath;
                positiveChannelPath.moveTo(startx, mappedRect.bottom());
                for (; i < endpixel + cropLeft + offset; i += offset) {
                    double value = m_binClip->audioFrameCache.at(qMin(i * channels, audioLevelCount)).toDouble() / 256;
                    for (int channel = 1; channel < channels; channel ++) {
                        value = qMax(value, m_binClip->audioFrameCache.at(qMin(i * channels + channel, audioLevelCount)).toDouble() / 256);
                    }
                    positiveChannelPath.lineTo(startx + (i - startOffset) * scale, mappedRect.bottom() - (value * channelHeight));
                }
                positiveChannelPath.lineTo(startx + (i - startOffset) * scale, mappedRect.bottom());
                painter->setPen(Qt::NoPen);
                painter->setBrush(QBrush(QColor(80, 80, 150, 200)));
                painter->drawPath(positiveChannelPath);
            } else {
                // Pixels are larger than frames, draw simple lines
                painter->setPen(QColor(80, 80, 150, 200));
                i = startx;
                for (; i < endx; i++) {
                    int framePos = startOffset + ((i - startx) / scale);
                    double value = m_binClip->audioFrameCache.at(qMin(framePos * channels, audioLevelCount)).toDouble() / 256;
                    for (int channel = 1; channel < channels; channel ++) {
                        value = qMax(value, m_binClip->audioFrameCache.at(qMin(framePos * channels + channel, audioLevelCount)).toDouble() / 256);
                    }
                    painter->drawLine(i, mappedRect.bottom() - (value * channelHeight), i, mappedRect.bottom());
724 725
                }
            }
Vincent Pinon's avatar
Vincent Pinon committed
726
        } else if (channels >= 0) {
727
            int channelHeight = (int)(mappedRect.height() + 0.5) / channels;
728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773
            int startOffset = startpixel + cropLeft;
            double value = 0;
            if (offset * scale > 1.0) {
                // Pixels are smaller than a frame, draw using painterpath
                QMap<int, QPainterPath > positiveChannelPaths;
                QMap<int, QPainterPath > negativeChannelPaths;
                int i;
                painter->setPen(QColor(80, 80, 150));
                for (int channel = 0; channel < channels; channel ++) {
                    int y = channelHeight * channel + channelHeight / 2;
                    positiveChannelPaths[channel].moveTo(startx, mappedRect.bottom() - y);
                    negativeChannelPaths[channel].moveTo(startx, mappedRect.bottom() - y);
                    // Draw channel median line
                    i = startOffset;
                    painter->drawLine(startx, mappedRect.bottom() - y, endx, mappedRect.bottom() - y);
                    for (; i < endpixel + cropLeft + offset; i += offset) {
                        value = m_binClip->audioFrameCache.at(qMin(i * channels + channel, audioLevelCount)).toDouble() / 256 * channelHeight / 2;
                        positiveChannelPaths[channel].lineTo(startx + (i - startOffset) * scale, mappedRect.bottom() - y - value);
                        negativeChannelPaths[channel].lineTo(startx + (i - startOffset) * scale, mappedRect.bottom() - y + value);
                    }
                }
                painter->setPen(Qt::NoPen);
                painter->setBrush(QBrush(QColor(80, 80, 150, 200)));
                for (int channel = 0; channel < channels; channel ++) {
                    int y = channelHeight * channel + channelHeight / 2;
                    positiveChannelPaths[channel].lineTo(startx + (i - startOffset) * scale, mappedRect.bottom() - y);
                    negativeChannelPaths[channel].lineTo(startx + (i - startOffset) * scale, mappedRect.bottom() - y);
                    painter->drawPath(positiveChannelPaths.value(channel));
                    painter->drawPath(negativeChannelPaths.value(channel));
                }
            } else {
                // Pixels are larger than frames, draw simple lines
                painter->setPen(QColor(80, 80, 150));
                for (int channel = 0; channel < channels; channel ++) {
                    // Draw channel median line
                    painter->drawLine(startx, mappedRect.bottom() - (channelHeight * channel + channelHeight / 2), endx, mappedRect.bottom() - (channelHeight * channel + channelHeight / 2));
                }
                int i = startx;
                painter->setPen(QColor(80, 80, 150, 200));
                for (; i < endx; i++) {
                    int framePos = startOffset + ((i - startx) / scale);
                    for (int channel = 0; channel < channels; channel ++) {
                        int y = channelHeight * channel + channelHeight / 2;
                        value = m_binClip->audioFrameCache.at(qMin(framePos * channels + channel, audioLevelCount)).toDouble() / 256 * channelHeight / 2;
                        painter->drawLine(i, mappedRect.bottom() - value - y, i, mappedRect.bottom() - y + value);
                    }
774
                }
775
            }
776
        }
777
        painter->setPen(QPen());
778
    }
779
    if (m_clipState == PlaylistState::Disabled) {
780
        painter->setOpacity(1);
781
    }
782
    if (m_isMainSelectedClip) {
Laurent Montel's avatar
Laurent Montel committed
783
        framePen.setColor(Qt::red);
784
    }
785

786
    // only paint details if clip is big enough
787
    int fontUnit = QFontMetrics(painter->font()).lineSpacing();
788
    if (mapped.width() > (2 * fontUnit)) {
789 790 791 792 793 794 795 796
        // Check offset
        int effectOffset = 0;
        if (parentItem()) {
            //TODO: optimize, calculate offset only on resize or move
            AbstractGroupItem *grp = static_cast <AbstractGroupItem *>(parentItem());
            QGraphicsItem *other = grp->otherClip(this);
            if (other && other->type() == AVWidget) {
                ClipItem *otherClip = static_cast <ClipItem *>(other);
797
                if (otherClip->getBinId() == getBinId() && (startPos() - otherClip->startPos() != cropStart() - otherClip->cropStart())) {
798
                    painter->setPen(Qt::red);
799
                    QString txt = i18n("Offset: %1", (startPos() - cropStart() - otherClip->startPos() + otherClip->cropStart()).frames(m_fps));
800 801 802 803 804 805 806 807 808 809 810
                    QRectF txtBounding = painter->boundingRect(mapped, Qt::AlignLeft | Qt::AlignTop, txt);
                    painter->setBrush(Qt::red);
                    painter->setPen(Qt::NoPen);
                    painter->drawRoundedRect(txtBounding.adjusted(-1, -2, 4, -1), 3, 3);
                    painter->setPen(Qt::white);
                    painter->drawText(txtBounding.adjusted(2, 0, 1, -1), Qt::AlignCenter, txt);
                    effectOffset = txtBounding.width();
                }
            }
        }

811
        // Draw effects names
812
        if (!m_effectNames.isEmpty() && mapped.width() > (5 * fontUnit)) {
813
            QRectF txtBounding = painter->boundingRect(mapped, Qt::AlignLeft | Qt::AlignTop, m_effectNames);
814
            QColor bColor = palette.window().color();
815 816
            QColor tColor = palette.text().color();
            tColor.setAlpha(220);
817 818 819
            if (m_timeLine && m_timeLine->state() == QTimeLine::Running) {
                qreal value = m_timeLine->currentValue();
                txtBounding.setWidth(txtBounding.width() * value);
820 821 822
                bColor.setAlpha(100 + 50 * value);
            };

823 824
            painter->setBrush(bColor);
            painter->setPen(Qt::NoPen);
825
            painter->drawRoundedRect(txtBounding.adjusted(-1 + effectOffset, -2, 4 + effectOffset, -1), 3, 3);
826
            painter->setPen(tColor);
827
            painter->drawText(txtBounding.adjusted(2 + effectOffset, 0, 1 + effectOffset, -1), Qt::AlignCenter, m_effectNames);
828
        }
829

830
        // Draw clip name
831
        QString name = clipName();
832
        QRectF txtBounding2 = painter->boundingRect(mapped, Qt::AlignRight | Qt::AlignTop, name);
833
        painter->setPen(Qt::NoPen);
834 835 836 837 838
        if (m_clipState != PlaylistState::Original) {
            txtBounding2.adjust(-fontUnit, 0, fontUnit, 0);
        } else {
            fontUnit = 0;
        }
839
        if (txtBounding2.left() < mapped.left()) {