bin.cpp 191 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
/*
Copyright (C) 2012  Till Theato <root@ttill.de>
Copyright (C) 2014  Jean-Baptiste Mardelle <jb@kdenlive.org>
This file is part of Kdenlive. See www.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) version 3 or any later version
accepted by the membership of KDE e.V. (or its successor approved
11
by the membership of KDE e.V.), which shall act as a proxy
12
13
14
15
16
17
18
19
20
21
22
23
defined in Section 14 of version 3 of the license.

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, see <http://www.gnu.org/licenses/>.
*/

#include "bin.h"
Nicolas Carion's avatar
Nicolas Carion committed
24
#include "bincommands.h"
25
#include "clipcreator.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
26
27
#include "core.h"
#include "dialogs/clipcreationdialog.h"
28
#include "dialogs/timeremap.h"
Nicolas Carion's avatar
Nicolas Carion committed
29
30
31
#include "doc/documentchecker.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
32
#include "effects/effectstack/model/effectstackmodel.hpp"
33
#include "jobs/transcodetask.h"
34
#include "jobs/taskmanager.h"
35
#include "jobs/abstracttask.h"
36
#include "jobs/cliploadtask.h"
37
#include "kdenlive_debug.h"
38
#include "kdenlivesettings.h"
Nicolas Carion's avatar
Nicolas Carion committed
39
40
41
42
43
#include "mainwindow.h"
#include "mlt++/Mlt.h"
#include "mltcontroller/clipcontroller.h"
#include "mltcontroller/clippropertiescontroller.h"
#include "monitor/monitor.h"
Julius Künzel's avatar
Julius Künzel committed
44
#include "monitor/monitormanager.h"
45
#include "project/dialogs/slideshowclip.h"
Nicolas Carion's avatar
Nicolas Carion committed
46
#include "project/invaliddialog.h"
47
#include "project/transcodeseek.h"
48
#include "project/projectcommands.h"
Nicolas Carion's avatar
Nicolas Carion committed
49
50
51
52
#include "project/projectmanager.h"
#include "projectclip.h"
#include "projectfolder.h"
#include "projectitemmodel.h"
53
#include "projectsortproxymodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
54
#include "projectsubclip.h"
55
#include "tagwidget.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
56
57
#include "titler/titlewidget.h"
#include "ui_qtextclip_ui.h"
Nicolas Carion's avatar
Nicolas Carion committed
58
#include "undohelper.hpp"
59
#include "xml/xml.hpp"
60
#include <dialogs/textbasededit.h>
61
62
63
#include <memory>
#include <profiles/profilemodel.hpp>
#include <utils/thumbnailcache.hpp>
64

65
#include <KColorScheme>
66
#include <KRatingPainter>
67
#include <KMessageBox>
68
#include <KXMLGUIFactory>
69
#include <KIO/OpenFileManagerWindowJob>
70

71
#include <QToolBar>
Nicolas Carion's avatar
Nicolas Carion committed
72
#include <QCryptographicHash>
73
#include <QDrag>
Nicolas Carion's avatar
Nicolas Carion committed
74
#include <QFile>
75
#include <QMenu>
76
#include <QActionGroup>
Nicolas Carion's avatar
Nicolas Carion committed
77
78
#include <QSlider>
#include <QTimeLine>
79
#include <QUndoCommand>
Nicolas Carion's avatar
Nicolas Carion committed
80
81
#include <QUrl>
#include <QVBoxLayout>
Nicolas Carion's avatar
Nicolas Carion committed
82
#include <utility>
83
#include <jobs/audiolevelstask.h>
84

85
86
87
88
89
90
91
92
93
/**
 * @class BinItemDelegate
 * @brief This class is responsible for drawing items in the QTreeView.
 */
class BinItemDelegate : public QStyledItemDelegate
{
public:
    explicit BinItemDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent)
Nicolas Carion's avatar
Nicolas Carion committed
94

95
    {
96
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
97
    }
98
99
100
101
102
103
104
    void setEditorData(QWidget *w, const QModelIndex &i) const override
    {
        if (!m_editorOpen) {
            QStyledItemDelegate::setEditorData(w, i);
            m_editorOpen = true;
        }
    }
105
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
106
107
    {
        if (event->type() == QEvent::MouseButtonPress) {
108
            auto *me = static_cast<QMouseEvent *>(event);
109
110
111
112
113
114
115
116
            if (index.column() == 0) {
                if (m_audioDragRect.contains(me->pos())) {
                    dragType = PlaylistState::AudioOnly;
                } else if (m_videoDragRect.contains(me->pos())) {
                    dragType = PlaylistState::VideoOnly;
                } else {
                    dragType = PlaylistState::Disabled;
                }
117
118
            } else {
                dragType = PlaylistState::Disabled;
119
120
                if (index.column() == 7) {
                    // Rating
121
                    QRect rect = option.rect;
122
                    rect.adjust(option.rect.width() / 12, 0, 0, 0);
123
124
                    int rate = 0;
                    if (me->pos().x() > rect.x()) {
125
                        rate = KRatingPainter::getRatingFromPosition(rect, Qt::AlignLeft | Qt::AlignVCenter, qApp->layoutDirection(), me->pos());
126
                    }
127
128
129
130
131
                    if (rate > -1) {
                        // Full star rating only
                        if (rate %2 == 1) {
                            rate++;
                        }
132
                        emit static_cast<ProjectSortProxyModel *>(model)->updateRating(index, uint(rate));
133
134
                    }
                }
135
136
137
138
139
140
141
142
            }
        }
        event->ignore();
        return false;
    }
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() != 0) {
143
144
            QStyledItemDelegate::updateEditorGeometry(editor, option, index);
            return;
145
146
147
148
149
        }
        QStyleOptionViewItem opt = option;
        initStyleOption(&opt, index);
        QRect r1 = option.rect;
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
150
        int decoWidth = 0;
