bin.cpp 125 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 28 29 30
#include "core.h"
#include "dialogs/clipcreationdialog.h"
#include "doc/documentchecker.h"
#include "doc/docundostack.hpp"
#include "doc/kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
31
#include "effects/effectstack/model/effectstackmodel.hpp"
32
#include "jobs/audiothumbjob.hpp"
33 34
#include "jobs/jobmanager.h"
#include "jobs/loadjob.hpp"
35
#include "jobs/thumbjob.hpp"
36
#include "kdenlive_debug.h"
37
#include "kdenlivesettings.h"
Nicolas Carion's avatar
Nicolas Carion committed
38 39 40 41 42
#include "mainwindow.h"
#include "mlt++/Mlt.h"
#include "mltcontroller/clipcontroller.h"
#include "mltcontroller/clippropertiescontroller.h"
#include "monitor/monitor.h"
43
#include "project/dialogs/slideshowclip.h"
Nicolas Carion's avatar
Nicolas Carion committed
44
#include "project/invaliddialog.h"
45
#include "project/projectcommands.h"
Nicolas Carion's avatar
Nicolas Carion committed
46 47 48 49 50
#include "project/projectmanager.h"
#include "projectclip.h"
#include "projectfolder.h"
#include "projectfolderup.h"
#include "projectitemmodel.h"
51
#include "projectsortproxymodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
52 53 54
#include "projectsubclip.h"
#include "titler/titlewidget.h"
#include "ui_qtextclip_ui.h"
Nicolas Carion's avatar
Nicolas Carion committed
55
#include "undohelper.hpp"
56
#include "xml/xml.hpp"
57

58
#include "xml/xml.hpp"
59

60
#include <KColorScheme>
61
#include <KMessageBox>
62
#include <KXMLGUIFactory>
Nicolas Carion's avatar
Nicolas Carion committed
63
#include <QToolBar>
64

