File indexing completed on 2024-05-19 04:53:43

0001 /*
0002     SPDX-FileCopyrightText: 2017 Nicolas Carion
0003     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "assetlistwidget.hpp"
0007 #include "assets/assetlist/model/assetfilter.hpp"
0008 #include "assets/assetlist/model/assettreemodel.hpp"
0009 #include "assets/assetlist/view/asseticonprovider.hpp"
0010 #include "mltconnection.h"
0011 
0012 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0013 #include <KNS3/Entry>
0014 #else
0015 #include <KNSCore/Entry>
0016 #endif
0017 
0018 #include <KNSWidgets/Action>
0019 #include <QAction>
0020 #include <QFontDatabase>
0021 #include <QKeyEvent>
0022 #include <QLineEdit>
0023 #include <QMenu>
0024 #include <QSplitter>
0025 #include <QStandardPaths>
0026 #include <QStyledItemDelegate>
0027 #include <QTextBrowser>
0028 #include <QTextDocument>
0029 #include <QToolBar>
0030 #include <QVBoxLayout>
0031 
0032 class AssetDelegate : public QStyledItemDelegate
0033 {
0034 public:
0035     using QStyledItemDelegate::QStyledItemDelegate;
0036 
0037 protected:
0038     void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
0039     {
0040         int favorite = index.data(AssetTreeModel::FavoriteRole).toInt();
0041         QStyledItemDelegate::initStyleOption(option, index);
0042         if (favorite > 0) {
0043             QFont font(option->font);
0044             /*modify fonts*/
0045             font.setBold(true);
0046             option->font = font;
0047             option->fontMetrics = QFontMetrics(font);
0048         }
0049     }
0050 };
0051 
0052 AssetListWidget::AssetListWidget(bool isEffect, QWidget *parent)
0053     : QWidget(parent)
0054     , m_isEffect(isEffect)
0055 {
0056     m_lay = new QVBoxLayout(this);
0057     m_lay->setContentsMargins(0, 0, 0, 0);
0058     m_lay->setSpacing(0);
0059     m_contextMenu = new QMenu(this);
0060     QAction *addFavorite = new QAction(i18n("Add to favorites"), this);
0061     addFavorite->setData(QStringLiteral("favorite"));
0062     connect(addFavorite, &QAction::triggered, this, [this]() { setFavorite(m_effectsTree->currentIndex(), !isFavorite(m_effectsTree->currentIndex())); });
0063     m_contextMenu->addAction(addFavorite);
0064     if (m_isEffect) {
0065         // Delete effect
0066         QAction *customAction = new QAction(i18n("Delete custom effect"), this);
0067         customAction->setData(QStringLiteral("custom"));
0068         connect(customAction, &QAction::triggered, this, [this]() { deleteCustomEffect(m_effectsTree->currentIndex()); });
0069         m_contextMenu->addAction(customAction);
0070         // reload effect
0071         customAction = new QAction(i18n("Reload custom effect"), this);
0072         customAction->setData(QStringLiteral("custom"));
0073         connect(customAction, &QAction::triggered, this, [this]() { reloadCustomEffectIx(m_effectsTree->currentIndex()); });
0074         m_contextMenu->addAction(customAction);
0075         // Edit effect
0076         customAction = new QAction(i18n("Edit Info…"), this);
0077         customAction->setData(QStringLiteral("custom"));
0078         connect(customAction, &QAction::triggered, this, [this]() { editCustomAsset(m_effectsTree->currentIndex()); });
0079         m_contextMenu->addAction(customAction);
0080         // Edit effect
0081         customAction = new QAction(i18n("Export XML…"), this);
0082         customAction->setData(QStringLiteral("custom"));
0083         connect(customAction, &QAction::triggered, this, [this]() { exportCustomEffect(m_effectsTree->currentIndex()); });
0084         m_contextMenu->addAction(customAction);
0085     }
0086     // Create toolbar for buttons
0087     m_toolbar = new QToolBar(this);
0088     int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize);
0089     m_toolbar->setIconSize(QSize(iconSize, iconSize));
0090     m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
0091     QAction *allEffects = new QAction(this);
0092     allEffects->setIcon(QIcon::fromTheme(QStringLiteral("show-all-effects")));
0093     allEffects->setToolTip(m_isEffect ? i18n("Main effects") : i18n("Main compositions"));
0094     connect(allEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral()); });
0095     m_toolbar->addAction(allEffects);
0096     if (m_isEffect) {
0097         QAction *videoEffects = new QAction(this);
0098         videoEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video")));
0099         videoEffects->setToolTip(i18n("Show all video effects"));
0100         connect(videoEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("video")); });
0101         m_toolbar->addAction(videoEffects);
0102         QAction *audioEffects = new QAction(this);
0103         audioEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio")));
0104         audioEffects->setToolTip(i18n("Show all audio effects"));
0105         connect(audioEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("audio")); });
0106         m_toolbar->addAction(audioEffects);
0107         QAction *customEffects = new QAction(this);
0108         customEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-custom-effect")));
0109         customEffects->setToolTip(i18n("Show all custom effects"));
0110         connect(customEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("custom")); });
0111         m_toolbar->addAction(customEffects);
0112     } else {
0113         QAction *transOnly = new QAction(this);
0114         transOnly->setIcon(QIcon::fromTheme(QStringLiteral("transform-move-horizontal")));
0115         transOnly->setToolTip(i18n("Show transitions only"));
0116         connect(transOnly, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("transition")); });
0117         m_toolbar->addAction(transOnly);
0118     }
0119     QAction *favEffects = new QAction(this);
0120     favEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite")));
0121     favEffects->setToolTip(i18n("Show favorite items"));
0122     connect(favEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("favorites")); });
0123     m_toolbar->addAction(favEffects);
0124     m_lay->addWidget(m_toolbar);
0125     if (m_isEffect) {
0126         KNSWidgets::Action *downloadAction = new KNSWidgets::Action(i18n("Download New Effects..."), QStringLiteral(":data/kdenlive_effects.knsrc"), this);
0127         m_toolbar->addAction(downloadAction);
0128         connect(downloadAction, &KNSWidgets::Action::dialogFinished, this, [&](const QList<KNSCore::Entry> &changedEntries) {
0129             if (changedEntries.count() > 0) {
0130                 for (auto &ent : changedEntries) {
0131 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0132                     if (ent.status() == KNS3::Entry::Status::Deleted) {
0133 #else
0134                     if (ent.status() == KNSCore::Entry::Status::Deleted) {
0135 #endif
0136                         reloadTemplates();
0137                     } else {
0138                         QStringList files = ent.installedFiles();
0139                         for (auto &f : files) {
0140                             reloadCustomEffect(f);
0141                         }
0142                     }
0143                 }
0144                 if (!m_searchLine->text().isEmpty()) {
0145                     setFilterName(m_searchLine->text());
0146                 }
0147             }
0148         });
0149     } else {
0150         KNSWidgets::Action *downloadAction = new KNSWidgets::Action(i18n("Download New Wipes..."), QStringLiteral(":data/kdenlive_wipes.knsrc"), this);
0151         m_toolbar->addAction(downloadAction);
0152         connect(downloadAction, &KNSWidgets::Action::dialogFinished, this, [&](const QList<KNSCore::Entry> &changedEntries) {
0153             if (changedEntries.count() > 0) {
0154                 MltConnection::refreshLumas();
0155             }
0156         });
0157     }
0158     QWidget *empty = new QWidget(this);
0159     empty->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
0160     m_toolbar->addWidget(empty);
0161     QAction *showInfo = new QAction(this);
0162     showInfo->setCheckable(true);
0163     showInfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about")));
0164     showInfo->setToolTip(m_isEffect ? i18n("Show/hide description of the effects") : i18n("Show/hide description of the compositions"));
0165     m_toolbar->addAction(showInfo);
0166     m_lay->addWidget(m_toolbar);
0167 
0168     // Search line
0169     m_searchLine = new QLineEdit(this);
0170     m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
0171     // m_searchLine->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0172     m_searchLine->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
0173     m_searchLine->setClearButtonEnabled(true);
0174     m_searchLine->setPlaceholderText(i18n("Search…"));
0175     m_searchLine->setFocusPolicy(Qt::ClickFocus);
0176     m_searchLine->installEventFilter(this);
0177     connect(m_searchLine, &QLineEdit::textChanged, this, [this](const QString &str) { setFilterName(str); });
0178     m_lay->addWidget(m_searchLine);
0179 
0180     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
0181     setAcceptDrops(true);
0182     m_effectsTree = new QTreeView(this);
0183     m_effectsTree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0184     m_effectsTree->setHeaderHidden(true);
0185     m_effectsTree->setAlternatingRowColors(true);
0186     m_effectsTree->setRootIsDecorated(true);
0187     m_effectsTree->setDragEnabled(true);
0188     m_effectsTree->setDragDropMode(QAbstractItemView::DragDrop);
0189     m_effectsTree->setItemDelegate(new AssetDelegate);
0190     connect(m_effectsTree, &QTreeView::doubleClicked, this, &AssetListWidget::activate);
0191     m_effectsTree->installEventFilter(this);
0192     m_effectsTree->setContextMenuPolicy(Qt::CustomContextMenu);
0193     connect(m_effectsTree, &QTreeView::customContextMenuRequested, this, &AssetListWidget::onCustomContextMenu);
0194     auto *viewSplitter = new QSplitter(Qt::Vertical, this);
0195     viewSplitter->addWidget(m_effectsTree);
0196     QTextBrowser *textEdit = new QTextBrowser(this);
0197     textEdit->setReadOnly(true);
0198     textEdit->setAcceptRichText(true);
0199     textEdit->setOpenExternalLinks(true);
0200     m_infoDocument = new QTextDocument(this);
0201     textEdit->setDocument(m_infoDocument);
0202     viewSplitter->addWidget(textEdit);
0203     m_lay->addWidget(viewSplitter);
0204     viewSplitter->setStretchFactor(0, 4);
0205     viewSplitter->setStretchFactor(1, 2);
0206     viewSplitter->setSizes({50, 0});
0207     connect(showInfo, &QAction::triggered, this, [showInfo, viewSplitter]() {
0208         if (showInfo->isChecked()) {
0209             viewSplitter->setSizes({50, 20});
0210         } else {
0211             viewSplitter->setSizes({50, 0});
0212         }
0213     });
0214 }
0215 
0216 AssetListWidget::~AssetListWidget() {}
0217 
0218 bool AssetListWidget::eventFilter(QObject *watched, QEvent *event)
0219 {
0220     // To avoid shortcut conflicts between the media browser and main app, we dis/enable actions when we gain/lose focus
0221     switch (event->type()) {
0222     case QEvent::ShortcutOverride:
0223         if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
0224             setFilterName(QString());
0225             m_effectsTree->setFocus();
0226         }
0227         break;
0228     case QEvent::KeyPress: {
0229         const auto key = static_cast<QKeyEvent *>(event)->key();
0230         if (key == Qt::Key_Return || key == Qt::Key_Enter) {
0231             activate(m_effectsTree->currentIndex());
0232             setFilterName(QString());
0233             event->accept();
0234             return true;
0235         } else if (watched == m_searchLine && (key == Qt::Key_Down || key == Qt::Key_Up)) {
0236             QModelIndex current = m_effectsTree->currentIndex();
0237             QModelIndex ix;
0238             if (key == Qt::Key_Down) {
0239                 ix = current.siblingAtRow(current.row() + 1);
0240                 if (!ix.isValid()) {
0241                     ix = current.parent().siblingAtRow(current.parent().row() + 1);
0242                     if (ix.isValid()) {
0243                         ix = m_proxyModel->index(0, 0, ix);
0244                     }
0245                 }
0246             } else {
0247                 if (current.row() > 0) {
0248                     ix = current.siblingAtRow(current.row() - 1);
0249                 } else {
0250                     ix = current.parent().siblingAtRow(current.parent().row() - 1);
0251                     if (ix.isValid()) {
0252                         QModelIndex child = m_proxyModel->index(0, 0, ix);
0253                         int row = 1;
0254                         while (child.isValid()) {
0255                             ix = child;
0256                             child = child.siblingAtRow(row);
0257                             row++;
0258                         }
0259                     }
0260                 }
0261             }
0262             if (ix.isValid()) {
0263                 m_effectsTree->setCurrentIndex(ix);
0264             }
0265         }
0266         break;
0267     }
0268     default:
0269         break;
0270     }
0271     return QObject::eventFilter(watched, event);
0272 }
0273 
0274 QString AssetListWidget::getName(const QModelIndex &index) const
0275 {
0276     return m_model->getName(m_proxyModel->mapToSource(index));
0277 }
0278 
0279 bool AssetListWidget::isFavorite(const QModelIndex &index) const
0280 {
0281     return m_model->isFavorite(m_proxyModel->mapToSource(index));
0282 }
0283 
0284 void AssetListWidget::setFavorite(const QModelIndex &index, bool favorite)
0285 {
0286     m_model->setFavorite(m_proxyModel->mapToSource(index), favorite, isEffect());
0287     Q_EMIT m_proxyModel->dataChanged(index, index, QVector<int>());
0288     m_proxyModel->reloadFilterOnFavorite();
0289     Q_EMIT reloadFavorites();
0290 }
0291 
0292 void AssetListWidget::deleteCustomEffect(const QModelIndex &index)
0293 {
0294     m_model->deleteEffect(m_proxyModel->mapToSource(index));
0295 }
0296 
0297 QString AssetListWidget::getDescription(const QModelIndex &index) const
0298 {
0299     return m_model->getDescription(isEffect(), m_proxyModel->mapToSource(index));
0300 }
0301 
0302 void AssetListWidget::setFilterName(const QString &pattern)
0303 {
0304     QItemSelectionModel *sel = m_effectsTree->selectionModel();
0305     QModelIndex current = m_proxyModel->getModelIndex(sel->currentIndex());
0306     m_proxyModel->setFilterName(!pattern.isEmpty(), pattern);
0307     if (!pattern.isEmpty()) {
0308         QVariantList mapped = m_proxyModel->getCategories();
0309         for (auto &ix : mapped) {
0310             m_effectsTree->setExpanded(ix.toModelIndex(), true);
0311         }
0312         QModelIndex ix = m_proxyModel->firstVisibleItem(m_proxyModel->mapFromSource(current));
0313         sel->setCurrentIndex(ix, QItemSelectionModel::ClearAndSelect);
0314         m_effectsTree->scrollTo(ix);
0315     } else {
0316         m_effectsTree->scrollTo(m_proxyModel->mapFromSource(current));
0317     }
0318 }
0319 
0320 QVariantMap AssetListWidget::getMimeData(const QString &assetId) const
0321 {
0322     QVariantMap mimeData;
0323     mimeData.insert(getMimeType(assetId), assetId);
0324     if (isAudio(assetId)) {
0325         mimeData.insert(QStringLiteral("type"), QStringLiteral("audio"));
0326     }
0327     return mimeData;
0328 }
0329 
0330 void AssetListWidget::activate(const QModelIndex &ix)
0331 {
0332     if (!ix.isValid()) {
0333         return;
0334     }
0335     const QString assetId = m_model->data(m_proxyModel->mapToSource(ix), AssetTreeModel::IdRole).toString();
0336     if (assetId != QLatin1String("root")) {
0337         Q_EMIT activateAsset(getMimeData(assetId));
0338     } else {
0339         m_effectsTree->setExpanded(ix, !m_effectsTree->isExpanded(ix));
0340     }
0341 }
0342 
0343 bool AssetListWidget::showDescription() const
0344 {
0345     return KdenliveSettings::showeffectinfo();
0346 }
0347 
0348 void AssetListWidget::setShowDescription(bool show)
0349 {
0350     KdenliveSettings::setShoweffectinfo(show);
0351     Q_EMIT showDescriptionChanged();
0352 }
0353 
0354 void AssetListWidget::onCustomContextMenu(const QPoint &pos)
0355 {
0356     QModelIndex index = m_effectsTree->indexAt(pos);
0357     if (index.isValid()) {
0358         const QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
0359         const QString assetId = m_model->data(sourceIndex, AssetTreeModel::IdRole).toString();
0360         if (assetId != QStringLiteral("root")) {
0361             QList<QAction *> actions = m_contextMenu->actions();
0362             bool isFavorite = m_model->data(sourceIndex, AssetTreeModel::FavoriteRole).toBool();
0363             auto itemType = m_model->data(sourceIndex, AssetTreeModel::TypeRole).value<AssetListType::AssetType>();
0364             bool isCustom = itemType == AssetListType::AssetType::Custom || itemType == AssetListType::AssetType::CustomAudio;
0365             for (auto &ac : actions) {
0366                 if (ac->data().toString() == QLatin1String("custom")) {
0367                     ac->setEnabled(isCustom);
0368                 } else if (ac->data().toString() == QLatin1String("favorite")) {
0369                     ac->setText(isFavorite ? i18n("Remove from favorites") : i18n("Add to favorites"));
0370                 }
0371             }
0372             m_contextMenu->exec(m_effectsTree->viewport()->mapToGlobal(pos));
0373         }
0374     }
0375 }
0376 
0377 void AssetListWidget::updateAssetInfo(const QModelIndex &current, const QModelIndex &)
0378 {
0379     if (current.isValid()) {
0380         QString description = getDescription(current);
0381         const QString id = m_model->data(m_proxyModel->mapToSource(current), AssetTreeModel::IdRole).toString();
0382         if (id.isEmpty() || id == QStringLiteral("root")) {
0383             m_infoDocument->clear();
0384             return;
0385         }
0386         auto type = m_model->data(m_proxyModel->mapToSource(current), AssetTreeModel::TypeRole).value<AssetListType::AssetType>();
0387         // Add link to our documentation
0388         const QString link = buildLink(id, type);
0389         if (!description.isEmpty()) {
0390             description.append(QString("<br/><a title=\"%1\" href=\"%2\">&#128279; %3</a>").arg(i18nc("@info:tooltip", "Online documentation"), link, id));
0391         } else {
0392             description = QString("<a title=\"%1\" href=\"%2\">&#128279; %3</a>").arg(i18nc("@info:tooltip", "Online documentation"), link, id);
0393         }
0394         m_infoDocument->setHtml(description);
0395     } else {
0396         m_infoDocument->clear();
0397     }
0398 }
0399 
0400 const QString AssetListWidget::buildLink(const QString id, AssetListType::AssetType type) const
0401 {
0402     QString prefix;
0403     if (type == AssetListType::AssetType::Audio) {
0404         prefix = QStringLiteral("effect_link");
0405     } else if (type == AssetListType::AssetType::Video) {
0406         prefix = QStringLiteral("effect_link");
0407     } else if (type == AssetListType::AssetType::AudioComposition || type == AssetListType::AssetType::AudioTransition) {
0408         prefix = QStringLiteral("transition_link");
0409     } else if (type == AssetListType::AssetType::VideoShortComposition || type == AssetListType::AssetType::VideoComposition ||
0410                type == AssetListType::AssetType::VideoTransition) {
0411         prefix = QStringLiteral("transition_link");
0412     } else {
0413         prefix = QStringLiteral("other");
0414     }
0415     return QStringLiteral("https://docs.kdenlive.org/%1/%2").arg(prefix).arg(id);
0416 }