151
        if (opt.decorationSize.height() > 0) {
Vincent Pinon's avatar
Vincent Pinon committed
152
            decoWidth += int(r1.height() * pCore->getCurrentDar());
153
        }
154
        int mid = 0;
155
        if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
156
            mid = int((r1.height() / 2));
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
        }
        r1.adjust(decoWidth, 0, 0, -mid);
        QFont ft = option.font;
        ft.setBold(true);
        QFontMetricsF fm(ft);
        QRect r2 = fm.boundingRect(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString()).toRect();
        editor->setGeometry(r2);
    }

    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        QSize hint = QStyledItemDelegate::sizeHint(option, index);
        QString text = index.data(AbstractProjectItem::DataName).toString();
        QRectF r = option.rect;
        QFont ft = option.font;
        ft.setBold(true);
        QFontMetricsF fm(ft);
        QStyle *style = option.widget ? option.widget->style() : QApplication::style();
        const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
176
        int width = int(fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width()) + 2 * textMargin;
177
178
        hint.setWidth(width);
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
179
        if (type == AbstractProjectItem::FolderItem) {
180
181
182
183
184
185
            return QSize(hint.width(), qMin(option.fontMetrics.lineSpacing() + 4, hint.height()));
        }
        if (type == AbstractProjectItem::ClipItem) {
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMax(hint.height(), option.decorationSize.height())));
        }
        if (type == AbstractProjectItem::SubClipItem) {
186
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), int(option.decorationSize.height() / 1.5))));
187
188
189
190
191
        }
        QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
        QString line1 = index.data(Qt::DisplayRole).toString();
        QString line2 = index.data(Qt::UserRole).toString();

Vincent Pinon's avatar
Vincent Pinon committed
192
        int textW = qMax(option.fontMetrics.horizontalAdvance(line1), option.fontMetrics.horizontalAdvance(line2));
193
        QSize iconSize = icon.actualSize(option.decorationSize);
Nicolas Carion's avatar
Nicolas Carion committed
194
        return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4};
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
    }

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() == 0 && !index.data().isNull()) {
            QRect r1 = option.rect;
            painter->save();
            painter->setClipRect(r1);
            QStyleOptionViewItem opt(option);
            initStyleOption(&opt, index);
            int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
            QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
            const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1;
            // QRect r = QStyle::alignedRect(opt.direction, Qt::AlignVCenter | Qt::AlignLeft, opt.decorationSize, r1);

            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
211
            if ((option.state & static_cast<int>(QStyle::State_Selected)) != 0) {
212
213
214
215
216
217
218
219
220
                painter->setPen(option.palette.highlightedText().color());
            } else {
                painter->setPen(option.palette.text().color());
            }
            QRect r = r1;
            QFont font = painter->font();
            font.setBold(true);
            painter->setFont(font);
            if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
221
                int decoWidth = 0;
222
                FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
223
                if (opt.decorationSize.height() > 0) {
224
                    r.setWidth(int(r.height() * pCore->getCurrentDar()));
225
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
226
227
228
229
230
231
                    if (!pix.isNull()) {
                        // Draw icon
                        decoWidth += r.width() + textMargin;
                        r.setWidth(r.height() * pix.width() / pix.height());
                        painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
                    }
232
                    m_thumbRect = r;
233
                }
234
                // Draw frame in case of missing source
235
                int cType = index.data(AbstractProjectItem::ClipType).toInt();
236
                if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
237
                    painter->save();
238
                    painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
239
                    painter->drawRect(m_thumbRect.adjusted(0, 0, -1, -1));
240
                    painter->restore();
241
242
243
244
245
246
247
248
249
250
251
                } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
                    // Draw 'photo' frame to identify image clips
                    painter->save();
                    int penWidth = m_thumbRect.height() / 14;
                    penWidth += penWidth % 2;
                    painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
                    penWidth /= 2;
                    painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth - 1), 4, 4);
                    painter->setPen(QPen(Qt::black, 1));
                    painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, -1), 4, 4);
                    painter->restore();
252
                }
253
                int mid = int((r1.height() / 2));