Nicolas Carion's avatar
Nicolas Carion committed
65 66
#include "kdenlive_debug.h"
#include <QCryptographicHash>
67
#include <QDesktopServices>
68
#include <QDialogButtonBox>
69
#include <QDrag>
Nicolas Carion's avatar
Nicolas Carion committed
70
#include <QFile>
71
#include <QMenu>
Nicolas Carion's avatar
Nicolas Carion committed
72 73
#include <QSlider>
#include <QTimeLine>
74
#include <QUndoCommand>
Nicolas Carion's avatar
Nicolas Carion committed
75 76 77
#include <QUrl>
#include <QVBoxLayout>
#include <QtConcurrent>
Nicolas Carion's avatar
Nicolas Carion committed
78
#include <utility>
79 80 81 82 83 84 85 86 87 88
/**
 * @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
89

90
    {
91
        connect(this, &QStyledItemDelegate::closeEditor, [&]() { m_editorOpen = false; });
92
    }
93
    void setDar(double dar) { m_dar = dar; }
94 95 96 97 98 99 100
    void setEditorData(QWidget *w, const QModelIndex &i) const override
    {
        if (!m_editorOpen) {
            QStyledItemDelegate::setEditorData(w, i);
            m_editorOpen = true;
        }
    }
101
    bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
102
    {
103 104 105
        Q_UNUSED(model);
        Q_UNUSED(option);
        Q_UNUSED(index);
106
        if (event->type() == QEvent::MouseButtonPress) {
Nicolas Carion's avatar
Nicolas Carion committed
107
            auto *me = (QMouseEvent *)event;
108 109 110 111 112 113 114 115 116 117 118 119 120 121
            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;
    }
    void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override
    {
        if (index.column() != 0) {
122 123
            QStyledItemDelegate::updateEditorGeometry(editor, option, index);
            return;
124 125 126 127 128
        }
        QStyleOptionViewItem opt = option;
        initStyleOption(&opt, index);
        QRect r1 = option.rect;
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
129
        int decoWidth = 0;
130 131
        if (opt.decorationSize.height() > 0) {
            decoWidth += r1.height() * m_dar;
132
        }
133
        int mid = 0;
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
        if (type == AbstractProjectItem::ClipItem || type == AbstractProjectItem::SubClipItem) {
            mid = (int)((r1.height() / 2));
        }
        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;
        int width = fm.boundingRect(r, Qt::AlignLeft | Qt::AlignTop, text).width() + option.decorationSize.width() + 2 * textMargin;
        hint.setWidth(width);
        int type = index.data(AbstractProjectItem::ItemTypeRole).toInt();
        if (type == AbstractProjectItem::FolderItem || type == AbstractProjectItem::FolderUpItem) {
            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) {
            return QSize(hint.width(), qMax(option.fontMetrics.lineSpacing() * 2 + 4, qMin(hint.height(), (int)(option.decorationSize.height() / 1.5))));
        }
        QIcon icon = qvariant_cast<QIcon>(index.data(Qt::DecorationRole));
        QString line1 = index.data(Qt::DisplayRole).toString();
        QString line2 = index.data(Qt::UserRole).toString();

        int textW = qMax(option.fontMetrics.width(line1), option.fontMetrics.width(line2));
        QSize iconSize = icon.actualSize(option.decorationSize);
Nicolas Carion's avatar
Nicolas Carion committed
173
        return {qMax(textW, iconSize.width()) + 4, option.fontMetrics.lineSpacing() * 2 + 4};
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
    }

    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);
190
            if ((option.state & static_cast<int>(QStyle::State_Selected)) != 0) {
191 192 193 194 195 196 197 198 199
                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) {
200 201
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
202 203
                    r.setWidth(r.height() * m_dar);
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
204
                    // Draw icon
205 206 207
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
                }
                int mid = (int)((r1.height() / 2));
                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();
                if (!subText.isEmpty()) {
                    r2.adjust(0, bounding.bottom() - r2.top(), 0, 0);
                    QColor subTextColor = painter->pen().color();
                    subTextColor.setAlphaF(.5);
                    painter->setPen(subTextColor);
                    // Draw usage counter
                    int usage = index.data(AbstractProjectItem::UsageCount).toInt();
                    if (usage > 0) {
                        subText.append(QString().sprintf(" [%d]", usage));
                    }
                    painter->drawText(r2, Qt::AlignLeft | Qt::AlignTop, subText, &bounding);

                    // Add audio/video icons for selective drag
                    int cType = index.data(AbstractProjectItem::ClipType).toInt();
232 233
                    bool hasAudioAndVideo = index.data(AbstractProjectItem::ClipHasAudioAndVideo).toBool();
                    if (hasAudioAndVideo && (cType == ClipType::AV || cType == ClipType::Playlist) && (opt.state & QStyle::State_MouseOver)) {
234 235 236 237 238 239 240 241 242 243 244
                        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 {
245 246
                        //m_audioDragRect = QRect();
                        //m_videoDragRect = QRect();
247 248 249 250 251 252 253 254 255 256 257 258 259
                    }
                }
                if (type == AbstractProjectItem::ClipItem) {
                    // Overlay icon if necessary
                    QVariant v = index.data(AbstractProjectItem::IconOverlay);
                    if (!v.isNull()) {
                        QIcon reload = QIcon::fromTheme(v.toString());
                        r.setTop(r.bottom() - bounding.height());
                        r.setWidth(bounding.height());
                        reload.paint(painter, r);
                    }

                    int jobProgress = index.data(AbstractProjectItem::JobProgress).toInt();
Nicolas Carion's avatar
Nicolas Carion committed
260
                    auto status = index.data(AbstractProjectItem::JobStatus).value<JobManagerStatus>();
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
                    if (status == JobManagerStatus::Pending || status == JobManagerStatus::Running) {
                        // 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);
                        if (status == JobManagerStatus::Running) {
                            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 {
                // Folder or Folder Up items
290 291
                int decoWidth = 0;
                if (opt.decorationSize.height() > 0) {
292 293
                    r.setWidth(r.height() * m_dar);
                    QPixmap pix = opt.icon.pixmap(opt.icon.actualSize(r.size()));
294
                    // Draw icon
295 296 297
                    decoWidth += r.width() + textMargin;
                    r.setWidth(r.height() * pix.width() / pix.height());
                    painter->drawPixmap(r, pix, QRect(0, 0, pix.width(), pix.height()));
298 299 300 301 302 303 304 305 306 307
                }
                r1.adjust(decoWidth, 0, 0, 0);
                QRectF bounding;
                painter->drawText(r1, Qt::AlignLeft | Qt::AlignTop, index.data(AbstractProjectItem::DataName).toString(), &bounding);
            }
            painter->restore();
        } else {
            QStyledItemDelegate::paint(painter, option, index);
        }
    }
308 309

private:
Nicolas Carion's avatar
Nicolas Carion committed
310
    mutable bool m_editorOpen{false};
311 312
    mutable QRect m_audioDragRect;
    mutable QRect m_videoDragRect;
Nicolas Carion's avatar
Nicolas Carion committed
313
    double m_dar{1.778};
314 315

public:
Nicolas Carion's avatar
Nicolas Carion committed
316
    PlaylistState::ClipState dragType{PlaylistState::Disabled};
317 318
};

319 320
MyListView::MyListView(QWidget *parent)
    : QListView(parent)
321 322 323 324 325 326 327 328 329 330 331 332 333 334
{
    setViewMode(QListView::IconMode);
    setMovement(QListView::Static);
    setResizeMode(QListView::Adjust);
    setUniformItemSizes(true);
    setDragDropMode(QAbstractItemView::DragDrop);
    setAcceptDrops(true);
    setDragEnabled(true);
    viewport()->setAcceptDrops(true);
}

void MyListView::focusInEvent(QFocusEvent *event)
{
    QListView::focusInEvent(event);
335 336 337
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
338 339
}

340 341
MyTreeView::MyTreeView(QWidget *parent)
    : QTreeView(parent)
342 343 344
{
    setEditing(false);
}
345 346 347

void MyTreeView::mousePressEvent(QMouseEvent *event)
{
348
    QTreeView::mousePressEvent(event);
349 350
    if (event->button() == Qt::LeftButton) {
        m_startPos = event->pos();
351 352 353
        QModelIndex ix = indexAt(m_startPos);
        if (ix.isValid()) {
            QAbstractItemDelegate *del = itemDelegate(ix);
354
            m_dragType = static_cast<BinItemDelegate *>(del)->dragType;
355 356 357
        } else {
            m_dragType = PlaylistState::Disabled;
        }
358 359 360
    }
}

361 362 363
void MyTreeView::focusInEvent(QFocusEvent *event)
{
    QTreeView::focusInEvent(event);
364 365 366
    if (event->reason() == Qt::MouseFocusReason) {
        emit focusView();
    }
367 368
}

369
void MyTreeView::mouseMoveEvent(QMouseEvent *event)
370 371
{
    bool dragged = false;
372
    if ((event->buttons() & Qt::LeftButton) != 0u) {
373
        int distance = (event->pos() - m_startPos).manhattanLength();
374 375 376 377 378 379
        if (distance >= QApplication::startDragDistance()) {
            dragged = performDrag();
        }
    }
    if (!dragged) {
        QTreeView::mouseMoveEvent(event);
380 381 382
    }
}

383 384 385 386 387 388 389 390 391 392 393 394
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
395
bool MyTreeView::isEditing() const
396
{
397
    return state() == QAbstractItemView::EditingState;
398 399 400 401
}

void MyTreeView::setEditing(bool edit)
{
402
    setState(edit ? QAbstractItemView::EditingState : QAbstractItemView::NoState);
403 404
}

405 406 407 408 409 410 411 412 413
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);
        }
    }
414 415 416
    if (indexes.isEmpty()) {
        return false;
    }
417 418
    // Check if we want audio or video only
    emit updateDragMode(m_dragType);
Nicolas Carion's avatar
Nicolas Carion committed
419
    auto *drag = new QDrag(this);
420
    drag->setMimeData(model()->mimeData(indexes));
Laurent Montel's avatar
Laurent Montel committed
421
    QModelIndex ix = indexes.constFirst();
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446
    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();
    return true;
}

447 448
SmallJobLabel::SmallJobLabel(QWidget *parent)
    : QPushButton(parent)
Nicolas Carion's avatar
Nicolas Carion committed
449

450 451 452 453
{
    setFixedWidth(0);
    setFlat(true);
    m_timeLine = new QTimeLine(500, this);
Laurent Montel's avatar
Laurent Montel committed
454 455
    QObject::connect(m_timeLine, &QTimeLine::valueChanged, this, &SmallJobLabel::slotTimeLineChanged);
    QObject::connect(m_timeLine, &QTimeLine::finished, this, &SmallJobLabel::slotTimeLineFinished);
456 457 458 459 460
    hide();
}

const QString SmallJobLabel::getStyleSheet(const QPalette &p)
{
461
    KColorScheme scheme(p.currentColorGroup(), KColorScheme::Window);
462 463
    QColor bg = scheme.background(KColorScheme::LinkBackground).color();
    QColor fg = scheme.foreground(KColorScheme::LinkText).color();
Nicolas Carion's avatar
Nicolas Carion committed
464 465 466 467 468 469 470 471
    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());
472

473 474
    bg = scheme.background(KColorScheme::ActiveBackground).color();
    fg = scheme.foreground(KColorScheme::ActiveText).color();
Nicolas Carion's avatar
Nicolas Carion committed
475 476 477 478 479 480 481 482
    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
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 508 509 510 511
    return style;
}

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

void SmallJobLabel::slotTimeLineChanged(qreal value)
{
    setFixedWidth(qMin(value * 2, qreal(1.0)) * sizeHint().width());
    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
512
    QMutexLocker lk(&m_locker);
513 514 515 516
    if (jobCount > 0) {
        // prepare animation
        setText(i18np("%1 job", "%1 jobs", jobCount));
        setToolTip(i18np("%1 pending job", "%1 pending jobs", jobCount));
517

Nicolas Carion's avatar
Nicolas Carion committed
518
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
519 520 521 522
            setFixedWidth(sizeHint().width());
            m_action->setVisible(true);
            return;
        }
523

524 525 526 527 528
        if (m_action->isVisible()) {
            setFixedWidth(sizeHint().width());
            update();
            return;
        }
529

530 531 532 533 534 535 536 537
        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();
        }
538
    } else {
Nicolas Carion's avatar
Nicolas Carion committed
539
        if (style()->styleHint(QStyle::SH_Widget_Animate, nullptr, this) != 0) {
540 541 542 543 544 545 546 547 548 549 550 551
            setFixedWidth(0);
            m_action->setVisible(false);
            return;
        }
        // hide
        m_timeLine->setDirection(QTimeLine::Backward);
        if (m_timeLine->state() == QTimeLine::NotRunning) {
            m_timeLine->start();
        }
    }
}

552 553
LineEventEater::LineEventEater(QObject *parent)
    : QObject(parent)
554 555 556 557 558
{
}

bool LineEventEater::eventFilter(QObject *obj, QEvent *event)
{
559
    switch (event->type()) {
560 561 562 563 564 565 566 567 568 569 570
    case QEvent::ShortcutOverride:
        if (((QKeyEvent *)event)->key() == Qt::Key_Escape) {
            emit clearSearchLine();
        }
        break;
    case QEvent::Resize:
        // Workaround Qt BUG 54676
        emit showClearButton(((QResizeEvent *)event)->size().width() > QFontMetrics(QApplication::font()).averageCharWidth() * 8);
        break;
    default:
        break;
571 572 573 574
    }
    return QObject::eventFilter(obj, event);
}

Nicolas Carion's avatar
Nicolas Carion committed
575
Bin::Bin(std::shared_ptr<ProjectItemModel> model, QWidget *parent)
576 577
    : QWidget(parent)
    , isLoading(false)
Nicolas Carion's avatar
Nicolas Carion committed
578
    , m_itemModel(std::move(model))
579 580 581 582 583 584 585 586 587 588 589 590 591 592
    , m_itemView(nullptr)
    , m_doc(nullptr)
    , m_extractAudioAction(nullptr)
    , m_transcodeAction(nullptr)
    , m_clipsActionsMenu(nullptr)
    , m_inTimelineAction(nullptr)
    , m_listType((BinViewType)KdenliveSettings::binMode())
    , m_iconSize(160, 90)
    , m_propertiesPanel(nullptr)
    , m_blankThumb()
    , m_invalidClipDialog(nullptr)
    , m_gainedFocus(false)
    , m_audioDuration(0)
    , m_processedAudio(0)
593
{
594
    m_layout = new QVBoxLayout(this);
595 596

    // Create toolbar for buttons
597
    m_toolbar = new QToolBar(this);
598 599 600
    int size = style()->pixelMetric(QStyle::PM_SmallIconSize);
    QSize iconSize(size, size);
    m_toolbar->setIconSize(iconSize);
601
    m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
602
    m_layout->addWidget(m_toolbar);
603 604
    m_layout->setSpacing(0);
    m_layout->setContentsMargins(0, 0, 0, 0);
605 606 607
    // Search line
    m_proxyModel = new ProjectSortProxyModel(this);
    m_proxyModel->setDynamicSortFilter(true);
608 609
    m_searchLine = new QLineEdit(this);
    m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
Nicolas Carion's avatar
Nicolas Carion committed
610
    // m_searchLine->setClearButtonEnabled(true);
611 612
    m_searchLine->setPlaceholderText(i18n("Search"));
    m_searchLine->setFocusPolicy(Qt::ClickFocus);
Laurent Montel's avatar
Laurent Montel committed
613
    connect(m_searchLine, &QLineEdit::textChanged, m_proxyModel, &ProjectSortProxyModel::slotSetSearchString);
614

Nicolas Carion's avatar
Nicolas Carion committed
615
    auto *leventEater = new LineEventEater(this);
616
    m_searchLine->installEventFilter(leventEater);
Laurent Montel's avatar
Laurent Montel committed
617
    connect(leventEater, &LineEventEater::clearSearchLine, m_searchLine, &QLineEdit::clear);
618
    connect(leventEater, &LineEventEater::showClearButton, this, &Bin::showClearButton);
619

620
    setFocusPolicy(Qt::ClickFocus);
621

622
    connect(m_itemModel.get(), &ProjectItemModel::refreshPanel, this, &Bin::refreshPanel);
623
    connect(m_itemModel.get(), &ProjectItemModel::refreshAudioThumbs, this, &Bin::doRefreshAudioThumbs);
624 625 626
    connect(m_itemModel.get(), &ProjectItemModel::refreshClip, this, &Bin::refreshClip);
    connect(m_itemModel.get(), &ProjectItemModel::updateTimelineProducers, this, &Bin::updateTimelineProducers);
    connect(m_itemModel.get(), &ProjectItemModel::emitMessage, this, &Bin::emitMessage);
627

628
    // Connect models
629
    m_proxyModel->setSourceModel(m_itemModel.get());
Vincent Pinon's avatar
Vincent Pinon committed
630
    connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, m_proxyModel, &ProjectSortProxyModel::slotDataChanged);
Laurent Montel's avatar
Laurent Montel committed
631
    connect(m_proxyModel, &ProjectSortProxyModel::selectModel, this, &Bin::selectProxyModel);
Nicolas Carion's avatar
Nicolas Carion committed
632 633 634 635
    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,
            static_cast<void (Bin::*)(const QList<QUrl> &, const QModelIndex &)>(&Bin::slotItemDropped));
636
    connect(m_itemModel.get(), &ProjectItemModel::effectDropped, this, &Bin::slotEffectDropped);
637
    connect(m_itemModel.get(), &QAbstractItemModel::dataChanged, this, &Bin::slotItemEdited);
638
    connect(this, &Bin::refreshPanel, this, &Bin::doRefreshPanel);
639

640
    // Zoom slider
641
    QWidget *container = new QWidget(this);
Nicolas Carion's avatar
Nicolas Carion committed
642
    auto *lay = new QHBoxLayout;
643 644
    m_slider = new QSlider(Qt::Horizontal, this);
    m_slider->setMaximumWidth(100);
645
    m_slider->setMinimumWidth(40);
646
    m_slider->setRange(0, 10);
647
    m_slider->setValue(KdenliveSettings::bin_zoom());
Laurent Montel's avatar
Laurent Montel committed
648
    connect(m_slider, &QAbstractSlider::valueChanged, this, &Bin::slotSetIconSize);
Nicolas Carion's avatar
Nicolas Carion committed
649
    auto *tb1 = new QToolButton(this);
650
    tb1->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
651
    connect(tb1, &QToolButton::clicked, [&]() { m_slider->setValue(qMin(m_slider->value() + 1, m_slider->maximum())); });
Nicolas Carion's avatar
Nicolas Carion committed
652
    auto *tb2 = new QToolButton(this);
653
    tb2->setIcon(QIcon::fromTheme(QStringLiteral("zoom-out")));
654
    connect(tb2, &QToolButton::clicked, [&]() { m_slider->setValue(qMax(m_slider->value() - 1, m_slider->minimum())); });
655
    lay->addWidget(tb2);
656 657
    lay->addWidget(m_slider);
    lay->addWidget(tb1);
658
    container->setLayout(lay);
Nicolas Carion's avatar
Nicolas Carion committed
659
    auto *widgetslider = new QWidgetAction(this);
660
    widgetslider->setDefaultWidget(container);
661 662

    // View type
663
    KSelectAction *listType = new KSelectAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("View Mode"), this);
664
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode"), listType);
665
    QAction *treeViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), i18n("Tree View"));
666
    listType->addAction(treeViewAction);
667 668 669 670
    treeViewAction->setData(BinTreeView);
    if (m_listType == treeViewAction->data().toInt()) {
        listType->setCurrentAction(treeViewAction);
    }
671
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_tree"), treeViewAction);
672

673
    QAction *iconViewAction = listType->addAction(QIcon::fromTheme(QStringLiteral("view-list-icons")), i18n("Icon View"));
674 675 676 677
    iconViewAction->setData(BinIconView);
    if (m_listType == iconViewAction->data().toInt()) {
        listType->setCurrentAction(iconViewAction);
    }
678
    pCore->window()->actionCollection()->addAction(QStringLiteral("bin_view_mode_icon"), iconViewAction);
679 680

    QAction *disableEffects = new QAction(i18n("Disable Bin Effects"), this);
681
    connect(disableEffects, &QAction::triggered, [this](bool disable) { this->setBinEffectsEnabled(!disable); });
682
    disableEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite")));
683 684 685
    disableEffects->setData("disable_bin_effects");
    disableEffects->setCheckable(true);
    disableEffects->setChecked(false);
686
    pCore->window()->actionCollection()->addAction(QStringLiteral("disable_bin_effects"), disableEffects);
687

688 689 690 691
    m_renameAction = KStandardAction::renameFile(this, SLOT(slotRenameItem()), this);
    m_renameAction->setText(i18n("Rename"));
    m_renameAction->setData("rename");
    pCore->window()->actionCollection()->addAction(QStringLiteral("rename"), m_renameAction);
692

693
    listType->setToolBarMode(KSelectAction::MenuMode);
Nicolas Carion's avatar
Nicolas Carion committed
694
    connect(listType, static_cast<void (KSelectAction::*)(QAction *)>(&KSelectAction::triggered), this, &Bin::slotInitView);
695 696 697 698 699

    // Settings menu
    QMenu *settingsMenu = new QMenu(i18n("Settings"), this);
    settingsMenu->addAction(listType);
    QMenu *sliderMenu = new QMenu(i18n("Zoom"), this);
700
    sliderMenu->setIcon(QIcon::fromTheme(QStringLiteral("zoom-in")));
701 702
    sliderMenu->addAction(widgetslider);
    settingsMenu->addMenu(sliderMenu);
703

704 705 706
    // Column show / hide actions
    m_showDate = new QAction(i18n("Show date"), this);
    m_showDate->setCheckable(true);
Laurent Montel's avatar
Laurent Montel committed
707
    connect(m_showDate, &QAction::triggered, this, &Bin::slotShowDateColumn);
708 709
    m_showDesc = new QAction(i18n("Show description"), this);
    m_showDesc->setCheckable(true);
Laurent Montel's avatar
Laurent Montel committed
710
    connect(m_showDesc, &QAction::triggered, this, &Bin::slotShowDescColumn);
711 712
    settingsMenu->addAction(m_showDate);
    settingsMenu->addAction(m_showDesc);
713
    settingsMenu->addAction(disableEffects);
Nicolas Carion's avatar
Nicolas Carion committed
714
    auto *button = new QToolButton;
715
    button->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu")));
716
    button->setToolTip(i18n("Options"));
717 718 719
    button->setMenu(settingsMenu);
    button->setPopupMode(QToolButton::InstantPopup);
    m_toolbar->addWidget(button);
720

721 722 723
    // small info button for pending jobs
    m_infoLabel = new SmallJobLabel(this);
    m_infoLabel->setStyleSheet(SmallJobLabel::getStyleSheet(palette()));
724
    connect(pCore->jobManager().get(), &JobManager::jobCount, m_infoLabel, &SmallJobLabel::slotSetJobCount);
725 726
    QAction *infoAction = m_toolbar->addWidget(m_infoLabel);
    m_jobsMenu = new QMenu(this);
727
    // connect(m_jobsMenu, &QMenu::aboutToShow, this, &Bin::slotPrepareJobsMenu);
728 729 730 731
    m_cancelJobs = new QAction(i18n("Cancel All Jobs"), this);
    m_cancelJobs->setCheckable(false);
    m_discardCurrentClipJobs = new QAction(i18n("Cancel Current Clip Jobs"), this);
    m_discardCurrentClipJobs->setCheckable(false);
732 733
    m_discardPendingJobs = new QAction(i18n("Cancel Pending Jobs"), this);
    m_discardPendingJobs->setCheckable(false);
734 735
    m_jobsMenu->addAction(m_cancelJobs);
    m_jobsMenu->addAction(m_discardCurrentClipJobs);
736
    m_jobsMenu->addAction(m_discardPendingJobs);
737 738 739 740
    m_infoLabel->setMenu(m_jobsMenu);
    m_infoLabel->setAction(infoAction);

    // Hack, create toolbar spacer
741
    QWidget *spacer = new QWidget();
742 743 744 745
    spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    m_toolbar->addWidget(spacer);

    // Add search line
746
    m_toolbar->addWidget(m_searchLine);
747

748
    m_binTreeViewDelegate = new BinItemDelegate(this);
Nicolas Carion's avatar
Nicolas Carion committed
749
    // connect(pCore->projectManager(), SIGNAL(projectOpened(Project*)), this, SLOT(setProject(Project*)));
750
    m_headerInfo = QByteArray::fromBase64(KdenliveSettings::treeviewheaders().toLatin1());
751 752
    m_propertiesPanel = new QScrollArea(this);
    m_propertiesPanel->setFrameShape(QFrame::NoFrame);
753 754 755
    // Insert listview
    m_itemView = new MyTreeView(this);
    m_layout->addWidget(m_itemView);
756
    // Info widget for failed jobs, other errors
757
    m_infoMessage = new KMessageWidget(this);
758
    m_layout->addWidget(m_infoMessage);
759
    m_infoMessage->setCloseButtonVisible(false);
760
    connect(m_infoMessage, &KMessageWidget::hideAnimationFinished, this, &Bin::slotResetInfoMessage);
Nicolas Carion's avatar
Nicolas Carion committed
761
    // m_infoMessage->setWordWrap(true);
762
    m_infoMessage->hide();
Vincent Pinon's avatar
Vincent Pinon committed
763
    connect(this, &Bin::requesteInvalidRemoval, this, &Bin::slotQueryRemoval);
Laurent Montel's avatar
Laurent Montel committed
764
    connect(this, SIGNAL(displayBinMessage(QString, KMessageWidget::MessageType)), this, SLOT(doDisplayMessage(QString, KMessageWidget::MessageType)));
765 766 767 768
}

Bin::~Bin()
{
769
    blockSignals(true);
770
    m_proxyModel->selectionModel()->blockSignals(true);
771
    setEnabled(false);
772
    m_propertiesPanel = nullptr;
773
    abortOperations();
774
    m_itemModel->clean();
775 776
}

777 778 779 780 781
QDockWidget *Bin::clipPropertiesDock()
{
    return m_propertiesDock;
}

782 783
void Bin::abortOperations()
{
784
    m_infoMessage->hide();
785 786
    blockSignals(true);
    if (m_propertiesPanel) {
Nicolas Carion's avatar
Nicolas Carion committed
787
        for (QWidget *w : m_propertiesPanel->findChildren<ClipPropertiesController *>()) {
788 789 790 791
            delete w;
        }
    }
    delete m_itemView;
Laurent Montel's avatar
Laurent Montel committed
792
    m_itemView = nullptr;
793 794 795
    blockSignals(false);
}

796 797
bool Bin::eventFilter(QObject *obj, QEvent *event)
{
798
    if (event->type() == QEvent::MouseButtonRelease) {
799 800 801
        if (!m_monitor->isActive()) {
            m_monitor->slotActivateMonitor();
        }
802 803
        bool success = QWidget::eventFilter(obj, event);
        if (m_gainedFocus) {
Nicolas Carion's avatar
Nicolas Carion committed
804 805
            auto *mouseEvent = static_cast<QMouseEvent *>(event);
            auto *view = qobject_cast<QAbstractItemView *>(obj->parent());
806 807
            if (view) {
                QModelIndex idx = view->indexAt(mouseEvent->pos());
808
                m_gainedFocus = false;
809
                if (idx.isValid()) {
810
                    std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(idx));
811 812 813
                    editMasterEffect(item);
                } else {
                    editMasterEffect(nullptr);
814 815 816 817 818 819
                }
            }
            // make sure we discard the focus indicator
            m_gainedFocus = false;
        }
        return success;
820 821
    }
    if (event->type() == QEvent::MouseButtonDblClick) {
Nicolas Carion's avatar
Nicolas Carion committed
822 823
        auto *mouseEvent = static_cast<QMouseEvent *>(event);
        auto *view = qobject_cast<QAbstractItemView *>(obj->parent());
824 825 826 827 828
        if (view) {
            QModelIndex idx = view->indexAt(mouseEvent->pos());
            if (!idx.isValid()) {
                // User double clicked on empty area
                slotAddClip();
829
            } else {
830 831
                slotItemDoubleClicked(idx, mouseEvent->pos());
            }
832 833
        } else {
            qCDebug(KDENLIVE_LOG) << " +++++++ NO VIEW-------!!";
834 835
        }
        return true;
Nicolas Carion's avatar
Nicolas Carion committed
836 837
    }
    if (event->type() == QEvent::Wheel) {
Nicolas Carion's avatar
Nicolas Carion committed
838
        auto *e = static_cast<QWheelEvent *>(event);
Nicolas Carion's avatar
Nicolas Carion committed
839
        if ((e != nullptr) && e->modifiers() == Qt::ControlModifier) {
840
            slotZoomView(e->delta() > 0);
Nicolas Carion's avatar
Nicolas Carion committed
841
            // emit zoomView(e->delta() > 0);
842 843 844
            return true;
        }
    }
845
    return QWidget::eventFilter(obj, event);
846 847
}

848 849 850 851 852 853
void Bin::refreshIcons()
{
    QList<QMenu *> allMenus = this->findChildren<QMenu *>();
    for (int i = 0; i < allMenus.count(); i++) {
        QMenu *m = allMenus.at(i);
        QIcon ic = m->icon();
854 855 856
        if (ic.isNull() || ic.name().isEmpty()) {
            continue;
        }
857
        QIcon newIcon = QIcon::fromTheme(ic.name());
858 859 860 861 862 863
        m->setIcon(newIcon);
    }
    QList<QToolButton *> allButtons = this->findChildren<QToolButton *>();
    for (int i = 0; i < allButtons.count(); i++) {
        QToolButton *m = allButtons.at(i);
        QIcon ic = m->icon();
864 865 866
        if (ic.isNull() || ic.name().isEmpty()) {
            continue;
        }
867
        QIcon newIcon = QIcon::fromTheme(ic.name());
868 869 870 871
        m->setIcon(newIcon);
    }
}

872 873
void Bin::slotSaveHeaders()
{
Nicolas Carion's avatar
Nicolas Carion committed
874
    if ((m_itemView != nullptr) && m_listType == BinTreeView) {
875
        // save current treeview state (column width)
Nicolas Carion's avatar
Nicolas Carion committed
876
        auto *view = static_cast<QTreeView *>(m_itemView);
877 878 879 880 881
        m_headerInfo = view->header()->saveState();
        KdenliveSettings::setTreeviewheaders(m_headerInfo.toBase64());
    }
}

882 883 884
void Bin::slotZoomView(bool zoomIn)
{
    if (m_itemModel->rowCount() == 0) {
Nicolas Carion's avatar
Nicolas Carion committed
885
        // Don't zoom on empty bin
886 887
        return;
    }
Nicolas Carion's avatar
Nicolas Carion committed
888
    int progress = (zoomIn) ? 1 : -1;
889 890 891
    m_slider->setValue(m_slider->value() + progress);
}

892 893 894 895 896
Monitor *Bin::monitor()
{
    return m_monitor;
}

Laurent Montel's avatar
Laurent Montel committed
897
const QStringList Bin::getFolderInfo(const QModelIndex &selectedIx)
898
{
899 900 901 902 903 904
    QModelIndexList indexes;
    if (selectedIx.isValid()) {
        indexes << selectedIx;
    } else {
        indexes = m_proxyModel->selectionModel()->selectedIndexes();
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
905
    if (indexes.isEmpty()) {
906
        // return root folder info
907
        QStringList folderInfo;
908 909
        folderInfo << QString::number(-1);
        folderInfo << QString();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
910 911
        return folderInfo;
    }
Laurent Montel's avatar
Laurent Montel committed
912
    QModelIndex ix = indexes.constFirst();
913
    if (ix.isValid() && (m_proxyModel->selectionModel()->isSelected(ix) || selectedIx.isValid())) {
914
        return m_itemModel->getEnclosingFolderInfo(m_proxyModel->mapToSource(ix));
Nicolas Carion's avatar
Nicolas Carion committed
915 916 917 918 919 920
    }
    // return root folder info
    QStringList folderInfo;
    folderInfo << QString::number(-1);
    folderInfo << QString();
    return folderInfo;
921 922 923 924 925
}

void Bin::slotAddClip()
{
    // Check if we are in a folder
926 927
    QString parentFolder = getCurrentFolder();
    ClipCreationDialog::createClipsCommand(m_doc, parentFolder, m_itemModel);
928 929
}

930
std::shared_ptr<ProjectClip> Bin::getFirstSelectedClip()
931
{
Laurent Montel's avatar
Laurent Montel committed
932
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
933
    if (indexes.isEmpty()) {
934
        return std::shared_ptr<ProjectClip>();
935
    }
Laurent Montel's avatar
Laurent Montel committed
936
    for (const QModelIndex &ix : indexes) {
937
        std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
938
        if (item->itemType() == AbstractProjectItem::ClipItem) {
939
            auto clip = std::static_pointer_cast<ProjectClip>(item);
940 941 942
            if (clip) {
                return clip;
            }
943 944
        }
    }
Laurent Montel's avatar
Laurent Montel committed
945
    return nullptr;
946 947
}

948 949
void Bin::slotDeleteClip()
{
Nicolas Carion's avatar
Nicolas Carion committed
950 951 952 953
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
    std::vector<std::shared_ptr<AbstractProjectItem>> items;
    bool included = false;
    bool usedFolder = false;
954 955 956
    auto checkInclusion = [](bool accum, std::shared_ptr<TreeItem> item) {
        return accum || std::static_pointer_cast<AbstractProjectItem>(item)->isIncludedInTimeline();
    };
Nicolas Carion's avatar
Nicolas Carion committed
957 958 959 960 961 962
    for (const QModelIndex &ix : indexes) {
        if (!ix.isValid() || ix.column() != 0) {
            continue;
        }
        std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
        if (!item) {
Nicolas Carion's avatar
Nicolas Carion committed
963
            qDebug() << "Suspicious: item not found when trying to delete";
Nicolas Carion's avatar
Nicolas Carion committed
964 965 966 967 968 969 970 971 972 973 974 975 976 977 978
            continue;
        }
        included = included || item->accumulate(false, checkInclusion);
        // Check if we are deleting non-empty folders:
        usedFolder = usedFolder || item->childCount() > 0;
        items.push_back(item);
    }
    if (included && (KMessageBox::warningContinueCancel(this, i18n("This will delete all selected clips from timeline")) != KMessageBox::Continue)) {
        return;
    }
    if (usedFolder && (KMessageBox::warningContinueCancel(this, i18n("This will delete all folder content")) != KMessageBox::Continue)) {
        return;
    }
    Fun undo = []() { return true; };
    Fun redo = []() { return true; };
Nicolas Carion's avatar
Nicolas Carion committed
979
    for (const auto &item : items) {
980
        m_itemModel->requestBinClipDeletion(item, undo, redo);
Nicolas Carion's avatar
Nicolas Carion committed
981 982
    }
    pCore->pushUndo(undo, redo, i18n("Delete bin Clips"));
983 984 985 986
}

void Bin::slotReloadClip()
{
Laurent Montel's avatar
Laurent Montel committed
987
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
988
    for (const QModelIndex &ix : indexes) {
989
        if (!ix.isValid() || ix.column() != 0) {
990 991
            continue;
        }
992
        std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
993 994 995 996 997 998
        std::shared_ptr<ProjectClip> currentItem = nullptr;
        if (item->itemType() == AbstractProjectItem::ClipItem) {
            currentItem = std::static_pointer_cast<ProjectClip>(item);
        } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
            currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
        }
999
        if (currentItem) {
1000
            emit openClip(std::shared_ptr<ProjectClip>());
1001
            if (currentItem->clipType() == ClipType::Playlist) {
Nicolas Carion's avatar
Nicolas Carion committed
1002
                // Check if a clip inside playlist is missing
1003
                QString path = currentItem->url();
1004 1005 1006 1007 1008
                QFile f(path);
                QDomDocument doc;
                doc.setContent(&f, false);
                f.close();
                DocumentChecker d(QUrl::fromLocalFile(path), doc);
1009
                if (!d.hasErrorInClips() && doc.documentElement().hasAttribute(QStringLiteral("modified"))) {
1010 1011 1012 1013 1014 1015 1016 1017 1018
                    QString backupFile = path + QStringLiteral(".backup");
                    KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile));
                    if (copyjob->exec()) {
                        if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
                            KMessageBox::sorry(this, i18n("Unable to write to file %1", path));
                        } else {
                            QTextStream out(&f);
                            out << doc.toString();
                            f.close();
Nicolas Carion's avatar
Nicolas Carion committed
1019 1020
                            KMessageBox::information(
                                this,
Pino Toscano's avatar
Pino Toscano committed
1021
                                i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.",
Nicolas Carion's avatar
Nicolas Carion committed
1022
                                     backupFile));
1023 1024 1025
                        }
                    }
                }
1026
            }
1027
            currentItem->reloadProducer(false);
1028 1029 1030 1031
        }
    }
}

1032 1033
void Bin::slotLocateClip()
{
1034 1035
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
    for (const QModelIndex &ix : indexes) {
1036 1037 1038
        if (!ix.isValid() || ix.column() != 0) {
            continue;
        }
1039
        std::shared_ptr<AbstractProjectItem> item = m_itemModel->getBinItemByIndex(m_proxyModel->mapToSource(ix));
1040 1041 1042 1043 1044 1045
        std::shared_ptr<ProjectClip> currentItem = nullptr;
        if (item->itemType() == AbstractProjectItem::ClipItem) {
            currentItem = std::static_pointer_cast<ProjectClip>(item);
        } else if (item->itemType() == AbstractProjectItem::SubClipItem) {
            currentItem = std::static_pointer_cast<ProjectSubClip>(item)->getMasterClip();
        }
1046
        if (currentItem) {
1047
            QUrl url = QUrl::fromLocalFile(currentItem->url()).adjusted(QUrl::RemoveFilename);
1048 1049 1050 1051 1052
            bool exists = QFile(url.toLocalFile()).exists();
            if (currentItem->hasUrl() && exists) {
                QDesktopServices::openUrl(url);
                qCDebug(KDENLIVE_LOG) << "  / / " + url.toString();
            } else {
Nicolas Carion's avatar
Nicolas Carion committed
1053
                if (!exists) {
Pino Toscano's avatar
Pino Toscano committed
1054
                    emitMessage(i18n("Could not locate %1", url.toString()), 100, ErrorMessage);
1055 1056 1057
                }
                return;
            }
1058 1059 1060 1061
        }
    }
}

1062 1063
void Bin::slotDuplicateClip()
{
1064 1065
    const QModelIndexList indexes = m_proxyModel->selectionModel()->selectedIndexes();
    for (const QModelIndex &ix : indexes) {
1066