254
255
256
257
258
259
260
261
                r1.adjust(decoWidth, 0, 0, -mid);
                QRect r2 = option.rect;
                r2.adjust(decoWidth, mid, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
                font.setBold(false);
                painter->setFont(font);
                QString subText = index.data(AbstractProjectItem::DataDuration).toString();
262
263
264
                QString tags = index.data(AbstractProjectItem::DataTag).toString();
                if (!tags.isEmpty()) {
                    QStringList t = tags.split(QLatin1Char(';'));
265
266
267
                    QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
                    tagRect.setWidth(r1.height() / 3.5);
                    tagRect.setHeight(tagRect.width());
Vincent Pinon's avatar
Vincent Pinon committed
268
                    for (const QString &color : qAsConst(t)) {
269
270
271
                        painter->setBrush(QColor(color));
                        painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
                        tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
272
273
                    }
                }
274
                if (!subText.isEmpty()) {
275
                    r2.adjust(0, int(bounding.bottom() - r2.top()), 0, 0);
276
277
278
279
280
281
                    QColor subTextColor = painter->pen().color();
                    subTextColor.setAlphaF(.5);
                    painter->setPen(subTextColor);
                    // Draw usage counter
                    int usage = index.data(AbstractProjectItem::UsageCount).toInt();
                    if (usage > 0) {
Laurent Montel's avatar
Laurent Montel committed
282
                        subText.append(QString::asprintf(" [%d]", usage));
283
284
                    }
                    painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding);
285
                    // Add audio/video icons for selective drag
286
287
                    bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
                    if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) {
288
289
290
291
292
293
294
295
296
297
298
                        bounding.moveLeft(bounding.right() + (2 * textMargin));
                        bounding.adjust(0, textMargin, 0, -textMargin);
                        QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
                        m_audioDragRect = bounding.toRect();
                        m_audioDragRect.setWidth(m_audioDragRect.height());
                        aDrag.paint(painter, m_audioDragRect, Qt::AlignLeft);
                        m_videoDragRect = m_audioDragRect;
                        m_videoDragRect.moveLeft(m_audioDragRect.right());
                        QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video"));
                        vDrag.paint(painter, m_videoDragRect, Qt::AlignLeft);
                    } else {
299
300
                        //m_audioDragRect = QRect();
                        //m_videoDragRect = QRect();
301
302
303
304
305
306
307
                    }
                }
                if (type == AbstractProjectItem::ClipItem) {
                    // Overlay icon if necessary
                    QVariant v = index.data(AbstractProjectItem::IconOverlay);
                    if (!v.isNull()) {
                        QIcon reload = QIcon::fromTheme(v.toString());
308
309
                        r.setTop(int(r.bottom() - bounding.height()));
                        r.setWidth(int(bounding.height()));
310
311
312
                        reload.paint(painter, r);
                    }
                    int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
313
314
                    auto status = index.data(AbstractProjectItem::JobStatus).value<TaskManagerStatus>();
                    if (status == TaskManagerStatus::Pending || status == TaskManagerStatus::Running) {
315
316
317
318
319
320
                        // Draw job progress bar
                        int progressWidth = option.fontMetrics.averageCharWidth() * 8;
                        int progressHeight = option.fontMetrics.ascent() / 4;
                        QRect progress(r1.x() + 1, opt.rect.bottom() - progressHeight - 2, progressWidth, progressHeight);
                        painter->setPen(Qt::NoPen);
                        painter->setBrush(Qt::darkGray);
321
                        if (status == TaskManagerStatus::Running) {
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
                            painter->drawRoundedRect(progress, 2, 2);
                            painter->setBrush((option.state & static_cast<int>((QStyle::State_Selected) != 0)) != 0 ? option.palette.text()
                                                                                                                    : option.palette.highlight());
                            progress.setWidth((progressWidth - 2) * jobProgress / 100);
                            painter->drawRoundedRect(progress, 2, 2);
                        } else {
                            // Draw kind of a pause icon
                            progress.setWidth(3);
                            painter->drawRect(progress);
                            progress.moveLeft(progress.right() + 3);
                            painter->drawRect(progress);
                        }
                    }
                    bool jobsucceeded = index.data(AbstractProjectItem::JobSuccess).toBool();
                    if (!jobsucceeded) {
                        QIcon warning = QIcon::fromTheme(QStringLiteral("process-stop"));
                        warning.paint(painter, r2);
                    }
                }
            } else {
342
                // Folder
343
344
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
345
                    r.setWidth(int(r.height() * pCore->getCurrentDar()));
346
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
347
                    // Draw icon
348
349
350
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
351
352
353
354
355
356
                }
                r1.adjust(decoWidth, 0, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
            }
            painter->restore();
357
        } else if (index.column() == 7) {
358
359
360
            QStyleOptionViewItem opt(option);
            initStyleOption(&opt, index);
            QRect r1 = opt.rect;
361
362
            // Tweak bg opacity since breeze dark star has same color as highlighted background
            painter->setOpacity(0.5);
363
364
            QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
            style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget);
365
            painter->setOpacity(1);
366
            if (index.data(AbstractProjectItem::ItemTypeRole).toInt() != AbstractProjectItem::FolderItem) {
367
                r1.adjust(r1.width() / 12, 0, 0, 0);
368
                KRatingPainter::paintRating(painter, r1, Qt::AlignLeft | Qt::AlignVCenter, index.data().toInt());
369
            }
370
371
372
373
        } else {
            QStyledItemDelegate::paint(painter, option, index);
        }
    }
374
375
376
377

    int getFrame(QModelIndex index, int mouseX)
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
378
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)) {
379
380
            return 0;
        }
381
382
383
        if (mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
            return -1;
        }
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
        return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width();
    }

private:
    mutable bool m_editorOpen{false};
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
    mutable QRect m_thumbRect;

public:
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
};

/**
 * @class BinListItemDelegate
 * @brief This class is responsible for drawing items in the QListView.
 */

class BinListItemDelegate : public QStyledItemDelegate
{
public:
    explicit BinListItemDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent)

    {
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
    }
411
412
413
414
415
416
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
    {
        Q_UNUSED(model);
        Q_UNUSED(option);
        Q_UNUSED(index);
        if (event->type() == QEvent::MouseButtonPress) {
417
            auto *me = static_cast<QMouseEvent *>(event);
418
419
420
421
422
423
424
425
426
427
428
            if (m_audioDragRect.contains(me->pos())) {
                dragType = PlaylistState::AudioOnly;
            } else if (m_videoDragRect.contains(me->pos())) {
                dragType = PlaylistState::VideoOnly;
            } else {
                dragType = PlaylistState::Disabled;
            }
        }
        event->ignore();
        return false;
    }
429
430
431
432

    void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (!index.data().isNull()) {
433
            QStyleOptionViewItem opt(option);
434
            initStyleOption(&opt, index);
435
            QStyledItemDelegate::paint(painter, option, index);
436
            int adjust = (opt.rect.width() - opt.decorationSize.width()) / 2;
437
            QRect rect(opt.rect.x(), opt.rect.y(), opt.decorationSize.width(), opt.decorationSize.height());
438
439
440
441
            if (adjust > 0 && adjust < rect.width()) {
                rect.translate(adjust, 0);
            }
            m_thumbRect = rect;
442

443
444
445
446
447
448
449
            //Tags
            QString tags = index.data(AbstractProjectItem::DataTag).toString();
            if (!tags.isEmpty()) {
                QStringList t = tags.split(QLatin1Char(';'));
                QRectF tagRect = m_thumbRect.adjusted(2, 2, 0, 2);
                tagRect.setWidth(m_thumbRect.height() / 5);
                tagRect.setHeight(tagRect.width());
Vincent Pinon's avatar
Vincent Pinon committed
450
                for (const QString &color : qAsConst(t)) {
451
452
453
454
455
                    painter->setBrush(QColor(color));
                    painter->drawRoundedRect(tagRect, tagRect.height() / 2, tagRect.height() / 2);
                    tagRect.moveTop(tagRect.bottom() + tagRect.height() / 4);
                }
            }
456

457
            // Add audio/video icons for selective drag
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
            int cType = index.data(AbstractProjectItem::ClipType).toInt();
            bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
            if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) {
                QRect thumbRect = m_thumbRect;
                int iconSize = painter->boundingRect(thumbRect, Qt::AlignLeft, QStringLiteral("O")).height();
                thumbRect.setLeft(opt.rect.right() - iconSize - 4);
                thumbRect.setWidth(iconSize);
                thumbRect.setBottom(m_thumbRect.top() + iconSize);
                QIcon aDrag = QIcon::fromTheme(QStringLiteral("audio-volume-medium"));
                m_audioDragRect = thumbRect;
                aDrag.paint(painter, m_audioDragRect, Qt::AlignRight);
                m_videoDragRect = m_audioDragRect;
                m_videoDragRect.moveTop(thumbRect.bottom());
                QIcon vDrag = QIcon::fromTheme(QStringLiteral("kdenlive-show-video"));
                vDrag.paint(painter, m_videoDragRect, Qt::AlignRight);
            } else {
                //m_audioDragRect = QRect();
                //m_videoDragRect = QRect();
            }
477
            // Draw frame in case of missing source
478
            FileStatus::ClipStatus clipStatus = FileStatus::ClipStatus(index.data(AbstractProjectItem::ClipStatus).toInt());
479
            if (clipStatus == FileStatus::StatusMissing || clipStatus == FileStatus::StatusProxyOnly) {
480
                painter->save();
481
                painter->setPen(QPen(clipStatus == FileStatus::StatusProxyOnly ? Qt::yellow : Qt::red, 3));
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
                painter->drawRect(m_thumbRect);
                painter->restore();
            } else if (cType == ClipType::Image || cType == ClipType::SlideShow) {
                // Draw 'photo' frame to identify image clips
                painter->save();
                int penWidth = m_thumbRect.height() / 14;
                penWidth += penWidth % 2;
                painter->setPen(QPen(QColor(255, 255, 255, 160), penWidth));
                penWidth /= 2;
                painter->drawRoundedRect(m_thumbRect.adjusted(penWidth, penWidth, -penWidth - 1, -penWidth + 1), 4, 4);
                painter->setPen(QPen(Qt::black, 1));
                painter->drawRoundedRect(m_thumbRect.adjusted(0, 0, -1, 1), 4, 4);
                painter->restore();
            }
            int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
            if (type == AbstractProjectItem::ClipItem) {
                // Overlay icon if necessary
                QVariant v = index.data(AbstractProjectItem::IconOverlay);
                if (!v.isNull()) {
                    QRect r = m_thumbRect;
                    QIcon reload = QIcon::fromTheme(v.toString());
                    r.setTop(r.bottom() - (opt.rect.height() - r.height()));
                    r.setWidth(r.height());
                    reload.paint(painter, r);
                }
            }
508
509
510
        }
    }

511
512
513
    int getFrame(QModelIndex index, int mouseX)
    {
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
514
        if ((type != AbstractProjectItem::ClipItem && type != AbstractProjectItem::SubClipItem)|| mouseX < m_thumbRect.x() || mouseX > m_thumbRect.right()) {
515
516
517
518
            return 0;
        }
        return 100 * (mouseX - m_thumbRect.x()) / m_thumbRect.width();
    }
519
520

private:
Nicolas Carion's avatar
Nicolas Carion committed
521
    mutable bool m_editorOpen{false};
522
523
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
524
    mutable QRect m_thumbRect;
525
526

public:
Nicolas Carion's avatar
Nicolas Carion committed
527
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
528
529
};

530

531
532
MyListView::MyListView(QWidget *parent)
    : QListView(parent)
533
534
535
536
{
    setViewMode(QListView::IconMode);
    setMovement(QListView::Static);
    setResizeMode(QListView::Adjust);
537
    setWordWrap(true);
538
539
540
541
542
543
544
545
546
    setDragDropMode(QAbstractItemView::DragDrop);
    setAcceptDrops(true);
    setDragEnabled(true);
    viewport()->setAcceptDrops(true);
}

void MyListView::focusInEvent(QFocusEvent *event)
{
    QListView::focusInEvent(event);
547
548
549
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
550
551
}

552
553
554
void MyListView::enterEvent(QEvent *event)
{
    QListView::enterEvent(event);
555
    pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
556
557
558
559
560
}

void MyListView::leaveEvent(QEvent *event)
{
    QListView::leaveEvent(event);
561
    pCore->setWidgetKeyBinding();
562
563
}

564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
void MyListView::mousePressEvent(QMouseEvent *event)
{
    QListView::mousePressEvent(event);
    if (event->button() == Qt::LeftButton) {
        m_startPos = event->pos();
        QModelIndex ix = indexAt(m_startPos);
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
            m_dragType = static_cast<BinListItemDelegate *>(del)->dragType;
        } else {
            m_dragType = PlaylistState::Disabled;
        }
        emit updateDragMode(m_dragType);
    }
}

580
581
void MyListView::mouseMoveEvent(QMouseEvent *event)
{
582
583
584
    QModelIndex index = indexAt(event->pos());
    if (index.isValid()) {
        if (KdenliveSettings::hoverPreview()) {
585
            QAbstractItemDelegate *del = itemDelegate(index);
586
587
588
            if (del) {
                auto delegate = static_cast<BinListItemDelegate *>(del);
                QRect vRect = visualRect(index);
589
590
591
592
                if (vRect.contains(event->pos())) {
                    int frame = delegate->getFrame(index, event->pos().x() - vRect.x());
                    emit displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
                }
593
594
595
            } else {
                qDebug()<<"<<< NO DELEGATE!!!";
            }
596
597
598
599
600
601
602
603
604
            if (m_lastHoveredItem != index) {
                if (m_lastHoveredItem.isValid()) {
                    emit displayBinFrame(m_lastHoveredItem, -1);
                }
                m_lastHoveredItem = index;
            }
            pCore->window()->showKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
        } else {
            pCore->window()->showKeyBinding(i18n("<b>F2</b> to rename selected item"));
605
        }
606
607
    } else {
        pCore->window()->showKeyBinding();
608
609
610
611
        if (m_lastHoveredItem.isValid()) {
            emit displayBinFrame(m_lastHoveredItem, -1);
            m_lastHoveredItem = QModelIndex();
        }
612
613
614
615
    }
    QListView::mouseMoveEvent(event);
}

616
617
MyTreeView::MyTreeView(QWidget *parent)
    : QTreeView(parent)
618
619
{
    setEditing(false);
620
    setAcceptDrops(true);
621
}
622
623
624

void MyTreeView::mousePressEvent(QMouseEvent *event)
{
625
    QTreeView::mousePressEvent(event);
626
627
    if (event->button() == Qt::LeftButton) {
        m_startPos = event->pos();
628
629
630
        QModelIndex ix = indexAt(m_startPos);
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
631
            m_dragType = static_cast<BinItemDelegate *>(del)->dragType;
632
633
634
        } else {
            m_dragType = PlaylistState::Disabled;
        }
635
636
637
    }
}

638
639
640
void MyTreeView::focusInEvent(QFocusEvent *event)
{
    QTreeView::focusInEvent(event);
641
642
643
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
644
645
}

646
647
648
void MyTreeView::enterEvent(QEvent *event)
{
    QTreeView::enterEvent(event);
649
    pCore->setWidgetKeyBinding(i18n("<b>Double click</b> to add a file to the project"));
650
651
652
653
654
}

void MyTreeView::leaveEvent(QEvent *event)
{
    QTreeView::leaveEvent(event);
655
    pCore->setWidgetKeyBinding();
656
657
}

658
void MyTreeView::mouseMoveEvent(QMouseEvent *event)
659
660
{
    bool dragged = false;
661
    if ((event->buttons() & Qt::LeftButton) != 0u) {
662
        int distance = (event->pos() - m_startPos).manhattanLength();
663
664
665
        if (distance >= QApplication::startDragDistance()) {
            dragged = performDrag();
        }
666
    } else {
667
668
        QModelIndex index = indexAt(event->pos());
        if (index.isValid()) {
669
670
671
            if (KdenliveSettings::hoverPreview()) {
                QAbstractItemDelegate *del = itemDelegate(index);
                int frame = static_cast<BinItemDelegate *>(del)->getFrame(index, event->pos().x());
672
673
674
675
676
677
678
679
                if (frame >= 0) {
                    emit displayBinFrame(index, frame, event->modifiers() & Qt::ShiftModifier);
                    if (m_lastHoveredItem != index) {
                        if (m_lastHoveredItem.isValid()) {
                            emit displayBinFrame(m_lastHoveredItem, -1);
                        }
                        m_lastHoveredItem = index;
                    }
680
681
682
683
684
                } else {
                    if (m_lastHoveredItem.isValid()) {
                        emit displayBinFrame(m_lastHoveredItem, -1);
                        m_lastHoveredItem = QModelIndex();
                    }
685
686
687
688
                }
                pCore->window()->showKeyBinding(i18n("<b>Shift+seek</b> over thumbnail to set default thumbnail, <b>F2</b> to rename selected item"));
            } else {
                pCore->window()->showKeyBinding(i18n("<b>F2</b> to rename selected item"));
689
690
            }
        } else {
691
692
693
694
            if (m_lastHoveredItem.isValid()) {
                emit displayBinFrame(m_lastHoveredItem, -1);
                m_lastHoveredItem = QModelIndex();
            }
695
            pCore->window()->showKeyBinding();
696
        }
697
698
699
    }
    if (!dragged) {
        QTreeView::mouseMoveEvent(event);
700
701
702
    }
}

703
704
705
706
707
708
709
710
711
712
713
714
void MyTreeView::closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint)
{
    QAbstractItemView::closeEditor(editor, hint);
    setEditing(false);
}

void MyTreeView::editorDestroyed(QObject *editor)
{
    QAbstractItemView::editorDestroyed(editor);
    setEditing(false);
}

Laurent Montel's avatar
Laurent Montel committed
715
bool MyTreeView::isEditing() const
716
{
717
    return state() == QAbstractItemView::EditingState;
718
719
720
721
}

void MyTreeView::setEditing(bool edit)
{
722
    setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState);
723
724
725
726
    if (!edit) {
        // Ensure edited item is selected
        emit selectCurrent();
    }
727
728
}

729
730
731
732
733
734
735
736
737
bool MyTreeView::performDrag()
{
    QModelIndexList bases = selectedIndexes();
    QModelIndexList indexes;
    for (int i = 0; i < bases.count(); i++) {
        if (bases.at(i).column() == 0) {
            indexes << bases.at(i);
        }
    }
738
739
740
    if (indexes.isEmpty()) {
        return false;
    }
741
742
    // Check if we want audio or video only
    emit updateDragMode(m_dragType);
Nicolas Carion's avatar
Nicolas Carion committed
743
    auto *drag = new QDrag(this);
744
    drag->setMimeData(model()->mimeData(indexes));
Laurent Montel's avatar
Laurent Montel committed
745
    QModelIndex ix = indexes.constFirst();
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
    if (ix.isValid()) {
        QIcon icon = ix.data(AbstractProjectItem::DataThumbnail).value<QIcon>();
        QPixmap pix = icon.pixmap(iconSize());
        QSize size = pix.size();
        QImage image(size, QImage::Format_ARGB32_Premultiplied);
        image.fill(Qt::transparent);
        QPainter p(&image);
        p.setOpacity(0.7);
        p.drawPixmap(0, 0, pix);
        p.setOpacity(1);
        if (indexes.count() > 1) {
            QPalette palette;
            int radius = size.height() / 3;
            p.setBrush(palette.highlight());
            p.setPen(palette.highlightedText().color());
            p.drawEllipse(QPoint(size.width() / 2, size.height() / 2), radius, radius);
            p.drawText(size.width() / 2 - radius, size.height() / 2 - radius, 2 * radius, 2 * radius, Qt::AlignCenter, QString::number(indexes.count()));
        }
        p.end();
        drag->setPixmap(QPixmap::fromImage(image));
    }
    drag->exec();
768
    emit processDragEnd();
769
770
771
    return true;
}

772
773
SmallJobLabel::SmallJobLabel(QWidget *parent)
    : QPushButton(parent)
Nicolas Carion's avatar
Nicolas Carion committed
774

775
776
777
778
{
    setFixedWidth(0);
    setFlat(true);
    m_timeLine = new QTimeLine(500, this);
Laurent Montel's avatar
Laurent Montel committed
779
780
    QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged);
    QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished);
781
782
783
784
785
    hide();
}

const QString SmallJobLabel::getStyleSheet(const QPalette &p)
{
786
    KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window);
787
788
    QColor bg = scheme.background(KColorScheme::LinkBackground).color();
    QColor fg = scheme.foreground(KColorScheme::LinkText).color();
Nicolas Carion's avatar
Nicolas Carion committed
789
790
791
792
793
794
795
796
    QString style =
        QStringLiteral("QPushButton {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
            .arg(bg.red())
            .arg(bg.green())
            .arg(bg.blue())
            .arg(fg.red())
            .arg(fg.green())
            .arg(fg.blue());
797

798
799
    bg = scheme.background(KColorScheme::ActiveBackground).color();
    fg = scheme.foreground(KColorScheme::ActiveText).color();
Nicolas Carion's avatar
Nicolas Carion committed
800
801
802
803
804
805
806
807
    style.append(
        QStringLiteral("\nQPushButton:hover {margin:3px;padding:2px;background-color: rgb(%1, %2, %3);border-radius: 4px;border: none;color: rgb(%4, %5, %6)}")
            .arg(bg.red())
            .arg(bg.green())
            .arg(bg.blue())
            .arg(fg.red())
            .arg(fg.green())
            .arg(fg.blue()));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
808

809
810
811
812
813
814
815
816
817
818
    return style;
}

void SmallJobLabel::setAction(QAction *action)
{
    m_action = action;
}

void SmallJobLabel::slotTimeLineChanged(qreal value)
{
819
    setFixedWidth(int(qMin(value * 2, qreal(1.0)) * sizeHint().width()));
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
    update();
}

void SmallJobLabel::slotTimeLineFinished()
{
    if (m_timeLine->direction() == QTimeLine::Forward) {
        // Show
        m_action->setVisible(true);
    } else {
        // Hide
        m_action->setVisible(false);
        setText(QString());
    }
}

void SmallJobLabel::slotSetJobCount(int jobCount)
{
Nicolas Carion's avatar
Nicolas Carion committed
837
    QMutexLocker lk(&m_locker);
838
839
840
841
    if (jobCount > 0) {
        // prepare animation
        setText(i18np("%1 job", "%1 jobs", jobCount));
        setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount));
842

Nicolas Carion's avatar
Nicolas Carion committed
843
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
844
845
846
847
            setFixedWidth(sizeHint().width());
            m_action->setVisible(true);
            return;
        }
848

849
850
851
852
853
        if (m_action->isVisible()) {
            setFixedWidth(sizeHint().width());
            update();
            return;
        }
854

855
856
857
858
859
860
861
862
        setFixedWidth(0);
        m_action->setVisible(true);
        int wantedWidth = sizeHint().width();
        setGeometry(-wantedWidth, 0, wantedWidth, height());
        m_timeLine->setDirection(QTimeLine::Forward);
        if (m_timeLine->state() == QTimeLine::NotRunning) {
            m_timeLine->start();
        }
863
    } else {
Nicolas Carion's avatar
Nicolas Carion committed
864
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
865
866
867
868
869
870
871
872
873
874
875
876
            setFixedWidth(0);
            m_action->setVisible(false);
            return;
        }
        // hide
        m_timeLine->setDirection(QTimeLine::Backward);
        if (m_timeLine->state() == QTimeLine::NotRunning) {
            m_timeLine->start();
        }
    }
}

877
878
LineEventEater::LineEventEater(QObject *parent)
    : QObject(parent)
879
880
881
882
883
{
}

bool LineEventEater::eventFilter(QObject *obj, QEvent *event)
{
884
    switch (event->type()) {
885
    case QEvent::ShortcutOverride:
886
        if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
887
888
889
890
891
            emit clearSearchLine();
        }
        break;
    case QEvent::Resize:
        // Workaround Qt BUG 54676
892
        emit showClearButton(static_cast<QResizeEvent *>(event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8);
893
894
895
        break;
    default:
        break;
896
897
898
899
    }
    return QObject::eventFilter(obj, event);
}

900
void ClipWidget::init(QDockWidget* m_DockClipWidget)
Vivek Yadav's avatar
Vivek Yadav committed
901
{
902
    ClipCreationDialog::clipWidget(m_DockClipWidget);
Vivek Yadav's avatar
Vivek Yadav committed
903
}
Vivek Yadav's avatar
Vivek Yadav committed
904

Nicolas Carion's avatar
Nicolas Carion committed
905
Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent)
906
907
    : QWidget(parent)
    , isLoading(false)
Nicolas Carion's avatar
Nicolas Carion committed
908
    , m_itemModel(std::move(model))
909
    , m_itemView(nullptr)
910
911
    , m_binTreeViewDelegate(nullptr)
    , m_binListViewDelegate(nullptr)
912
913
914
915
916
    , m_doc(nullptr)
    , m_extractAudioAction(nullptr)
    , m_transcodeAction(nullptr)
    , m_clipsActionsMenu(nullptr)
    , m_inTimelineAction(nullptr)
917
    , m_listType(BinViewType(KdenliveSettings::binMode()))
918
919
920
    , m_iconSize(160, 90)
    , m_propertiesPanel(nullptr)
    , m_blankThumb()
921
    , m_clipWidget()
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
922
    , m_filterGroup(this)
923
924
    , m_filterRateGroup(this)
    , m_filterTypeGroup(this)
925
    , m_invalidClipDialog(nullptr)
926
    , m_transcodingDialog(nullptr)
927
928
929
    , m_gainedFocus(false)
    , m_audioDuration(0)
    , m_processedAudio(0)
930
{
931
    m_layout = new QVBoxLayout(this);
932
933

    // Create toolbar for buttons
934
    m_toolbar = new QToolBar(this);
935
936
937
    int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
    QSize iconSize(size, size);
    m_toolbar->setIconSize(iconSize);
938
    m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
939
    m_layout->addWidget(m_toolbar);
940
941
942
943

    // Tags panel
    m_tagsWidget = new TagWidget(this);
    connect(m_tagsWidget, &TagWidget::switchTag, this, &Bin::switchTag);
944
    connect(m_tagsWidget, &TagWidget::updateProjectTags, this, &Bin::updateTags);
945
946
947
    m_tagsWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
    m_layout->addWidget(m_tagsWidget);
    m_tagsWidget->setVisible(false);
948

949
950
    m_layout->setSpacing(0);
    m_layout->setContentsMargins(0, 0, 0, 0);
951
    // Search line
952
953
    m_searchLine = new QLineEdit(this);
    m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
954
    m_searchLine->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
Nicolas Carion's avatar
Nicolas Carion committed
955
    // m_searchLine->setClearButtonEnabled(true);
956
    m_searchLine->setPlaceholderText(i18n("Search..."));
957
    m_searchLine->setFocusPolicy(Qt::ClickFocus);
958
959
960
961
962
963
    connect(m_searchLine, &QLineEdit::textChanged, [this] (const QString &str) {
        m_proxyModel->slotSetSearchString(str);
        if (str.isEmpty()) {
            // focus last selected item when clearing search line
            QModelIndex current = m_proxyModel->selectionModel()->currentIndex();
            if (current.isValid()) {
964
                m_itemView->scrollTo(current, QAbstractItemView::EnsureVisible);
965
966
967
            }
        }
    });
968

Nicolas Carion's avatar
Nicolas Carion committed
969
    auto *leventEater = new LineEventEater(this);
970
    m_searchLine->installEventFilter(leventEater);
Laurent Montel's avatar
Laurent Montel committed
971
    connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear);
972
    connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton);
973

974
    setFocusPolicy(Qt::ClickFocus);
975

976
    connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel);
977
    connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip);
Nicolas Carion's avatar
Nicolas Carion committed
978
979
980
    connect(m_itemModel.get(), static_cast<void (ProjectItemModel::*)(const QStringList &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this,
            static_cast<void (Bin::*)(const QStringList &, const QModelIndex &)>(&Bin::slotItemDropped));
    connect(m_itemModel.get(), static_cast<void (ProjectItemModel::*)(const QList<QUrl> &, const QModelIndex &)>(&ProjectItemModel::itemDropped), this,
981
            static_cast<const QString (Bin::*)(const QList<QUrl> &, const QModelIndex &)>(&Bin::slotItemDropped));
982
    connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped);
983
    connect(m_itemModel.get(), &ProjectItemModel::addTag, this, &Bin::slotTagDropped);
984
    connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited);
985
    connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel);
986

987
    // Zoom slider
988
    QWidget *container = new QWidget(this);
Nicolas Carion's avatar
Nicolas Carion committed
989
    auto *lay = new QHBoxLayout;
990
991
    m_slider = new QSlider(Qt::Horizontal, this);
    m_slider->setMaximumWidth(100);
992
    m_slider->setMinimumWidth(40);
993
    m_slider->setRange(0, 10);
994
    m_slider->setValue(KdenliveSettings::bin_zoom());
Laurent Montel's avatar
Laurent Montel committed
995
    connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize);
Nicolas Carion's avatar
Nicolas Carion committed
996
    auto *tb1 = new QToolButton(this);
997
    tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
Vincent Pinon's avatar
Vincent Pinon committed
998
    connect(tb1, &QToolButton::clicked, this, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); });
Nicolas Carion's avatar
Nicolas Carion committed
999
    auto *tb2 = new QToolButton(this);
1000
    tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out")));
Vincent Pinon's avatar
Vincent Pinon committed
1001
    connect(tb2, &QToolButton::clicked, this, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); });
1002
    lay->addWidget(tb2);
1003
1004
    lay->addWidget(m_slider);
    lay->addWidget(tb1);
1005
    container->setLayout(lay);
Nicolas Carion's avatar
Nicolas Carion committed
1006
    auto *widgetslider = new QWidgetAction(this);
1007
    widgetslider->setDefaultWidget(container);